1use crate::{
5 dep_helpers::{
6 assert_all_links, assert_deps_internal, assert_topo_ids, assert_topo_metadatas,
7 assert_transitive_deps_internal,
8 },
9 package_id,
10};
11use ahash::AHashMap;
12use camino::Utf8PathBuf;
13use guppy::{
14 DependencyKind, PackageId, Version,
15 errors::FeatureGraphWarning,
16 graph::{
17 BuildTargetId, BuildTargetKind, DependencyDirection, EnabledStatus, PackageGraph,
18 PackageLink, PackageMetadata, PackageSource, Workspace,
19 },
20 platform::{EnabledTernary, Platform, PlatformSpec},
21};
22use pretty_assertions::assert_eq;
23use std::collections::BTreeMap;
24
25pub struct FixtureDetails {
28 workspace_members: Option<BTreeMap<Utf8PathBuf, PackageId>>,
29 package_details: AHashMap<PackageId, PackageDetails>,
30 link_details: AHashMap<(PackageId, PackageId), LinkDetails>,
31 feature_graph_warnings: Vec<FeatureGraphWarning>,
32 cycles: Vec<Vec<PackageId>>,
33}
34
35impl FixtureDetails {
36 pub fn new(package_details: AHashMap<PackageId, PackageDetails>) -> Self {
37 Self {
38 workspace_members: None,
39 package_details,
40 link_details: AHashMap::new(),
41 feature_graph_warnings: vec![],
42 cycles: vec![],
43 }
44 }
45
46 pub fn with_workspace_members<'a>(
47 mut self,
48 workspace_members: impl IntoIterator<Item = (impl Into<Utf8PathBuf>, &'a str)>,
49 ) -> Self {
50 self.workspace_members = Some(
51 workspace_members
52 .into_iter()
53 .map(|(path, id)| (path.into(), package_id(id)))
54 .collect(),
55 );
56 self
57 }
58
59 pub fn with_link_details(
60 mut self,
61 link_details: AHashMap<(PackageId, PackageId), LinkDetails>,
62 ) -> Self {
63 self.link_details = link_details;
64 self
65 }
66
67 pub fn with_feature_graph_warnings(mut self, mut warnings: Vec<FeatureGraphWarning>) -> Self {
68 warnings.sort();
69 self.feature_graph_warnings = warnings;
70 self
71 }
72
73 pub fn with_cycles(mut self, cycles: Vec<Vec<&'static str>>) -> Self {
74 let cycles: Vec<_> = cycles
75 .into_iter()
76 .map(|cycle| cycle.into_iter().map(package_id).collect())
77 .collect();
78 self.cycles = cycles;
81 self
82 }
83
84 pub fn known_ids(&self) -> impl Iterator<Item = &PackageId> {
85 self.package_details.keys()
86 }
87
88 pub fn assert_workspace(&self, workspace: Workspace) {
89 if let Some(expected_members) = &self.workspace_members {
90 let members: Vec<_> = workspace
91 .iter_by_path()
92 .map(|(path, metadata)| (path, metadata.id()))
93 .collect();
94 assert_eq!(
95 expected_members
96 .iter()
97 .map(|(path, id)| (path.as_path(), id))
98 .collect::<Vec<_>>(),
99 members,
100 "workspace members should be correct"
101 );
102
103 assert_eq!(
104 workspace.iter_by_path().len(),
105 workspace.iter_by_name().len(),
106 "workspace.members() and members_by_name() return the same number of items"
107 );
108 for (name, metadata) in workspace.iter_by_name() {
109 assert_eq!(
110 name,
111 metadata.name(),
112 "members_by_name returns consistent results"
113 );
114 }
115 }
116 }
117
118 pub fn assert_topo(&self, graph: &PackageGraph) {
119 assert_topo_ids(graph, DependencyDirection::Forward, "topo sort");
120 assert_topo_ids(graph, DependencyDirection::Reverse, "reverse topo sort");
121 assert_topo_metadatas(graph, DependencyDirection::Forward, "topo sort (metadatas)");
122 assert_topo_metadatas(
123 graph,
124 DependencyDirection::Reverse,
125 "reverse topo sort (metadatas)",
126 );
127 assert_all_links(graph, DependencyDirection::Forward, "all links");
128 assert_all_links(graph, DependencyDirection::Reverse, "all links reversed");
129 }
130
131 pub fn assert_metadata(&self, id: &PackageId, metadata: PackageMetadata<'_>, msg: &str) {
132 let details = &self.package_details[id];
133 details.assert_metadata(metadata, msg);
134 }
135
136 pub fn has_build_targets(&self, id: &PackageId) -> bool {
141 let details = &self.package_details[id];
142 details.build_targets.is_some()
143 }
144
145 pub fn assert_build_targets(&self, metadata: PackageMetadata<'_>, msg: &str) {
146 let build_targets = self.package_details[metadata.id()]
147 .build_targets
148 .as_ref()
149 .unwrap();
150
151 let mut actual: Vec<_> = metadata
152 .build_targets()
153 .map(|build_target| {
154 let path = build_target
156 .path()
157 .strip_prefix(
158 metadata
159 .manifest_path()
160 .parent()
161 .expect("manifest path is a file"),
162 )
163 .expect("build target path is inside source dir")
164 .to_path_buf();
165
166 (build_target.id(), build_target.kind().clone(), path)
167 })
168 .collect();
169 actual.sort();
170
171 assert_eq!(build_targets, &actual, "{}: build targets match", msg,);
172 }
173
174 pub fn has_deps(&self, id: &PackageId) -> bool {
180 let details = &self.package_details[id];
181 details.deps.is_some()
182 }
183
184 pub fn assert_deps(&self, graph: &PackageGraph, id: &PackageId, msg: &str) {
185 let details = &self.package_details[id];
186 assert_deps_internal(graph, DependencyDirection::Forward, details, msg);
187 }
188
189 pub fn has_reverse_deps(&self, id: &PackageId) -> bool {
191 let details = &self.package_details[id];
192 details.reverse_deps.is_some()
193 }
194
195 pub fn assert_reverse_deps(&self, graph: &PackageGraph, id: &PackageId, msg: &str) {
196 let details = &self.package_details[id];
197 assert_deps_internal(graph, DependencyDirection::Reverse, details, msg);
198 }
199
200 pub fn has_transitive_deps(&self, id: &PackageId) -> bool {
206 let details = &self.package_details[id];
207 details.transitive_deps.is_some()
208 }
209
210 pub fn assert_transitive_deps(&self, graph: &PackageGraph, id: &PackageId, msg: &str) {
211 assert_transitive_deps_internal(
212 graph,
213 DependencyDirection::Forward,
214 &self.package_details[id],
215 msg,
216 )
217 }
218
219 pub fn has_transitive_reverse_deps(&self, id: &PackageId) -> bool {
221 let details = &self.package_details[id];
222 details.transitive_reverse_deps.is_some()
223 }
224
225 pub fn assert_transitive_reverse_deps(&self, graph: &PackageGraph, id: &PackageId, msg: &str) {
226 assert_transitive_deps_internal(
227 graph,
228 DependencyDirection::Reverse,
229 &self.package_details[id],
230 msg,
231 )
232 }
233
234 pub fn assert_link_details(&self, graph: &PackageGraph, msg: &str) {
239 for ((from, to), details) in &self.link_details {
240 let metadata = graph
241 .metadata(from)
242 .unwrap_or_else(|err| panic!("{msg}: {err}"));
243 let mut links: Vec<_> = metadata
244 .direct_links()
245 .filter(|link| link.to().id() == to)
246 .collect();
247 assert_eq!(
248 links.len(),
249 1,
250 "{}: exactly 1 link between '{}' and '{}'",
251 msg,
252 from,
253 to
254 );
255
256 let link = links.pop().unwrap();
257 let msg = format!("{msg}: {from} -> {to}");
258 details.assert_metadata(link, &msg);
259 }
260 }
261
262 pub fn has_named_features(&self, id: &PackageId) -> bool {
267 self.package_details[id].named_features.is_some()
268 }
269
270 pub fn assert_named_features(&self, graph: &PackageGraph, id: &PackageId, msg: &str) {
271 let mut actual: Vec<_> = graph
272 .metadata(id)
273 .expect("package id should be valid")
274 .named_features()
275 .collect();
276 actual.sort_unstable();
277 let expected = self.package_details[id].named_features.as_ref().unwrap();
278 assert_eq!(expected, &actual, "{}", msg);
279 }
280
281 pub fn assert_feature_graph_warnings(&self, graph: &PackageGraph, msg: &str) {
282 let mut actual: Vec<_> = graph.feature_graph().build_warnings().to_vec();
283 actual.sort();
284 assert_eq!(&self.feature_graph_warnings, &actual, "{}", msg);
285 }
286
287 pub fn assert_cycles(&self, graph: &PackageGraph, msg: &str) {
292 let actual: Vec<_> = graph.cycles().all_cycles().collect();
293 assert_eq!(&self.cycles, &actual, "{}", msg);
296
297 let mut cache = graph.new_depends_cache();
298
299 for cycle in actual {
300 for &id1 in &cycle {
301 for &id2 in &cycle {
302 assert!(
303 graph.depends_on(id1, id2).expect("valid package IDs"),
304 "{msg}: within cycle, {id1} depends on {id2}"
305 );
306 assert!(
307 cache.depends_on(id1, id2).expect("valid package IDs"),
308 "{msg}: within cycle, {id1} depends on {id2} (using cache)"
309 )
310 }
311 }
312 }
313
314 let _: Vec<_> = graph.feature_graph().cycles().all_cycles().collect();
316 }
317}
318
319pub struct PackageDetails {
320 id: PackageId,
321 name: &'static str,
322 version: Version,
323 authors: Vec<&'static str>,
324 description: Option<&'static str>,
325 license: Option<&'static str>,
326
327 source: Option<PackageSource<'static>>,
328 build_targets: Option<
329 Vec<(
330 BuildTargetId<'static>,
331 BuildTargetKind<'static>,
332 Utf8PathBuf,
333 )>,
334 >,
335 deps: Option<Vec<(&'static str, PackageId)>>,
338 reverse_deps: Option<Vec<(&'static str, PackageId)>>,
339 transitive_deps: Option<Vec<PackageId>>,
340 transitive_reverse_deps: Option<Vec<PackageId>>,
341 named_features: Option<Vec<&'static str>>,
342}
343
344impl PackageDetails {
345 pub fn new(
346 id: &'static str,
347 name: &'static str,
348 version: &'static str,
349 authors: Vec<&'static str>,
350 description: Option<&'static str>,
351 license: Option<&'static str>,
352 ) -> Self {
353 Self {
354 id: package_id(id),
355 name,
356 version: Version::parse(version).expect("version should be valid"),
357 authors,
358 description,
359 license,
360 source: None,
361 build_targets: None,
362 deps: None,
363 reverse_deps: None,
364 transitive_deps: None,
365 transitive_reverse_deps: None,
366 named_features: None,
367 }
368 }
369
370 pub fn with_workspace_path(mut self, path: &'static str) -> Self {
371 self.source = Some(PackageSource::Workspace(path.into()));
372 self
373 }
374
375 pub fn with_local_path(mut self, path: &'static str) -> Self {
376 self.source = Some(PackageSource::Path(path.into()));
377 self
378 }
379
380 pub fn with_crates_io(self) -> Self {
381 self.with_external_source(PackageSource::CRATES_IO_REGISTRY)
382 }
383
384 pub fn with_external_source(mut self, source: &'static str) -> Self {
385 self.source = Some(PackageSource::External(source));
386 self
387 }
388
389 pub fn with_build_targets(
390 mut self,
391 mut build_targets: Vec<(
392 BuildTargetId<'static>,
393 BuildTargetKind<'static>,
394 &'static str,
395 )>,
396 ) -> Self {
397 build_targets.sort();
398 self.build_targets = Some(
399 build_targets
400 .into_iter()
401 .map(|(id, kind, path)| (id, kind, path.to_string().into()))
402 .collect(),
403 );
404 self
405 }
406
407 pub fn with_deps(mut self, mut deps: Vec<(&'static str, &'static str)>) -> Self {
408 deps.sort_unstable();
409 self.deps = Some(
410 deps.into_iter()
411 .map(|(name, id)| (name, package_id(id)))
412 .collect(),
413 );
414 self
415 }
416
417 pub fn with_reverse_deps(
418 mut self,
419 mut reverse_deps: Vec<(&'static str, &'static str)>,
420 ) -> Self {
421 reverse_deps.sort_unstable();
422 self.reverse_deps = Some(
423 reverse_deps
424 .into_iter()
425 .map(|(name, id)| (name, package_id(id)))
426 .collect(),
427 );
428 self
429 }
430
431 pub fn with_transitive_deps(mut self, mut transitive_deps: Vec<&'static str>) -> Self {
432 transitive_deps.sort_unstable();
433 self.transitive_deps = Some(transitive_deps.into_iter().map(package_id).collect());
434 self
435 }
436
437 pub fn with_transitive_reverse_deps(
438 mut self,
439 mut transitive_reverse_deps: Vec<&'static str>,
440 ) -> Self {
441 transitive_reverse_deps.sort_unstable();
442 self.transitive_reverse_deps = Some(
443 transitive_reverse_deps
444 .into_iter()
445 .map(package_id)
446 .collect(),
447 );
448 self
449 }
450
451 pub fn with_named_features(mut self, mut named_features: Vec<&'static str>) -> Self {
452 named_features.sort_unstable();
453 self.named_features = Some(named_features);
454 self
455 }
456
457 pub fn insert_into(self, map: &mut AHashMap<PackageId, PackageDetails>) {
458 map.insert(self.id.clone(), self);
459 }
460
461 pub fn id(&self) -> &PackageId {
462 &self.id
463 }
464
465 pub fn deps(&self, direction: DependencyDirection) -> Option<&[(&'static str, PackageId)]> {
466 match direction {
467 DependencyDirection::Forward => self.deps.as_deref(),
468 DependencyDirection::Reverse => self.reverse_deps.as_deref(),
469 }
470 }
471
472 pub fn transitive_deps(&self, direction: DependencyDirection) -> Option<&[PackageId]> {
473 match direction {
474 DependencyDirection::Forward => self.transitive_deps.as_deref(),
475 DependencyDirection::Reverse => self.transitive_reverse_deps.as_deref(),
476 }
477 }
478
479 pub fn assert_metadata(&self, metadata: PackageMetadata<'_>, msg: &str) {
480 assert_eq!(&self.id, metadata.id(), "{}: same package ID", msg);
481 assert_eq!(self.name, metadata.name(), "{}: same name", msg);
482 assert_eq!(&self.version, metadata.version(), "{}: same version", msg);
483 assert_eq!(
484 &self.authors,
485 &metadata
486 .authors()
487 .iter()
488 .map(|author| author.as_str())
489 .collect::<Vec<_>>(),
490 "{}: same authors",
491 msg
492 );
493 assert_eq!(
494 &self.description,
495 &metadata.description(),
496 "{}: same description",
497 msg
498 );
499 assert_eq!(&self.license, &metadata.license(), "{}: same license", msg);
500 if let Some(source) = &self.source {
501 assert_eq!(source, &metadata.source(), "{}: same source", msg);
502 }
503 }
504}
505
506#[derive(Clone, Debug)]
507pub struct LinkDetails {
508 from: PackageId,
509 to: PackageId,
510 platform_results: Vec<(DependencyKind, Platform, PlatformResults)>,
511 features: Vec<(DependencyKind, Vec<&'static str>)>,
512}
513
514impl LinkDetails {
515 pub fn new(from: PackageId, to: PackageId) -> Self {
516 Self {
517 from,
518 to,
519 platform_results: vec![],
520 features: vec![],
521 }
522 }
523
524 pub fn with_platform_status(
525 mut self,
526 dep_kind: DependencyKind,
527 platform: Platform,
528 status: PlatformResults,
529 ) -> Self {
530 self.platform_results.push((dep_kind, platform, status));
531 self
532 }
533
534 pub fn with_features(
535 mut self,
536 dep_kind: DependencyKind,
537 mut features: Vec<&'static str>,
538 ) -> Self {
539 features.sort_unstable();
540 self.features.push((dep_kind, features));
541 self
542 }
543
544 pub fn insert_into(self, map: &mut AHashMap<(PackageId, PackageId), Self>) {
545 map.insert((self.from.clone(), self.to.clone()), self);
546 }
547
548 pub fn assert_metadata(&self, link: PackageLink<'_>, msg: &str) {
549 let required_enabled = |status: EnabledStatus<'_>, platform_spec: &PlatformSpec| {
550 (
551 status.required_on(platform_spec),
552 status.enabled_on(platform_spec),
553 )
554 };
555
556 for (dep_kind, platform, results) in &self.platform_results {
557 let platform_spec = platform.clone().into();
558 let req = link.req_for_kind(*dep_kind);
559 assert_eq!(
560 required_enabled(req.status(), &platform_spec),
561 results.status,
562 "{}: for platform '{}', kind {}, status is correct",
563 msg,
564 platform.triple_str(),
565 dep_kind,
566 );
567 assert_eq!(
568 required_enabled(req.default_features(), &platform_spec),
569 results.default_features,
570 "{}: for platform '{}', kind {}, default features is correct",
571 msg,
572 platform.triple_str(),
573 dep_kind,
574 );
575 for (feature, status) in &results.feature_statuses {
576 assert_eq!(
577 required_enabled(req.feature_status(feature), &platform_spec),
578 *status,
579 "{}: for platform '{}', kind {}, feature '{}' has correct status",
580 msg,
581 platform.triple_str(),
582 dep_kind,
583 feature
584 );
585 }
586 }
587
588 for (dep_kind, features) in &self.features {
589 let metadata = link.req_for_kind(*dep_kind);
590 let mut actual_features: Vec<_> = metadata.features().collect();
591 actual_features.sort_unstable();
592 assert_eq!(&actual_features, features, "{}: features is correct", msg);
593 }
594 }
595}
596
597#[derive(Clone, Debug)]
598pub struct PlatformResults {
599 status: (EnabledTernary, EnabledTernary),
601 default_features: (EnabledTernary, EnabledTernary),
602 feature_statuses: AHashMap<String, (EnabledTernary, EnabledTernary)>,
603}
604
605impl PlatformResults {
606 pub fn new(
607 status: (EnabledTernary, EnabledTernary),
608 default_features: (EnabledTernary, EnabledTernary),
609 ) -> Self {
610 Self {
611 status,
612 default_features,
613 feature_statuses: AHashMap::new(),
614 }
615 }
616
617 pub fn with_feature_status(
618 mut self,
619 feature: &str,
620 status: (EnabledTernary, EnabledTernary),
621 ) -> Self {
622 self.feature_statuses.insert(feature.to_string(), status);
623 self
624 }
625}