1use crate::{
5 HakariBuilder, UnifyTargetHost,
6 hakari::{DepFormatVersion, WorkspaceHackLineStyle},
7};
8use guppy::{
9 PackageId,
10 graph::{PackageGraph, cargo::CargoResolverVersion},
11 platform::{Platform, TargetFeatures},
12};
13use proptest::{
14 collection::{hash_set, vec},
15 prelude::*,
16};
17
18impl<'g> HakariBuilder<'g> {
25 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 guppy::graph::{DependencyDirection, PackageMetadata};
90 use proptest::option;
91 use std::collections::HashSet;
92
93 #[test]
95 fn builder_summary_roundtrip() {
96 for (&name, fixture) in JsonFixture::all_fixtures() {
97 let graph = fixture.graph();
98 let workspace = graph.workspace();
99 let strategy = HakariBuilder::proptest1_strategy(
100 graph,
101 option::of(workspace.proptest1_id_strategy()),
102 );
103 proptest!(|(builder in strategy)| {
104 let summary = builder.to_summary().unwrap_or_else(|err| {
105 panic!("for fixture {name}, builder -> summary conversion failed: {err}");
106 });
107 let builder2 = summary.to_hakari_builder(graph).unwrap_or_else(|err| {
108 panic!("for fixture {name}, summary -> builder conversion failed: {err}");
109 });
110 let summary2 = builder2.to_summary().unwrap_or_else(|err| {
111 panic!("for fixture {name}, second builder -> summary conversion failed: {err}");
112 });
113 assert_eq!(summary, summary2, "summary roundtripped correctly");
114 });
115 }
116 }
117
118 #[test]
120 fn traversal_excludes() {
121 for (&name, fixture) in JsonFixture::all_fixtures() {
122 let graph = fixture.graph();
123 let workspace = graph.workspace();
124 let strategy = HakariBuilder::proptest1_strategy(
125 graph,
126 option::of(workspace.proptest1_id_strategy()),
127 );
128 proptest!(|(builder in strategy, queries in vec(graph.proptest1_id_strategy(), 0..64))| {
129 if let Some(package) = builder.hakari_package() {
131 assert!(
132 builder.is_traversal_excluded(package.id()).expect("valid package ID"),
133 "for fixture {name}, hakari package is excluded from traversals",
134 );
135 }
136 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 {name}, traversal_excludes and is_traversal_excluded match",
143 );
144 }
145 });
146 }
147 }
148
149 #[test]
152 fn structural_excludes() {
153 for (&name, fixture) in JsonFixture::all_fixtures() {
154 let graph = fixture.graph();
155 let workspace = graph.workspace();
156 let strategy = HakariBuilder::proptest1_strategy(
157 graph,
158 option::of(workspace.proptest1_id_strategy()),
159 );
160 proptest!(|(builder in strategy)| {
161 let structural_excludes = builder.make_structural_excludes().cycle_forming;
165
166 let is_root = |package: &PackageMetadata<'_>| {
167 builder
168 .hakari_package()
169 .is_some_and(|hakari_package| hakari_package.id() == package.id())
170 || builder.is_managed_member(package)
171 };
172
173 let reaches_root = |package: &PackageMetadata<'_>| {
174 package
175 .to_package_query(DependencyDirection::Forward)
176 .resolve_with_fn(|_, link| !link.dev_only())
177 .packages(DependencyDirection::Forward)
178 .any(|dep| is_root(&dep))
179 };
180
181 for package_id in &structural_excludes {
184 let package = graph.metadata(package_id).expect("valid package ID");
185 assert!(
186 !package.in_workspace(),
187 "for fixture {name}, structurally excluded {} is a third-party package",
188 package.name(),
189 );
190 assert!(
191 reaches_root(&package),
192 "for fixture {name}, structurally excluded {} reaches the hakari package \
193 or a managed member",
194 package.name(),
195 );
196 }
197
198 let mut workspace_targets = HashSet::new();
205 for package in graph.packages() {
206 if package.in_workspace() || structural_excludes.contains(package.id()) {
207 continue;
208 }
209 for link in package.direct_links() {
210 if link.dev_only() {
211 continue;
212 }
213 let to = link.to();
214 assert!(
215 !is_root(&to),
216 "for fixture {name}, {} isn't structurally excluded, so it has no \
217 non-dev link to root {}",
218 package.name(),
219 to.name(),
220 );
221 assert!(
222 !structural_excludes.contains(to.id()),
223 "for fixture {name}, {} isn't structurally excluded, so it has no \
224 non-dev link to structurally excluded {}",
225 package.name(),
226 to.name(),
227 );
228 if to.in_workspace() {
229 workspace_targets.insert(to.id());
230 }
231 }
232 }
233 for package_id in workspace_targets {
234 let package = graph.metadata(package_id).expect("valid package ID");
235 assert!(
236 !reaches_root(&package),
237 "for fixture {name}, workspace package {} is reached by a \
238 third-party package that isn't structurally excluded, so it must not \
239 reach a root itself",
240 package.name(),
241 );
242 }
243 });
244 }
245 }
246}