cargo/core/compiler/fingerprint/
dirty_reason.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::fmt::Debug;
4
5use serde::Serialize;
6
7use super::*;
8use crate::core::Shell;
9use crate::core::compiler::UnitIndex;
10
11/// Tells a better story of why a build is considered "dirty" that leads
12/// to a recompile. Usually constructed via [`Fingerprint::compare`].
13///
14/// [`Fingerprint::compare`]: super::Fingerprint::compare
15#[derive(Clone, Debug, Serialize, Deserialize)]
16#[serde(tag = "dirty_reason", rename_all = "kebab-case")]
17pub enum DirtyReason {
18    RustcChanged,
19    FeaturesChanged {
20        old: String,
21        new: String,
22    },
23    DeclaredFeaturesChanged {
24        old: String,
25        new: String,
26    },
27    TargetConfigurationChanged,
28    PathToSourceChanged,
29    ProfileConfigurationChanged,
30    RustflagsChanged {
31        old: Vec<String>,
32        new: Vec<String>,
33    },
34    ConfigSettingsChanged,
35    CompileKindChanged,
36    LocalLengthsChanged,
37    PrecalculatedComponentsChanged {
38        old: String,
39        new: String,
40    },
41    ChecksumUseChanged {
42        old: bool,
43    },
44    DepInfoOutputChanged {
45        old: PathBuf,
46        new: PathBuf,
47    },
48    RerunIfChangedOutputFileChanged {
49        old: PathBuf,
50        new: PathBuf,
51    },
52    RerunIfChangedOutputPathsChanged {
53        old: Vec<PathBuf>,
54        new: Vec<PathBuf>,
55    },
56    EnvVarsChanged {
57        old: String,
58        new: String,
59    },
60    EnvVarChanged {
61        name: String,
62        old_value: Option<String>,
63        new_value: Option<String>,
64    },
65    LocalFingerprintTypeChanged {
66        old: String,
67        new: String,
68    },
69    NumberOfDependenciesChanged {
70        old: usize,
71        new: usize,
72    },
73    UnitDependencyNameChanged {
74        old: InternedString,
75        new: InternedString,
76    },
77    UnitDependencyInfoChanged {
78        unit: UnitIndex,
79    },
80    FsStatusOutdated(FsStatus),
81    NothingObvious,
82    Forced,
83    /// First time to build something.
84    FreshBuild,
85}
86
87trait ShellExt {
88    fn dirty_because(&mut self, unit: &Unit, s: impl fmt::Display) -> CargoResult<()>;
89}
90
91impl ShellExt for Shell {
92    fn dirty_because(&mut self, unit: &Unit, s: impl fmt::Display) -> CargoResult<()> {
93        self.status("Dirty", format_args!("{}: {s}", &unit.pkg))
94    }
95}
96
97struct FileTimeDiff {
98    old_time: FileTime,
99    new_time: FileTime,
100}
101
102impl fmt::Display for FileTimeDiff {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        let s_diff = self.new_time.seconds() - self.old_time.seconds();
105        if s_diff >= 1 {
106            write!(f, "{:#}", jiff::SignedDuration::from_secs(s_diff))
107        } else {
108            // format nanoseconds as it is, jiff would display ms, us and ns
109            let ns_diff = self.new_time.nanoseconds() - self.old_time.nanoseconds();
110            write!(f, "{ns_diff}ns")
111        }
112    }
113}
114
115#[derive(Copy, Clone)]
116struct After {
117    old_time: FileTime,
118    new_time: FileTime,
119    what: &'static str,
120}
121
122impl fmt::Display for After {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        let Self {
125            old_time,
126            new_time,
127            what,
128        } = *self;
129        let diff = FileTimeDiff { old_time, new_time };
130
131        write!(f, "{new_time}, {diff} after {what} at {old_time}")
132    }
133}
134
135impl DirtyReason {
136    /// Whether a build is dirty because it is a fresh build being kicked off.
137    pub fn is_fresh_build(&self) -> bool {
138        matches!(self, DirtyReason::FreshBuild)
139    }
140
141    fn after(old_time: FileTime, new_time: FileTime, what: &'static str) -> After {
142        After {
143            old_time,
144            new_time,
145            what,
146        }
147    }
148
149    pub fn present_to(
150        &self,
151        s: &mut Shell,
152        unit: &Unit,
153        root: &Path,
154        index_to_unit: &HashMap<UnitIndex, Unit>,
155    ) -> CargoResult<()> {
156        match self {
157            DirtyReason::RustcChanged => s.dirty_because(unit, "the toolchain changed"),
158            DirtyReason::FeaturesChanged { .. } => {
159                s.dirty_because(unit, "the list of features changed")
160            }
161            DirtyReason::DeclaredFeaturesChanged { .. } => {
162                s.dirty_because(unit, "the list of declared features changed")
163            }
164            DirtyReason::TargetConfigurationChanged => {
165                s.dirty_because(unit, "the target configuration changed")
166            }
167            DirtyReason::PathToSourceChanged => {
168                s.dirty_because(unit, "the path to the source changed")
169            }
170            DirtyReason::ProfileConfigurationChanged => {
171                s.dirty_because(unit, "the profile configuration changed")
172            }
173            DirtyReason::RustflagsChanged { .. } => s.dirty_because(unit, "the rustflags changed"),
174            DirtyReason::ConfigSettingsChanged => {
175                s.dirty_because(unit, "the config settings changed")
176            }
177            DirtyReason::CompileKindChanged => {
178                s.dirty_because(unit, "the rustc compile kind changed")
179            }
180            DirtyReason::LocalLengthsChanged => {
181                s.dirty_because(unit, "the local lengths changed")?;
182                s.note(
183                    "this could happen because of added/removed `cargo::rerun-if` instructions in the build script",
184                )?;
185
186                Ok(())
187            }
188            DirtyReason::PrecalculatedComponentsChanged { .. } => {
189                s.dirty_because(unit, "the precalculated components changed")
190            }
191            DirtyReason::ChecksumUseChanged { old } => {
192                if *old {
193                    s.dirty_because(
194                        unit,
195                        "the prior compilation used checksum freshness and this one does not",
196                    )
197                } else {
198                    s.dirty_because(unit, "checksum freshness requested, prior compilation did not use checksum freshness")
199                }
200            }
201            DirtyReason::DepInfoOutputChanged { .. } => {
202                s.dirty_because(unit, "the dependency info output changed")
203            }
204            DirtyReason::RerunIfChangedOutputFileChanged { .. } => {
205                s.dirty_because(unit, "rerun-if-changed output file path changed")
206            }
207            DirtyReason::RerunIfChangedOutputPathsChanged { .. } => {
208                s.dirty_because(unit, "the rerun-if-changed instructions changed")
209            }
210            DirtyReason::EnvVarsChanged { .. } => {
211                s.dirty_because(unit, "the environment variables changed")
212            }
213            DirtyReason::EnvVarChanged { name, .. } => {
214                s.dirty_because(unit, format_args!("the env variable {name} changed"))
215            }
216            DirtyReason::LocalFingerprintTypeChanged { .. } => {
217                s.dirty_because(unit, "the local fingerprint type changed")
218            }
219            DirtyReason::NumberOfDependenciesChanged { old, new } => s.dirty_because(
220                unit,
221                format_args!("number of dependencies changed ({old} => {new})",),
222            ),
223            DirtyReason::UnitDependencyNameChanged { old, new } => s.dirty_because(
224                unit,
225                format_args!("name of dependency changed ({old} => {new})"),
226            ),
227            DirtyReason::UnitDependencyInfoChanged { unit: dep_unit } => {
228                let dep_name = index_to_unit.get(dep_unit).map(|u| u.pkg.name()).unwrap();
229                s.dirty_because(
230                    unit,
231                    format_args!("info of dependency `{dep_name}` changed"),
232                )
233            }
234            DirtyReason::FsStatusOutdated(status) => match status {
235                FsStatus::Stale => s.dirty_because(unit, "stale, unknown reason"),
236                FsStatus::StaleItem(item) => match item {
237                    StaleItem::MissingFile { path } => {
238                        let file = path.strip_prefix(root).unwrap_or(&path);
239                        s.dirty_because(
240                            unit,
241                            format_args!("the file `{}` is missing", file.display()),
242                        )
243                    }
244                    StaleItem::UnableToReadFile { path } => {
245                        let file = path.strip_prefix(root).unwrap_or(&path);
246                        s.dirty_because(
247                            unit,
248                            format_args!("the file `{}` could not be read", file.display()),
249                        )
250                    }
251                    StaleItem::FailedToReadMetadata { path } => {
252                        let file = path.strip_prefix(root).unwrap_or(&path);
253                        s.dirty_because(
254                            unit,
255                            format_args!("couldn't read metadata for file `{}`", file.display()),
256                        )
257                    }
258                    StaleItem::ChangedFile {
259                        stale,
260                        stale_mtime,
261                        reference_mtime,
262                        ..
263                    } => {
264                        let file = stale.strip_prefix(root).unwrap_or(&stale);
265                        let after = Self::after(*reference_mtime, *stale_mtime, "last build");
266                        s.dirty_because(
267                            unit,
268                            format_args!("the file `{}` has changed ({after})", file.display()),
269                        )
270                    }
271                    StaleItem::ChangedChecksum {
272                        source,
273                        stored_checksum,
274                        new_checksum,
275                    } => {
276                        let file = source.strip_prefix(root).unwrap_or(&source);
277                        s.dirty_because(
278                            unit,
279                            format_args!(
280                                "the file `{}` has changed (checksum didn't match, {stored_checksum} != {new_checksum})",
281                                file.display(),
282                            ),
283                        )
284                    }
285                    StaleItem::FileSizeChanged {
286                        path,
287                        old_size,
288                        new_size,
289                    } => {
290                        let file = path.strip_prefix(root).unwrap_or(&path);
291                        s.dirty_because(
292                            unit,
293                            format_args!(
294                                "file size changed ({old_size} != {new_size}) for `{}`",
295                                file.display()
296                            ),
297                        )
298                    }
299                    StaleItem::MissingChecksum { path } => {
300                        let file = path.strip_prefix(root).unwrap_or(&path);
301                        s.dirty_because(
302                            unit,
303                            format_args!("the checksum for file `{}` is missing", file.display()),
304                        )
305                    }
306                    StaleItem::ChangedEnv { var, .. } => s.dirty_because(
307                        unit,
308                        format_args!("the environment variable {var} changed"),
309                    ),
310                },
311                FsStatus::StaleDependency {
312                    unit: dep_unit,
313                    dep_mtime,
314                    max_mtime,
315                } => {
316                    let dep_name = index_to_unit.get(dep_unit).map(|u| u.pkg.name()).unwrap();
317                    let after = Self::after(*max_mtime, *dep_mtime, "last build");
318                    s.dirty_because(
319                        unit,
320                        format_args!("the dependency `{dep_name}` was rebuilt ({after})"),
321                    )
322                }
323                FsStatus::StaleDepFingerprint { unit: dep_unit } => {
324                    let dep_name = index_to_unit.get(dep_unit).map(|u| u.pkg.name()).unwrap();
325                    s.dirty_because(
326                        unit,
327                        format_args!("the dependency `{dep_name}` was rebuilt"),
328                    )
329                }
330                FsStatus::UpToDate { .. } => {
331                    unreachable!()
332                }
333            },
334            DirtyReason::NothingObvious => {
335                // See comment in fingerprint compare method.
336                s.dirty_because(unit, "the fingerprint comparison turned up nothing obvious")
337            }
338            DirtyReason::Forced => s.dirty_because(unit, "forced"),
339            DirtyReason::FreshBuild => s.dirty_because(unit, "fresh build"),
340        }
341    }
342}
343
344// These test the actual JSON structure that will be logged.
345// In the future we might decouple this from the actual log message schema.
346#[cfg(test)]
347mod json_schema {
348    use super::*;
349    use snapbox::IntoData;
350    use snapbox::assert_data_eq;
351    use snapbox::str;
352
353    fn to_json<T: Serialize>(value: &T) -> String {
354        serde_json::to_string_pretty(value).unwrap()
355    }
356
357    #[test]
358    fn rustc_changed() {
359        let reason = DirtyReason::RustcChanged;
360        assert_data_eq!(
361            to_json(&reason),
362            str![[r#"
363{
364  "dirty_reason": "rustc-changed"
365}
366"#]]
367            .is_json()
368        );
369    }
370
371    #[test]
372    fn fresh_build() {
373        let reason = DirtyReason::FreshBuild;
374        assert_data_eq!(
375            to_json(&reason),
376            str![[r#"
377{
378  "dirty_reason": "fresh-build"
379}
380"#]]
381            .is_json()
382        );
383    }
384
385    #[test]
386    fn forced() {
387        let reason = DirtyReason::Forced;
388        assert_data_eq!(
389            to_json(&reason),
390            str![[r#"
391{
392  "dirty_reason": "forced"
393}
394"#]]
395            .is_json()
396        );
397    }
398
399    #[test]
400    fn nothing_obvious() {
401        let reason = DirtyReason::NothingObvious;
402        assert_data_eq!(
403            to_json(&reason),
404            str![[r#"
405{
406  "dirty_reason": "nothing-obvious"
407}
408"#]]
409            .is_json()
410        );
411    }
412
413    #[test]
414    fn features_changed() {
415        let reason = DirtyReason::FeaturesChanged {
416            old: "f1".to_string(),
417            new: "f1,f2".to_string(),
418        };
419        assert_data_eq!(
420            to_json(&reason),
421            str![[r#"
422{
423  "dirty_reason": "features-changed",
424  "new": "f1,f2",
425  "old": "f1"
426}
427"#]]
428            .is_json()
429        );
430    }
431
432    #[test]
433    fn rustflags_changed() {
434        let reason = DirtyReason::RustflagsChanged {
435            old: vec!["-C".into(), "opt-level=2".into()],
436            new: vec!["--cfg".into(), "tokio_unstable".into()],
437        };
438        assert_data_eq!(
439            to_json(&reason),
440            str![[r#"
441{
442  "dirty_reason": "rustflags-changed",
443  "old": [
444    "-C",
445    "opt-level=2"
446  ],
447  "new": [
448    "--cfg",
449    "tokio_unstable"
450  ]
451}
452"#]]
453        );
454    }
455
456    #[test]
457    fn env_var_changed_both_some() {
458        let reason = DirtyReason::EnvVarChanged {
459            name: "VAR".into(),
460            old_value: Some("old".into()),
461            new_value: Some("new".into()),
462        };
463        assert_data_eq!(
464            to_json(&reason),
465            str![[r#"
466{
467  "dirty_reason": "env-var-changed",
468  "name": "VAR",
469  "new_value": "new",
470  "old_value": "old"
471}
472"#]]
473            .is_json()
474        );
475    }
476
477    #[test]
478    fn env_var_changed_old_none() {
479        let reason = DirtyReason::EnvVarChanged {
480            name: "VAR".into(),
481            old_value: None,
482            new_value: Some("new".into()),
483        };
484        assert_data_eq!(
485            to_json(&reason),
486            str![[r#"
487{
488  "dirty_reason": "env-var-changed",
489  "name": "VAR",
490  "new_value": "new",
491  "old_value": null
492}
493"#]]
494            .is_json()
495        );
496    }
497
498    #[test]
499    fn dep_info_output_changed() {
500        let reason = DirtyReason::DepInfoOutputChanged {
501            old: "target/debug/old.d".into(),
502            new: "target/debug/new.d".into(),
503        };
504        assert_data_eq!(
505            to_json(&reason),
506            str![[r#"
507{
508  "dirty_reason": "dep-info-output-changed",
509  "old": "target/debug/old.d",
510  "new": "target/debug/new.d"
511}
512"#]]
513            .is_json()
514        );
515    }
516
517    #[test]
518    fn number_of_dependencies_changed() {
519        let reason = DirtyReason::NumberOfDependenciesChanged { old: 5, new: 7 };
520        assert_data_eq!(
521            to_json(&reason),
522            str![[r#"
523{
524  "dirty_reason": "number-of-dependencies-changed",
525  "old": 5,
526  "new": 7
527}
528"#]]
529            .is_json()
530        );
531    }
532
533    #[test]
534    fn unit_dependency_name_changed() {
535        let reason = DirtyReason::UnitDependencyNameChanged {
536            old: "old_dep".into(),
537            new: "new_dep".into(),
538        };
539        assert_data_eq!(
540            to_json(&reason),
541            str![[r#"
542{
543  "dirty_reason": "unit-dependency-name-changed",
544  "new": "new_dep",
545  "old": "old_dep"
546}
547"#]]
548            .is_json()
549        );
550    }
551
552    #[test]
553    fn unit_dependency_info_changed() {
554        let reason = DirtyReason::UnitDependencyInfoChanged {
555            unit: UnitIndex(15),
556        };
557        assert_data_eq!(
558            to_json(&reason),
559            str![[r#"
560{
561  "dirty_reason": "unit-dependency-info-changed",
562  "unit": 15
563}
564"#]]
565            .is_json()
566        );
567    }
568
569    #[test]
570    fn fs_status_stale() {
571        let reason = DirtyReason::FsStatusOutdated(FsStatus::Stale);
572        assert_data_eq!(
573            to_json(&reason),
574            str![[r#"
575{
576  "dirty_reason": "fs-status-outdated",
577  "fs_status": "stale"
578}
579"#]]
580            .is_json()
581        );
582    }
583
584    #[test]
585    fn fs_status_missing_file() {
586        let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::MissingFile {
587            path: "src/lib.rs".into(),
588        }));
589        assert_data_eq!(
590            to_json(&reason),
591            str![[r#"
592{
593  "dirty_reason": "fs-status-outdated",
594  "fs_status": "stale-item",
595  "path": "src/lib.rs",
596  "stale_item": "missing-file"
597}
598"#]]
599            .is_json()
600        );
601    }
602
603    #[test]
604    fn fs_status_changed_file() {
605        let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::ChangedFile {
606            reference: "target/debug/deps/libfoo-abc123.rmeta".into(),
607            reference_mtime: FileTime::from_unix_time(1730567890, 123000000),
608            stale: "src/lib.rs".into(),
609            stale_mtime: FileTime::from_unix_time(1730567891, 456000000),
610        }));
611        assert_data_eq!(
612            to_json(&reason),
613            str![[r#"
614{
615  "dirty_reason": "fs-status-outdated",
616  "fs_status": "stale-item",
617  "reference": "target/debug/deps/libfoo-abc123.rmeta",
618  "reference_mtime": 1730567890123.0,
619  "stale": "src/lib.rs",
620  "stale_item": "changed-file",
621  "stale_mtime": 1730567891456.0
622}
623"#]]
624            .is_json()
625        );
626    }
627
628    #[test]
629    fn fs_status_changed_checksum() {
630        use super::dep_info::ChecksumAlgo;
631        let reason =
632            DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::ChangedChecksum {
633                source: "src/main.rs".into(),
634                stored_checksum: Checksum::new(ChecksumAlgo::Sha256, [0xaa; 32]),
635                new_checksum: Checksum::new(ChecksumAlgo::Sha256, [0xbb; 32]),
636            }));
637        assert_data_eq!(
638            to_json(&reason),
639            str![[r#"
640{
641  "dirty_reason": "fs-status-outdated",
642  "fs_status": "stale-item",
643  "new_checksum": "sha256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
644  "source": "src/main.rs",
645  "stale_item": "changed-checksum",
646  "stored_checksum": "sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
647}
648"#]]
649            .is_json()
650        );
651    }
652
653    #[test]
654    fn fs_status_stale_dependency() {
655        let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleDependency {
656            unit: UnitIndex(42),
657            dep_mtime: FileTime::from_unix_time(1730567892, 789000000),
658            max_mtime: FileTime::from_unix_time(1730567890, 123000000),
659        });
660        assert_data_eq!(
661            to_json(&reason),
662            str![[r#"
663{
664  "dep_mtime": 1730567892789.0,
665  "dirty_reason": "fs-status-outdated",
666  "fs_status": "stale-dependency",
667  "max_mtime": 1730567890123.0,
668  "unit": 42
669}
670"#]]
671            .is_json()
672        );
673    }
674
675    #[test]
676    fn fs_status_stale_dep_fingerprint() {
677        let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleDepFingerprint {
678            unit: UnitIndex(42),
679        });
680        assert_data_eq!(
681            to_json(&reason),
682            str![[r#"
683{
684  "dirty_reason": "fs-status-outdated",
685  "fs_status": "stale-dep-fingerprint",
686  "unit": 42
687}
688"#]]
689            .is_json()
690        );
691    }
692
693    #[test]
694    fn fs_status_unable_to_read_file() {
695        let reason =
696            DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::UnableToReadFile {
697                path: "src/lib.rs".into(),
698            }));
699        assert_data_eq!(
700            to_json(&reason),
701            str![[r#"
702{
703  "dirty_reason": "fs-status-outdated",
704  "fs_status": "stale-item",
705  "stale_item": "unable-to-read-file",
706  "path": "src/lib.rs"
707}
708"#]]
709        );
710    }
711
712    #[test]
713    fn fs_status_failed_to_read_metadata() {
714        let reason =
715            DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::FailedToReadMetadata {
716                path: "src/lib.rs".into(),
717            }));
718        assert_data_eq!(
719            to_json(&reason),
720            str![[r#"
721{
722  "dirty_reason": "fs-status-outdated",
723  "fs_status": "stale-item",
724  "stale_item": "failed-to-read-metadata",
725  "path": "src/lib.rs"
726}
727"#]]
728        );
729    }
730
731    #[test]
732    fn fs_status_file_size_changed() {
733        let reason =
734            DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::FileSizeChanged {
735                path: "src/lib.rs".into(),
736                old_size: 1024,
737                new_size: 2048,
738            }));
739        assert_data_eq!(
740            to_json(&reason),
741            str![[r#"
742{
743  "dirty_reason": "fs-status-outdated",
744  "fs_status": "stale-item",
745  "stale_item": "file-size-changed",
746  "path": "src/lib.rs",
747  "old_size": 1024,
748  "new_size": 2048
749}
750"#]]
751        );
752    }
753
754    #[test]
755    fn fs_status_missing_checksum() {
756        let reason =
757            DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::MissingChecksum {
758                path: "src/lib.rs".into(),
759            }));
760        assert_data_eq!(
761            to_json(&reason),
762            str![[r#"
763{
764  "dirty_reason": "fs-status-outdated",
765  "fs_status": "stale-item",
766  "stale_item": "missing-checksum",
767  "path": "src/lib.rs"
768}
769"#]]
770        );
771    }
772
773    #[test]
774    fn fs_status_changed_env() {
775        let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::ChangedEnv {
776            var: "VAR".into(),
777            previous: Some("old".into()),
778            current: Some("new".into()),
779        }));
780        assert_data_eq!(
781            to_json(&reason),
782            str![[r#"
783{
784  "dirty_reason": "fs-status-outdated",
785  "fs_status": "stale-item",
786  "stale_item": "changed-env",
787  "var": "VAR",
788  "previous": "old",
789  "current": "new"
790}
791"#]]
792        );
793    }
794
795    #[test]
796    fn checksum_use_changed() {
797        let reason = DirtyReason::ChecksumUseChanged { old: false };
798        assert_data_eq!(
799            to_json(&reason),
800            str![[r#"
801{
802  "dirty_reason": "checksum-use-changed",
803  "old": false
804}
805"#]]
806            .is_json()
807        );
808    }
809
810    #[test]
811    fn rerun_if_changed_output_paths_changed() {
812        let reason = DirtyReason::RerunIfChangedOutputPathsChanged {
813            old: vec!["file1.txt".into(), "file2.txt".into()],
814            new: vec!["file1.txt".into(), "file2.txt".into(), "file3.txt".into()],
815        };
816        assert_data_eq!(
817            to_json(&reason),
818            str![[r#"
819{
820  "dirty_reason": "rerun-if-changed-output-paths-changed",
821  "old": [
822    "file1.txt",
823    "file2.txt"
824  ],
825  "new": [
826    "file1.txt",
827    "file2.txt",
828    "file3.txt"
829  ]
830}
831"#]]
832            .is_json()
833        );
834    }
835
836    #[test]
837    fn local_fingerprint_type_changed() {
838        let reason = DirtyReason::LocalFingerprintTypeChanged {
839            old: "precalculated".to_owned(),
840            new: "rerun-if-changed".to_owned(),
841        };
842        assert_data_eq!(
843            to_json(&reason),
844            str![[r#"
845{
846  "dirty_reason": "local-fingerprint-type-changed",
847  "new": "rerun-if-changed",
848  "old": "precalculated"
849}
850"#]]
851            .is_json()
852        );
853    }
854}