cargo/core/compiler/fingerprint/
dirty_reason.rs

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