Skip to main content

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}