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}