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#[cfg(feature = "cli-support")]
19mod display;
20
21#[cfg(feature = "cli-support")]
22pub use display::VerifyErrorsDisplay;
23
24use crate::{Hakari, HakariBuilder, explain::HakariExplain};
25use guppy::PackageId;
26use std::collections::BTreeSet;
27
28impl<'g> HakariBuilder<'g> {
29    /// Verify that `hakari` worked properly.
30    ///
31    /// Returns `Ok(())` if only one version of every third-party dependency was built, or a list of
32    /// errors if at least one third-party dependency had more than one version built.
33    ///
34    /// For more about how this works, see the documentation for the [`verify`](crate::verify)
35    /// module.
36    pub fn verify(mut self) -> Result<(), VerifyErrors<'g>> {
37        self.verify_mode = true;
38        let hakari = self.compute();
39        if hakari.output_map.is_empty() {
40            Ok(())
41        } else {
42            let mut dependency_ids = BTreeSet::new();
43
44            for ((_, package_id), v) in &hakari.computed_map {
45                for (_, inner_map) in v.inner_maps() {
46                    if inner_map.len() > 1 {
47                        dependency_ids.insert(*package_id);
48                    }
49                }
50            }
51            Err(VerifyErrors {
52                hakari,
53                dependency_ids,
54            })
55        }
56    }
57}
58
59/// Context for errors returned by [`HakariBuilder::verify`].
60///
61/// For more about how verification works, see the documentation for the [`verify`](crate::verify)
62/// module.
63#[derive(Clone, Debug)]
64#[non_exhaustive]
65pub struct VerifyErrors<'g> {
66    /// The Hakari instance used to compute the errors.
67    ///
68    /// This is a special "verify mode" instance; for more about it, see the documentation for the
69    /// [`verify`](crate::verify) module.
70    pub hakari: Hakari<'g>,
71
72    /// The dependency package IDs that were built with more than one feature set.
73    pub dependency_ids: BTreeSet<&'g PackageId>,
74}
75
76impl<'g> VerifyErrors<'g> {
77    /// Returns individual verification errors as [`HakariExplain`] instances.
78    pub fn errors<'a>(&'a self) -> impl ExactSizeIterator<Item = HakariExplain<'g, 'a>> + 'a {
79        let hakari = &self.hakari;
80        self.dependency_ids
81            .iter()
82            .copied()
83            .map(move |id| HakariExplain::new(hakari, id).expect("package ID is from this graph"))
84    }
85
86    /// Returns a displayer for this instance.
87    #[cfg(feature = "cli-support")]
88    pub fn display<'verify>(&'verify self) -> VerifyErrorsDisplay<'g, 'verify> {
89        VerifyErrorsDisplay::new(self)
90    }
91}
92
93#[cfg(test)]
94#[cfg(feature = "cli-support")]
95mod cli_support_tests {
96    use crate::summaries::{DEFAULT_CONFIG_PATH, HakariConfig};
97    use guppy::MetadataCommand;
98
99    /// Verify that this repo's `workspace-hack` works correctly.
100    #[test]
101    fn cargo_guppy_verify() {
102        let graph = MetadataCommand::new()
103            .build_graph()
104            .expect("package graph built correctly");
105        let config_path = graph.workspace().root().join(DEFAULT_CONFIG_PATH);
106        let config_str = std::fs::read_to_string(&config_path).unwrap_or_else(|err| {
107            panic!("could not read hakari config at {}: {}", config_path, err)
108        });
109        let config: HakariConfig = config_str.parse().unwrap_or_else(|err| {
110            panic!(
111                "could not deserialize hakari config at {}: {}",
112                config_path, err
113            )
114        });
115
116        let builder = config.builder.to_hakari_builder(&graph).unwrap();
117        if let Err(errs) = builder.verify() {
118            panic!("verify failed: {}", errs.display());
119        }
120    }
121}