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    errors::FeatureGraphWarning,
15    graph::{
16        BuildTargetId, BuildTargetKind, DependencyDirection, EnabledStatus, PackageGraph,
17        PackageLink, PackageMetadata, PackageSource, Workspace,
18    },
19    platform::{EnabledTernary, Platform, PlatformSpec},
20    DependencyKind, PackageId, Version,
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    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        // Don't sort because the order returned by all_cycles (both the outer and inner vecs) is
79        // significant.
80        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    // ---
137    // Build targets
138    // ---
139
140    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                // Strip off the manifest path from the beginning.
155                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    // ---
175    // Direct dependencies
176    // ---
177
178    /// Returns true if the deps for this package are available to test against.
179    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    /// Returns true if the reverse deps for this package are available to test against.
190    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    // ---
201    // Transitive dependencies
202    // ---
203
204    /// Returns true if the transitive deps for this package are available to test against.
205    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    /// Returns true if the transitive reverse deps for this package are available to test against.
220    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    // ---
235    // Links
236    // ---
237
238    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    // ---
263    // Features
264    // ---
265
266    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    // ---
288    // Cycles
289    // ---
290
291    pub fn assert_cycles(&self, graph: &PackageGraph, msg: &str) {
292        let actual: Vec<_> = graph.cycles().all_cycles().collect();
293        // Don't sort because the order returned by all_cycles (both the outer and inner vecs) is
294        // significant.
295        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                        "{}: within cycle, {} depends on {}",
305                        msg,
306                        id1,
307                        id2
308                    );
309                    assert!(
310                        cache.depends_on(id1, id2).expect("valid package IDs"),
311                        "{}: within cycle, {} depends on {} (using cache)",
312                        msg,
313                        id1,
314                        id2
315                    )
316                }
317            }
318        }
319
320        // Just ensure that this doesn't crash for now -- we should add more checks later.
321        let _: Vec<_> = graph.feature_graph().cycles().all_cycles().collect();
322    }
323}
324
325pub struct PackageDetails {
326    id: PackageId,
327    name: &'static str,
328    version: Version,
329    authors: Vec<&'static str>,
330    description: Option<&'static str>,
331    license: Option<&'static str>,
332
333    source: Option<PackageSource<'static>>,
334    build_targets: Option<
335        Vec<(
336            BuildTargetId<'static>,
337            BuildTargetKind<'static>,
338            Utf8PathBuf,
339        )>,
340    >,
341    // The vector items are (name, package id).
342    // XXX add more details about dependency edges here?
343    deps: Option<Vec<(&'static str, PackageId)>>,
344    reverse_deps: Option<Vec<(&'static str, PackageId)>>,
345    transitive_deps: Option<Vec<PackageId>>,
346    transitive_reverse_deps: Option<Vec<PackageId>>,
347    named_features: Option<Vec<&'static str>>,
348}
349
350impl PackageDetails {
351    pub fn new(
352        id: &'static str,
353        name: &'static str,
354        version: &'static str,
355        authors: Vec<&'static str>,
356        description: Option<&'static str>,
357        license: Option<&'static str>,
358    ) -> Self {
359        Self {
360            id: package_id(id),
361            name,
362            version: Version::parse(version).expect("version should be valid"),
363            authors,
364            description,
365            license,
366            source: None,
367            build_targets: None,
368            deps: None,
369            reverse_deps: None,
370            transitive_deps: None,
371            transitive_reverse_deps: None,
372            named_features: None,
373        }
374    }
375
376    pub fn with_workspace_path(mut self, path: &'static str) -> Self {
377        self.source = Some(PackageSource::Workspace(path.into()));
378        self
379    }
380
381    pub fn with_local_path(mut self, path: &'static str) -> Self {
382        self.source = Some(PackageSource::Path(path.into()));
383        self
384    }
385
386    pub fn with_crates_io(self) -> Self {
387        self.with_external_source(PackageSource::CRATES_IO_REGISTRY)
388    }
389
390    pub fn with_external_source(mut self, source: &'static str) -> Self {
391        self.source = Some(PackageSource::External(source));
392        self
393    }
394
395    pub fn with_build_targets(
396        mut self,
397        mut build_targets: Vec<(
398            BuildTargetId<'static>,
399            BuildTargetKind<'static>,
400            &'static str,
401        )>,
402    ) -> Self {
403        build_targets.sort();
404        self.build_targets = Some(
405            build_targets
406                .into_iter()
407                .map(|(id, kind, path)| (id, kind, path.to_string().into()))
408                .collect(),
409        );
410        self
411    }
412
413    pub fn with_deps(mut self, mut deps: Vec<(&'static str, &'static str)>) -> Self {
414        deps.sort_unstable();
415        self.deps = Some(
416            deps.into_iter()
417                .map(|(name, id)| (name, package_id(id)))
418                .collect(),
419        );
420        self
421    }
422
423    pub fn with_reverse_deps(
424        mut self,
425        mut reverse_deps: Vec<(&'static str, &'static str)>,
426    ) -> Self {
427        reverse_deps.sort_unstable();
428        self.reverse_deps = Some(
429            reverse_deps
430                .into_iter()
431                .map(|(name, id)| (name, package_id(id)))
432                .collect(),
433        );
434        self
435    }
436
437    pub fn with_transitive_deps(mut self, mut transitive_deps: Vec<&'static str>) -> Self {
438        transitive_deps.sort_unstable();
439        self.transitive_deps = Some(transitive_deps.into_iter().map(package_id).collect());
440        self
441    }
442
443    pub fn with_transitive_reverse_deps(
444        mut self,
445        mut transitive_reverse_deps: Vec<&'static str>,
446    ) -> Self {
447        transitive_reverse_deps.sort_unstable();
448        self.transitive_reverse_deps = Some(
449            transitive_reverse_deps
450                .into_iter()
451                .map(package_id)
452                .collect(),
453        );
454        self
455    }
456
457    pub fn with_named_features(mut self, mut named_features: Vec<&'static str>) -> Self {
458        named_features.sort_unstable();
459        self.named_features = Some(named_features);
460        self
461    }
462
463    pub fn insert_into(self, map: &mut AHashMap<PackageId, PackageDetails>) {
464        map.insert(self.id.clone(), self);
465    }
466
467    pub fn id(&self) -> &PackageId {
468        &self.id
469    }
470
471    pub fn deps(&self, direction: DependencyDirection) -> Option<&[(&'static str, PackageId)]> {
472        match direction {
473            DependencyDirection::Forward => self.deps.as_deref(),
474            DependencyDirection::Reverse => self.reverse_deps.as_deref(),
475        }
476    }
477
478    pub fn transitive_deps(&self, direction: DependencyDirection) -> Option<&[PackageId]> {
479        match direction {
480            DependencyDirection::Forward => self.transitive_deps.as_deref(),
481            DependencyDirection::Reverse => self.transitive_reverse_deps.as_deref(),
482        }
483    }
484
485    pub fn assert_metadata(&self, metadata: PackageMetadata<'_>, msg: &str) {
486        assert_eq!(&self.id, metadata.id(), "{}: same package ID", msg);
487        assert_eq!(self.name, metadata.name(), "{}: same name", msg);
488        assert_eq!(&self.version, metadata.version(), "{}: same version", msg);
489        assert_eq!(
490            &self.authors,
491            &metadata
492                .authors()
493                .iter()
494                .map(|author| author.as_str())
495                .collect::<Vec<_>>(),
496            "{}: same authors",
497            msg
498        );
499        assert_eq!(
500            &self.description,
501            &metadata.description(),
502            "{}: same description",
503            msg
504        );
505        assert_eq!(&self.license, &metadata.license(), "{}: same license", msg);
506        if let Some(source) = &self.source {
507            assert_eq!(source, &metadata.source(), "{}: same source", msg);
508        }
509    }
510}
511
512#[derive(Clone, Debug)]
513pub struct LinkDetails {
514    from: PackageId,
515    to: PackageId,
516    platform_results: Vec<(DependencyKind, Platform, PlatformResults)>,
517    features: Vec<(DependencyKind, Vec<&'static str>)>,
518}
519
520impl LinkDetails {
521    pub fn new(from: PackageId, to: PackageId) -> Self {
522        Self {
523            from,
524            to,
525            platform_results: vec![],
526            features: vec![],
527        }
528    }
529
530    pub fn with_platform_status(
531        mut self,
532        dep_kind: DependencyKind,
533        platform: Platform,
534        status: PlatformResults,
535    ) -> Self {
536        self.platform_results.push((dep_kind, platform, status));
537        self
538    }
539
540    pub fn with_features(
541        mut self,
542        dep_kind: DependencyKind,
543        mut features: Vec<&'static str>,
544    ) -> Self {
545        features.sort_unstable();
546        self.features.push((dep_kind, features));
547        self
548    }
549
550    pub fn insert_into(self, map: &mut AHashMap<(PackageId, PackageId), Self>) {
551        map.insert((self.from.clone(), self.to.clone()), self);
552    }
553
554    pub fn assert_metadata(&self, link: PackageLink<'_>, msg: &str) {
555        let required_enabled = |status: EnabledStatus<'_>, platform_spec: &PlatformSpec| {
556            (
557                status.required_on(platform_spec),
558                status.enabled_on(platform_spec),
559            )
560        };
561
562        for (dep_kind, platform, results) in &self.platform_results {
563            let platform_spec = platform.clone().into();
564            let req = link.req_for_kind(*dep_kind);
565            assert_eq!(
566                required_enabled(req.status(), &platform_spec),
567                results.status,
568                "{}: for platform '{}', kind {}, status is correct",
569                msg,
570                platform.triple_str(),
571                dep_kind,
572            );
573            assert_eq!(
574                required_enabled(req.default_features(), &platform_spec),
575                results.default_features,
576                "{}: for platform '{}', kind {}, default features is correct",
577                msg,
578                platform.triple_str(),
579                dep_kind,
580            );
581            for (feature, status) in &results.feature_statuses {
582                assert_eq!(
583                    required_enabled(req.feature_status(feature), &platform_spec),
584                    *status,
585                    "{}: for platform '{}', kind {}, feature '{}' has correct status",
586                    msg,
587                    platform.triple_str(),
588                    dep_kind,
589                    feature
590                );
591            }
592        }
593
594        for (dep_kind, features) in &self.features {
595            let metadata = link.req_for_kind(*dep_kind);
596            let mut actual_features: Vec<_> = metadata.features().collect();
597            actual_features.sort_unstable();
598            assert_eq!(&actual_features, features, "{}: features is correct", msg);
599        }
600    }
601}
602
603#[derive(Clone, Debug)]
604pub struct PlatformResults {
605    // Each pair stands for (required on, enabled on).
606    status: (EnabledTernary, EnabledTernary),
607    default_features: (EnabledTernary, EnabledTernary),
608    feature_statuses: AHashMap<String, (EnabledTernary, EnabledTernary)>,
609}
610
611impl PlatformResults {
612    pub fn new(
613        status: (EnabledTernary, EnabledTernary),
614        default_features: (EnabledTernary, EnabledTernary),
615    ) -> Self {
616        Self {
617            status,
618            default_features,
619            feature_statuses: AHashMap::new(),
620        }
621    }
622
623    pub fn with_feature_status(
624        mut self,
625        feature: &str,
626        status: (EnabledTernary, EnabledTernary),
627    ) -> Self {
628        self.feature_statuses.insert(feature.to_string(), status);
629        self
630    }
631}