Skip to main content

fixtures/
details.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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
25/// This captures metadata fields that are relevant for tests. They are meant to be written out
26/// lazily as tests are filled out -- feel free to add more details as necessary!
27pub 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    /// Sets the hakari package for this fixture, if any.
62    ///
63    /// Some fixtures have a hakari package which can be used to test
64    /// hakari-specific behaviors.
65    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        // Don't sort because the order returned by all_cycles (both the outer and inner vecs) is
94        // significant.
95        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    // ---
165    // Build targets
166    // ---
167
168    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                // Strip off the manifest path from the beginning.
183                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    // ---
203    // Direct dependencies
204    // ---
205
206    /// Returns true if the deps for this package are available to test against.
207    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    /// Returns true if the reverse deps for this package are available to test against.
218    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    // ---
229    // Transitive dependencies
230    // ---
231
232    /// Returns true if the transitive deps for this package are available to test against.
233    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    /// Returns true if the transitive reverse deps for this package are available to test against.
248    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    // ---
263    // Links
264    // ---
265
266    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    // ---
291    // Features
292    // ---
293
294    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    /// Asserts invariants for `ConditionalLink::package_links`.
316    ///
317    /// * Every conditional link starts at its `from` feature's package.
318    /// * A cross-package one has exactly one package link, ending at its `to`
319    ///   feature's package.
320    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    // ---
353    // Cycles
354    // ---
355
356    pub fn assert_cycles(&self, graph: &PackageGraph, msg: &str) {
357        let actual: Vec<_> = graph.cycles().all_cycles().collect();
358        // Don't sort because the order returned by all_cycles (both the outer and inner vecs) is
359        // significant.
360        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        // Just ensure that this doesn't crash for now -- we should add more checks later.
380        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    // The vector items are (name, package id).
401    // XXX add more details about dependency edges here?
402    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    // Each pair stands for (required on, enabled on).
665    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}