1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// Copyright (c) The cargo-guppy Contributors
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Code related to ensuring that `hakari` works properly.
//!
//! # Verification algorithm
//!
//! By default, Hakari runs in "generate mode": the goal of this mode is to update an existing
//! Hakari package's TOML. In this mode, the Hakari package is always omitted from
//! consideration and added to the omitted packages.
//!
//! In verify mode, the goal is to ensure that Cargo builds actually produce a unique set of
//! features for every third-party dependency. In this mode, instead of being omitted, the Hakari package is always *included*
//! in feature resolution (with default features), through the `features_only` argument to
//! [`CargoSet::new`](guppy::graph::cargo::CargoSet::new). If, in the result, the
//! [`output_map`](crate::Hakari::output_map) is empty, then features were unified.

#[cfg(feature = "cli-support")]
mod display;

#[cfg(feature = "cli-support")]
pub use display::VerifyErrorsDisplay;

use crate::{explain::HakariExplain, Hakari, HakariBuilder};
use guppy::PackageId;
use std::collections::BTreeSet;

impl<'g> HakariBuilder<'g> {
    /// Verify that `hakari` worked properly.
    ///
    /// Returns `Ok(())` if only one version of every third-party dependency was built, or a list of
    /// errors if at least one third-party dependency had more than one version built.
    ///
    /// For more about how this works, see the documentation for the [`verify`](crate::verify)
    /// module.
    pub fn verify(mut self) -> Result<(), VerifyErrors<'g>> {
        self.verify_mode = true;
        let hakari = self.compute();
        if hakari.output_map.is_empty() {
            Ok(())
        } else {
            let mut dependency_ids = BTreeSet::new();

            for ((_, package_id), v) in &hakari.computed_map {
                for (_, inner_map) in v.inner_maps() {
                    if inner_map.len() > 1 {
                        dependency_ids.insert(*package_id);
                    }
                }
            }
            Err(VerifyErrors {
                hakari,
                dependency_ids,
            })
        }
    }
}

/// Context for errors returned by [`HakariBuilder::verify`].
///
/// For more about how verification works, see the documentation for the [`verify`](crate::verify)
/// module.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct VerifyErrors<'g> {
    /// The Hakari instance used to compute the errors.
    ///
    /// This is a special "verify mode" instance; for more about it, see the documentation for the
    /// [`verify`](crate::verify) module.
    pub hakari: Hakari<'g>,

    /// The dependency package IDs that were built with more than one feature set.
    pub dependency_ids: BTreeSet<&'g PackageId>,
}

impl<'g> VerifyErrors<'g> {
    /// Returns individual verification errors as [`HakariExplain`] instances.
    pub fn errors<'a>(&'a self) -> impl ExactSizeIterator<Item = HakariExplain<'g, 'a>> + 'a {
        let hakari = &self.hakari;
        self.dependency_ids
            .iter()
            .copied()
            .map(move |id| HakariExplain::new(hakari, id).expect("package ID is from this graph"))
    }

    /// Returns a displayer for this instance.
    #[cfg(feature = "cli-support")]
    pub fn display<'verify>(&'verify self) -> VerifyErrorsDisplay<'g, 'verify> {
        VerifyErrorsDisplay::new(self)
    }
}

#[cfg(test)]
#[cfg(feature = "cli-support")]
mod cli_support_tests {
    use crate::summaries::{HakariConfig, DEFAULT_CONFIG_PATH};
    use guppy::MetadataCommand;

    /// Verify that this repo's `workspace-hack` works correctly.
    #[test]
    fn cargo_guppy_verify() {
        let graph = MetadataCommand::new()
            .build_graph()
            .expect("package graph built correctly");
        let config_path = graph.workspace().root().join(DEFAULT_CONFIG_PATH);
        let config_str = std::fs::read_to_string(&config_path).unwrap_or_else(|err| {
            panic!("could not read hakari config at {}: {}", config_path, err)
        });
        let config: HakariConfig = config_str.parse().unwrap_or_else(|err| {
            panic!(
                "could not deserialize hakari config at {}: {}",
                config_path, err
            )
        });

        let builder = config.builder.to_hakari_builder(&graph).unwrap();
        if let Err(errs) = builder.verify() {
            panic!("verify failed: {}", errs.display());
        }
    }
}