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