Skip to main content

fixture_manager/
summaries.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::context::ContextImpl;
5use anyhow::Result;
6use camino::{Utf8Path, Utf8PathBuf};
7use fixtures::json::JsonFixture;
8use guppy::graph::{
9    cargo::CargoSet,
10    summaries::{CargoSetInputsSummary, Summary, diff::SummaryDiff},
11};
12use guppy_cmdlib::PackagesAndFeatures;
13use hakari::diffy::{PatchFormatter, create_patch};
14use once_cell::sync::Lazy;
15use proptest_ext::ValueGenerator;
16use std::fmt::Write;
17
18pub struct SummaryContext;
19
20pub struct ExistingSummary {
21    summary: Summary,
22    contents: String,
23}
24
25impl<'g> ContextImpl<'g> for SummaryContext {
26    type IterArgs = usize;
27    type IterItem = (usize, Summary);
28    type Existing = ExistingSummary;
29
30    fn dir_name(fixture: &'g JsonFixture) -> Utf8PathBuf {
31        fixture
32            .abs_path()
33            .parent()
34            .expect("up to dirname of summary")
35            .join("summaries")
36    }
37
38    fn file_name(fixture: &'g JsonFixture, &(count, _): &Self::IterItem) -> String {
39        format!("{}-{}.toml", fixture.name(), count)
40    }
41
42    fn iter(
43        fixture: &'g JsonFixture,
44        &count: &Self::IterArgs,
45    ) -> Box<dyn Iterator<Item = Self::IterItem> + 'g> {
46        // Make a fresh generator for each summary so that filtering by --fixtures continues to
47        // produce deterministic results.
48        let mut generator = ValueGenerator::from_seed(fixture.name());
49
50        let graph = fixture.graph();
51
52        let packages_features_strategy = PackagesAndFeatures::strategy(graph);
53        let cargo_opts_strategy = graph.proptest1_cargo_options_strategy();
54
55        let iter = (0..count).map(move |idx| {
56            // The partial clones mean that e.g. a change to the algorithm in
57            // packages_features_strategy won't affect generation of cargo_opts.
58            let mut iter_generator = generator.partial_clone();
59
60            let packages_features = iter_generator
61                .partial_clone()
62                .generate(&packages_features_strategy);
63            let (initials, features_only) = packages_features
64                .make_feature_sets(graph)
65                .expect("valid feature set");
66
67            let cargo_opts = iter_generator
68                .partial_clone()
69                .generate(&cargo_opts_strategy);
70            let cargo_set = CargoSet::new(initials, features_only, &cargo_opts)
71                .expect("into_cargo_set succeeded");
72            let summary = cargo_set
73                .to_summary()
74                .expect("generated summaries should serialize correctly");
75
76            let metadata: CargoSetInputsSummary = summary
77                .metadata
78                .clone()
79                .try_into()
80                .expect("metadata deserialized as a CargoSetInputsSummary");
81            let inputs = metadata
82                .to_cargo_set_inputs(graph)
83                .expect("cargo set inputs rebuilt from the summary");
84            assert_eq!(
85                &inputs.features_only,
86                &cargo_set.inputs().features_only,
87                "features-only set rebuilt from the summary",
88            );
89            let rebuilt_summary = inputs
90                .to_cargo_set(cargo_set.initials().clone())
91                .expect("cargo set rebuilt from the summary")
92                .to_summary()
93                .expect("rebuilt summary generated");
94            assert_eq!(
95                rebuilt_summary, summary,
96                "resolution rebuilt from the summary matches the original",
97            );
98
99            (idx, summary)
100        });
101
102        Box::new(iter)
103    }
104
105    fn parse_existing(_: &Utf8Path, contents: String) -> Result<Self::Existing> {
106        let summary = Summary::parse(&contents)?;
107        Ok(ExistingSummary { summary, contents })
108    }
109
110    fn is_changed(
111        fixture: &'g JsonFixture,
112        item: &Self::IterItem,
113        existing: &Self::Existing,
114    ) -> Result<bool> {
115        let mut rendered = String::new();
116        Self::write_to_string(fixture, item, &mut rendered)?;
117        Ok(existing.contents != rendered)
118    }
119
120    fn diff(
121        fixture: &'g JsonFixture,
122        item @ (_, summary): &Self::IterItem,
123        existing: Option<&Self::Existing>,
124    ) -> String {
125        // Need to make this a static to allow lifetimes to work out.
126        static EMPTY_SUMMARY: Lazy<Summary> = Lazy::new(Summary::default);
127
128        let existing_summary = match existing {
129            Some(existing) => &existing.summary,
130            None => &*EMPTY_SUMMARY,
131        };
132
133        let diff = SummaryDiff::new(existing_summary, summary);
134        if diff.is_changed() {
135            return format!("{}", diff.report());
136        }
137
138        let existing_contents = existing.map_or("", |existing| existing.contents.as_str());
139        let mut rendered = String::new();
140        match Self::write_to_string(fixture, item, &mut rendered) {
141            Ok(()) => {
142                let patch = create_patch(existing_contents, &rendered);
143                format!("{}", PatchFormatter::new().fmt_patch(&patch))
144            }
145            Err(err) => format!("error while rendering summary: {err}"),
146        }
147    }
148
149    fn write_to_string(
150        fixture: &'g JsonFixture,
151        (_, summary): &Self::IterItem,
152        out: &mut String,
153    ) -> Result<()> {
154        writeln!(
155            out,
156            "# This summary was @generated. To regenerate, run:\n\
157             #   cargo run -p fixture-manager -- generate-summaries --fixture {}\n",
158            fixture.name()
159        )?;
160
161        summary.write_to_string(out)?;
162        Ok(())
163    }
164}