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,
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    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                        "{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        // Just ensure that this doesn't crash for now -- we should add more checks later.
315        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    // The vector items are (name, package id).
336    // XXX add more details about dependency edges here?
337    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    // Each pair stands for (required on, enabled on).
600    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}