hakari/verify/mod.rs
1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Code related to ensuring that `hakari` works properly.
5//!
6//! # Verification algorithm
7//!
8//! By default, Hakari runs in "generate mode": the goal of this mode is to update an existing
9//! Hakari package's TOML. In this mode, the Hakari package is always omitted from
10//! consideration and added to the omitted packages.
11//!
12//! In verify mode, the goal is to ensure that Cargo builds actually produce a unique set of
13//! features for every third-party dependency. In this mode, instead of being omitted, the Hakari package is always *included*
14//! in feature resolution (with default features), through the `features_only` argument to
15//! [`CargoSet::new`](guppy::graph::cargo::CargoSet::new). If, in the result, the
16//! [`output_map`](crate::Hakari::output_map) is empty, then features were unified.
17//!
18//! [Structural excludes](Hakari::structural_excludes) can never be unified, so
19//! verification skips them even if they are built with more than one feature
20//! set.
21
22#[cfg(feature = "cli-support")]
23mod display;
24
25#[cfg(feature = "cli-support")]
26pub use display::VerifyErrorsDisplay;
27
28use crate::{Hakari, HakariBuilder, explain::HakariExplain};
29use guppy::PackageId;
30use std::collections::BTreeSet;
31
32impl<'g> HakariBuilder<'g> {
33 /// Verify that `hakari` worked properly.
34 ///
35 /// Returns `Ok(())` if only one version of every third-party dependency was built, or a list of
36 /// errors if at least one third-party dependency had more than one version built.
37 /// [Structural excludes](Hakari::structural_excludes) are skipped.
38 ///
39 /// For more about how this works, see the documentation for the [`verify`](crate::verify)
40 /// module.
41 pub fn verify(mut self) -> Result<(), VerifyErrors<'g>> {
42 self.verify_mode = true;
43 let hakari = self.compute();
44 if hakari.output_map.is_empty() {
45 Ok(())
46 } else {
47 let mut dependency_ids = BTreeSet::new();
48
49 for ((_, package_id), v) in &hakari.computed_map {
50 for (_, inner_map) in v.inner_maps() {
51 if inner_map.len() > 1 {
52 dependency_ids.insert(*package_id);
53 }
54 }
55 }
56 Err(VerifyErrors {
57 hakari: Box::new(hakari),
58 dependency_ids,
59 })
60 }
61 }
62}
63
64/// Context for errors returned by [`HakariBuilder::verify`].
65///
66/// For more about how verification works, see the documentation for the [`verify`](crate::verify)
67/// module.
68#[derive(Clone, Debug)]
69#[non_exhaustive]
70pub struct VerifyErrors<'g> {
71 /// The Hakari instance used to compute the errors.
72 ///
73 /// This is a special "verify mode" instance; for more about it, see the documentation for the
74 /// [`verify`](crate::verify) module.
75 pub hakari: Box<Hakari<'g>>,
76
77 /// The dependency package IDs that were built with more than one feature set.
78 pub dependency_ids: BTreeSet<&'g PackageId>,
79}
80
81impl<'g> VerifyErrors<'g> {
82 /// Returns individual verification errors as [`HakariExplain`] instances.
83 pub fn errors<'a>(&'a self) -> impl ExactSizeIterator<Item = HakariExplain<'g, 'a>> + 'a {
84 let hakari = &self.hakari;
85 self.dependency_ids
86 .iter()
87 .copied()
88 .map(move |id| HakariExplain::new(hakari, id).expect("package ID is from this graph"))
89 }
90
91 /// Returns a displayer for this instance.
92 #[cfg(feature = "cli-support")]
93 pub fn display<'verify>(&'verify self) -> VerifyErrorsDisplay<'g, 'verify> {
94 VerifyErrorsDisplay::new(self)
95 }
96}
97
98#[cfg(test)]
99#[cfg(feature = "cli-support")]
100mod cli_support_tests {
101 use crate::summaries::{DEFAULT_CONFIG_PATH, HakariConfig};
102 use guppy::MetadataCommand;
103
104 /// Verify that this repo's `workspace-hack` works correctly.
105 #[test]
106 fn cargo_guppy_verify() {
107 let graph = MetadataCommand::new()
108 .build_graph()
109 .expect("package graph built correctly");
110 let config_path = graph.workspace().root().join(DEFAULT_CONFIG_PATH);
111 let config_str = std::fs::read_to_string(&config_path)
112 .unwrap_or_else(|err| panic!("could not read hakari config at {config_path}: {err}"));
113 let config: HakariConfig = config_str.parse().unwrap_or_else(|err| {
114 panic!("could not deserialize hakari config at {config_path}: {err}")
115 });
116
117 let builder = config.builder.to_hakari_builder(&graph).unwrap();
118 if let Err(errs) = builder.verify() {
119 panic!("verify failed: {}", errs.display());
120 }
121 }
122}