hakari/
proptest_helpers.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    hakari::{DepFormatVersion, WorkspaceHackLineStyle},
6    HakariBuilder, UnifyTargetHost,
7};
8use guppy::{
9    graph::{cargo::CargoResolverVersion, PackageGraph},
10    platform::{Platform, TargetFeatures},
11    PackageId,
12};
13use proptest::{
14    collection::{hash_set, vec},
15    prelude::*,
16};
17
18/// ## Helpers for property testing
19///
20/// The methods in this section allow random instances of a `HakariBuilder` to be generated, for use
21/// in property-based testing scenarios.
22///
23/// Requires the `proptest1` feature to be enabled.
24impl<'g> HakariBuilder<'g> {
25    /// Returns a `Strategy` that generates random `HakariBuilder` instances based on this graph.
26    ///
27    /// Requires the `proptest1` feature to be enabled.
28    ///
29    /// ## Panics
30    ///
31    /// Panics if:
32    /// * there are no packages in this `PackageGraph`, or
33    /// * `hakari_id` is specified but it isn't known to the graph, or isn't in the workspace.
34    pub fn proptest1_strategy(
35        graph: &'g PackageGraph,
36        hakari_id_strategy: impl Strategy<Value = Option<&'g PackageId>> + 'g,
37    ) -> impl Strategy<Value = HakariBuilder<'g>> + 'g {
38        (
39            hakari_id_strategy,
40            vec(Platform::strategy(any::<TargetFeatures>()), 0..4),
41            any::<CargoResolverVersion>(),
42            hash_set(graph.proptest1_id_strategy(), 0..8),
43            hash_set(graph.proptest1_id_strategy(), 0..8),
44            any::<UnifyTargetHost>(),
45            any::<bool>(),
46            any::<DepFormatVersion>(),
47            any::<WorkspaceHackLineStyle>(),
48        )
49            .prop_map(
50                move |(
51                    hakari_id,
52                    platforms,
53                    version,
54                    traversal_excludes,
55                    final_excludes,
56                    unify_target_host,
57                    output_single_feature,
58                    dep_format_version,
59                    line_style,
60                )| {
61                    let mut builder = HakariBuilder::new(graph, hakari_id)
62                        .expect("HakariBuilder::new returned an error");
63                    let platforms: Vec<_> = platforms
64                        .iter()
65                        .map(|platform| platform.triple_str().to_owned())
66                        .collect();
67                    builder
68                        .set_platforms(platforms)
69                        .expect("all platforms are known")
70                        .set_resolver(version)
71                        .add_traversal_excludes(traversal_excludes)
72                        .expect("traversal excludes obtained from PackageGraph should work")
73                        .add_final_excludes(final_excludes)
74                        .expect("final excludes obtained from PackageGraph should work")
75                        .set_unify_target_host(unify_target_host)
76                        .set_dep_format_version(dep_format_version)
77                        .set_workspace_hack_line_style(line_style)
78                        .set_output_single_feature(output_single_feature);
79                    builder
80                },
81            )
82    }
83}
84
85#[cfg(all(test, feature = "cli-support"))]
86mod test {
87    use super::*;
88    use fixtures::json::JsonFixture;
89    use proptest::option;
90    use std::collections::HashSet;
91
92    /// Ensure that HakariBuilder roundtrips to its summary format.
93    #[test]
94    fn builder_summary_roundtrip() {
95        for (&name, fixture) in JsonFixture::all_fixtures() {
96            let graph = fixture.graph();
97            let workspace = graph.workspace();
98            let strategy = HakariBuilder::proptest1_strategy(
99                graph,
100                option::of(workspace.proptest1_id_strategy()),
101            );
102            proptest!(|(builder in strategy)| {
103                let summary = builder.to_summary().unwrap_or_else(|err| {
104                    panic!("for fixture {}, builder -> summary conversion failed: {}", name, err);
105                });
106                let builder2 = summary.to_hakari_builder(graph).unwrap_or_else(|err| {
107                    panic!("for fixture {}, summary -> builder conversion failed: {}", name, err);
108                });
109                let summary2 = builder2.to_summary().unwrap_or_else(|err| {
110                    panic!("for fixture {}, second builder -> summary conversion failed: {}", name, err);
111                });
112                assert_eq!(summary, summary2, "summary roundtripped correctly");
113            });
114        }
115    }
116
117    /// Ensure that HakariBuilder's traversal_excludes and is_traversal_excluded match up.
118    #[test]
119    fn traversal_excludes() {
120        for (&name, fixture) in JsonFixture::all_fixtures() {
121            let graph = fixture.graph();
122            let workspace = graph.workspace();
123            let strategy = HakariBuilder::proptest1_strategy(
124                graph,
125                option::of(workspace.proptest1_id_strategy()),
126            );
127            proptest!(|(builder in strategy, queries in vec(graph.proptest1_id_strategy(), 0..64))| {
128                // Ensure that the hakari package is omitted.
129                if let Some(package) = builder.hakari_package() {
130                    assert!(
131                        builder.is_traversal_excluded(package.id()).expect("valid package ID"),
132                        "for fixture {}, hakari package is excluded from traversals",
133                        name,
134                    );
135                }
136                // Ensure that omits_package and omitted_packages match.
137                let traversal_excludes: HashSet<_> = builder.traversal_excludes().collect();
138                for query_id in queries {
139                    assert_eq!(
140                        traversal_excludes.contains(query_id),
141                        builder.is_traversal_excluded(query_id).expect("valid package ID"),
142                        "for fixture {}, traversal_excludes and is_traversal_excluded match",
143                        name,
144                    );
145                }
146            });
147        }
148    }
149}