1use crate::util::data_structures::HashMap;
2use std::fmt;
3use std::fmt::Debug;
4
5use serde::Serialize;
6
7use super::*;
8use crate::compiler::UnitIndex;
9use crate::context::FingerprintMethod;
10use cargo_util_terminal::Shell;
11
12#[derive(Clone, Debug, Serialize, Deserialize)]
17#[serde(tag = "dirty_reason", rename_all = "kebab-case")]
18pub enum DirtyReason {
19 RustcChanged,
20 FeaturesChanged {
21 old: String,
22 new: String,
23 },
24 DeclaredFeaturesChanged {
25 old: String,
26 new: String,
27 },
28 TargetConfigurationChanged,
29 PathToSourceChanged,
30 ProfileConfigurationChanged,
31 RustflagsChanged {
32 old: Vec<String>,
33 new: Vec<String>,
34 },
35 ConfigSettingsChanged,
36 CompileKindChanged,
37 LocalLengthsChanged,
38 PrecalculatedComponentsChanged {
39 old: String,
40 new: String,
41 },
42 FingerprintMethodChanged {
43 old: FingerprintMethod,
44 new: FingerprintMethod,
45 },
46 DepInfoOutputChanged {
47 old: PathBuf,
48 new: PathBuf,
49 },
50 RerunIfChangedOutputFileChanged {
51 old: PathBuf,
52 new: PathBuf,
53 },
54 RerunIfChangedOutputPathsChanged {
55 old: Vec<PathBuf>,
56 new: Vec<PathBuf>,
57 },
58 EnvVarsChanged {
59 old: String,
60 new: String,
61 },
62 EnvVarChanged {
63 name: String,
64 old_value: Option<String>,
65 new_value: Option<String>,
66 },
67 LocalFingerprintTypeChanged {
68 old: String,
69 new: String,
70 },
71 NumberOfDependenciesChanged {
72 old: usize,
73 new: usize,
74 },
75 UnitDependencyNameChanged {
76 old: InternedString,
77 new: InternedString,
78 },
79 UnitDependencyInfoChanged {
80 unit: UnitIndex,
81 },
82 FsStatusOutdated(FsStatus),
83 NothingObvious,
84 Forced,
85 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 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 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(
152 &self,
153 s: &mut Shell,
154 unit: &Unit,
155 root: &Path,
156 index_to_unit: &HashMap<UnitIndex, Unit>,
157 ) -> CargoResult<()> {
158 match self {
159 DirtyReason::RustcChanged => s.dirty_because(unit, "the toolchain changed"),
160 DirtyReason::FeaturesChanged { .. } => {
161 s.dirty_because(unit, "the list of features changed")
162 }
163 DirtyReason::DeclaredFeaturesChanged { .. } => {
164 s.dirty_because(unit, "the list of declared features changed")
165 }
166 DirtyReason::TargetConfigurationChanged => {
167 s.dirty_because(unit, "the target configuration changed")
168 }
169 DirtyReason::PathToSourceChanged => {
170 s.dirty_because(unit, "the path to the source changed")
171 }
172 DirtyReason::ProfileConfigurationChanged => {
173 s.dirty_because(unit, "the profile configuration changed")
174 }
175 DirtyReason::RustflagsChanged { .. } => s.dirty_because(unit, "the rustflags changed"),
176 DirtyReason::ConfigSettingsChanged => {
177 s.dirty_because(unit, "the config settings changed")
178 }
179 DirtyReason::CompileKindChanged => {
180 s.dirty_because(unit, "the rustc compile kind changed")
181 }
182 DirtyReason::LocalLengthsChanged => {
183 s.dirty_because(unit, "the local lengths changed")?;
184 s.note(
185 "this could happen because of added/removed `cargo::rerun-if` instructions in the build script",
186 )?;
187
188 Ok(())
189 }
190 DirtyReason::PrecalculatedComponentsChanged { .. } => {
191 s.dirty_because(unit, "the precalculated components changed")
192 }
193 DirtyReason::FingerprintMethodChanged { old, new } => s.dirty_because(
194 unit,
195 format_args!("the prior compilation fingerprinted build inputs using `{old}` and this one used `{new}`"),
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 { unit: dep_unit } => {
224 let dep_name = index_to_unit.get(dep_unit).map(|u| u.pkg.name()).unwrap();
225 s.dirty_because(
226 unit,
227 format_args!("info of dependency `{dep_name}` changed"),
228 )
229 }
230 DirtyReason::FsStatusOutdated(status) => match status {
231 FsStatus::Stale => s.dirty_because(unit, "stale, unknown reason"),
232 FsStatus::StaleItem(item) => match item {
233 StaleItem::MissingFile { path } => {
234 let file = path.strip_prefix(root).unwrap_or(&path);
235 s.dirty_because(
236 unit,
237 format_args!("the file `{}` is missing", file.display()),
238 )
239 }
240 StaleItem::UnableToReadFile { path } => {
241 let file = path.strip_prefix(root).unwrap_or(&path);
242 s.dirty_because(
243 unit,
244 format_args!("the file `{}` could not be read", file.display()),
245 )
246 }
247 StaleItem::FailedToReadMetadata { path } => {
248 let file = path.strip_prefix(root).unwrap_or(&path);
249 s.dirty_because(
250 unit,
251 format_args!("couldn't read metadata for file `{}`", file.display()),
252 )
253 }
254 StaleItem::ChangedFile {
255 stale,
256 stale_mtime,
257 reference_mtime,
258 ..
259 } => {
260 let file = stale.strip_prefix(root).unwrap_or(&stale);
261 let after = Self::after(*reference_mtime, *stale_mtime, "last build");
262 s.dirty_because(
263 unit,
264 format_args!("the file `{}` has changed ({after})", file.display()),
265 )
266 }
267 StaleItem::ChangedChecksum {
268 source,
269 stored_checksum,
270 new_checksum,
271 } => {
272 let file = source.strip_prefix(root).unwrap_or(&source);
273 s.dirty_because(
274 unit,
275 format_args!(
276 "the file `{}` has changed (checksum didn't match, {stored_checksum} != {new_checksum})",
277 file.display(),
278 ),
279 )
280 }
281 StaleItem::FileSizeChanged {
282 path,
283 old_size,
284 new_size,
285 } => {
286 let file = path.strip_prefix(root).unwrap_or(&path);
287 s.dirty_because(
288 unit,
289 format_args!(
290 "file size changed ({old_size} != {new_size}) for `{}`",
291 file.display()
292 ),
293 )
294 }
295 StaleItem::MissingChecksum { path } => {
296 let file = path.strip_prefix(root).unwrap_or(&path);
297 s.dirty_because(
298 unit,
299 format_args!("the checksum for file `{}` is missing", file.display()),
300 )
301 }
302 StaleItem::ChangedEnv { var, .. } => s.dirty_because(
303 unit,
304 format_args!("the environment variable {var} changed"),
305 ),
306 },
307 FsStatus::StaleDependency {
308 unit: dep_unit,
309 dep_mtime,
310 max_mtime,
311 } => {
312 let dep_name = index_to_unit.get(dep_unit).map(|u| u.pkg.name()).unwrap();
313 let after = Self::after(*max_mtime, *dep_mtime, "last build");
314 s.dirty_because(
315 unit,
316 format_args!("the dependency `{dep_name}` was rebuilt ({after})"),
317 )
318 }
319 FsStatus::StaleDepFingerprint { unit: dep_unit } => {
320 let dep_name = index_to_unit.get(dep_unit).map(|u| u.pkg.name()).unwrap();
321 s.dirty_because(
322 unit,
323 format_args!("the dependency `{dep_name}` was rebuilt"),
324 )
325 }
326 FsStatus::UpToDate { .. } => {
327 unreachable!()
328 }
329 },
330 DirtyReason::NothingObvious => {
331 s.dirty_because(unit, "the fingerprint comparison turned up nothing obvious")
333 }
334 DirtyReason::Forced => s.dirty_because(unit, "forced"),
335 DirtyReason::FreshBuild => s.dirty_because(unit, "fresh build"),
336 }
337 }
338}
339
340#[cfg(test)]
343mod json_schema {
344 use super::*;
345 use snapbox::IntoData;
346 use snapbox::assert_data_eq;
347 use snapbox::str;
348
349 fn to_json<T: Serialize>(value: &T) -> String {
350 serde_json::to_string_pretty(value).unwrap()
351 }
352
353 #[test]
354 fn rustc_changed() {
355 let reason = DirtyReason::RustcChanged;
356 assert_data_eq!(
357 to_json(&reason),
358 str![[r#"
359{
360 "dirty_reason": "rustc-changed"
361}
362"#]]
363 .is_json()
364 );
365 }
366
367 #[test]
368 fn fresh_build() {
369 let reason = DirtyReason::FreshBuild;
370 assert_data_eq!(
371 to_json(&reason),
372 str![[r#"
373{
374 "dirty_reason": "fresh-build"
375}
376"#]]
377 .is_json()
378 );
379 }
380
381 #[test]
382 fn forced() {
383 let reason = DirtyReason::Forced;
384 assert_data_eq!(
385 to_json(&reason),
386 str![[r#"
387{
388 "dirty_reason": "forced"
389}
390"#]]
391 .is_json()
392 );
393 }
394
395 #[test]
396 fn nothing_obvious() {
397 let reason = DirtyReason::NothingObvious;
398 assert_data_eq!(
399 to_json(&reason),
400 str![[r#"
401{
402 "dirty_reason": "nothing-obvious"
403}
404"#]]
405 .is_json()
406 );
407 }
408
409 #[test]
410 fn features_changed() {
411 let reason = DirtyReason::FeaturesChanged {
412 old: "f1".to_string(),
413 new: "f1,f2".to_string(),
414 };
415 assert_data_eq!(
416 to_json(&reason),
417 str![[r#"
418{
419 "dirty_reason": "features-changed",
420 "new": "f1,f2",
421 "old": "f1"
422}
423"#]]
424 .is_json()
425 );
426 }
427
428 #[test]
429 fn rustflags_changed() {
430 let reason = DirtyReason::RustflagsChanged {
431 old: vec!["-C".into(), "opt-level=2".into()],
432 new: vec!["--cfg".into(), "tokio_unstable".into()],
433 };
434 assert_data_eq!(
435 to_json(&reason),
436 str![[r#"
437{
438 "dirty_reason": "rustflags-changed",
439 "old": [
440 "-C",
441 "opt-level=2"
442 ],
443 "new": [
444 "--cfg",
445 "tokio_unstable"
446 ]
447}
448"#]]
449 );
450 }
451
452 #[test]
453 fn env_var_changed_both_some() {
454 let reason = DirtyReason::EnvVarChanged {
455 name: "VAR".into(),
456 old_value: Some("old".into()),
457 new_value: Some("new".into()),
458 };
459 assert_data_eq!(
460 to_json(&reason),
461 str![[r#"
462{
463 "dirty_reason": "env-var-changed",
464 "name": "VAR",
465 "new_value": "new",
466 "old_value": "old"
467}
468"#]]
469 .is_json()
470 );
471 }
472
473 #[test]
474 fn env_var_changed_old_none() {
475 let reason = DirtyReason::EnvVarChanged {
476 name: "VAR".into(),
477 old_value: None,
478 new_value: Some("new".into()),
479 };
480 assert_data_eq!(
481 to_json(&reason),
482 str![[r#"
483{
484 "dirty_reason": "env-var-changed",
485 "name": "VAR",
486 "new_value": "new",
487 "old_value": null
488}
489"#]]
490 .is_json()
491 );
492 }
493
494 #[test]
495 fn dep_info_output_changed() {
496 let reason = DirtyReason::DepInfoOutputChanged {
497 old: "target/debug/old.d".into(),
498 new: "target/debug/new.d".into(),
499 };
500 assert_data_eq!(
501 to_json(&reason),
502 str![[r#"
503{
504 "dirty_reason": "dep-info-output-changed",
505 "old": "target/debug/old.d",
506 "new": "target/debug/new.d"
507}
508"#]]
509 .is_json()
510 );
511 }
512
513 #[test]
514 fn number_of_dependencies_changed() {
515 let reason = DirtyReason::NumberOfDependenciesChanged { old: 5, new: 7 };
516 assert_data_eq!(
517 to_json(&reason),
518 str![[r#"
519{
520 "dirty_reason": "number-of-dependencies-changed",
521 "old": 5,
522 "new": 7
523}
524"#]]
525 .is_json()
526 );
527 }
528
529 #[test]
530 fn unit_dependency_name_changed() {
531 let reason = DirtyReason::UnitDependencyNameChanged {
532 old: "old_dep".into(),
533 new: "new_dep".into(),
534 };
535 assert_data_eq!(
536 to_json(&reason),
537 str![[r#"
538{
539 "dirty_reason": "unit-dependency-name-changed",
540 "new": "new_dep",
541 "old": "old_dep"
542}
543"#]]
544 .is_json()
545 );
546 }
547
548 #[test]
549 fn unit_dependency_info_changed() {
550 let reason = DirtyReason::UnitDependencyInfoChanged {
551 unit: UnitIndex(15),
552 };
553 assert_data_eq!(
554 to_json(&reason),
555 str![[r#"
556{
557 "dirty_reason": "unit-dependency-info-changed",
558 "unit": 15
559}
560"#]]
561 .is_json()
562 );
563 }
564
565 #[test]
566 fn fs_status_stale() {
567 let reason = DirtyReason::FsStatusOutdated(FsStatus::Stale);
568 assert_data_eq!(
569 to_json(&reason),
570 str![[r#"
571{
572 "dirty_reason": "fs-status-outdated",
573 "fs_status": "stale"
574}
575"#]]
576 .is_json()
577 );
578 }
579
580 #[test]
581 fn fs_status_missing_file() {
582 let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::MissingFile {
583 path: "src/lib.rs".into(),
584 }));
585 assert_data_eq!(
586 to_json(&reason),
587 str![[r#"
588{
589 "dirty_reason": "fs-status-outdated",
590 "fs_status": "stale-item",
591 "path": "src/lib.rs",
592 "stale_item": "missing-file"
593}
594"#]]
595 .is_json()
596 );
597 }
598
599 #[test]
600 fn fs_status_changed_file() {
601 let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::ChangedFile {
602 reference: "target/debug/deps/libfoo-abc123.rmeta".into(),
603 reference_mtime: FileTime::from_unix_time(1730567890, 123000000),
604 stale: "src/lib.rs".into(),
605 stale_mtime: FileTime::from_unix_time(1730567891, 456000000),
606 }));
607 assert_data_eq!(
608 to_json(&reason),
609 str![[r#"
610{
611 "dirty_reason": "fs-status-outdated",
612 "fs_status": "stale-item",
613 "reference": "target/debug/deps/libfoo-abc123.rmeta",
614 "reference_mtime": 1730567890123.0,
615 "stale": "src/lib.rs",
616 "stale_item": "changed-file",
617 "stale_mtime": 1730567891456.0
618}
619"#]]
620 .is_json()
621 );
622 }
623
624 #[test]
625 fn fs_status_changed_checksum() {
626 use super::dep_info::ChecksumAlgo;
627 let reason =
628 DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::ChangedChecksum {
629 source: "src/main.rs".into(),
630 stored_checksum: Checksum::new(ChecksumAlgo::Sha256, [0xaa; 32]),
631 new_checksum: Checksum::new(ChecksumAlgo::Sha256, [0xbb; 32]),
632 }));
633 assert_data_eq!(
634 to_json(&reason),
635 str![[r#"
636{
637 "dirty_reason": "fs-status-outdated",
638 "fs_status": "stale-item",
639 "new_checksum": "sha256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
640 "source": "src/main.rs",
641 "stale_item": "changed-checksum",
642 "stored_checksum": "sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
643}
644"#]]
645 .is_json()
646 );
647 }
648
649 #[test]
650 fn fs_status_stale_dependency() {
651 let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleDependency {
652 unit: UnitIndex(42),
653 dep_mtime: FileTime::from_unix_time(1730567892, 789000000),
654 max_mtime: FileTime::from_unix_time(1730567890, 123000000),
655 });
656 assert_data_eq!(
657 to_json(&reason),
658 str![[r#"
659{
660 "dep_mtime": 1730567892789.0,
661 "dirty_reason": "fs-status-outdated",
662 "fs_status": "stale-dependency",
663 "max_mtime": 1730567890123.0,
664 "unit": 42
665}
666"#]]
667 .is_json()
668 );
669 }
670
671 #[test]
672 fn fs_status_stale_dep_fingerprint() {
673 let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleDepFingerprint {
674 unit: UnitIndex(42),
675 });
676 assert_data_eq!(
677 to_json(&reason),
678 str![[r#"
679{
680 "dirty_reason": "fs-status-outdated",
681 "fs_status": "stale-dep-fingerprint",
682 "unit": 42
683}
684"#]]
685 .is_json()
686 );
687 }
688
689 #[test]
690 fn fs_status_unable_to_read_file() {
691 let reason =
692 DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::UnableToReadFile {
693 path: "src/lib.rs".into(),
694 }));
695 assert_data_eq!(
696 to_json(&reason),
697 str![[r#"
698{
699 "dirty_reason": "fs-status-outdated",
700 "fs_status": "stale-item",
701 "stale_item": "unable-to-read-file",
702 "path": "src/lib.rs"
703}
704"#]]
705 );
706 }
707
708 #[test]
709 fn fs_status_failed_to_read_metadata() {
710 let reason =
711 DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::FailedToReadMetadata {
712 path: "src/lib.rs".into(),
713 }));
714 assert_data_eq!(
715 to_json(&reason),
716 str![[r#"
717{
718 "dirty_reason": "fs-status-outdated",
719 "fs_status": "stale-item",
720 "stale_item": "failed-to-read-metadata",
721 "path": "src/lib.rs"
722}
723"#]]
724 );
725 }
726
727 #[test]
728 fn fs_status_file_size_changed() {
729 let reason =
730 DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::FileSizeChanged {
731 path: "src/lib.rs".into(),
732 old_size: 1024,
733 new_size: 2048,
734 }));
735 assert_data_eq!(
736 to_json(&reason),
737 str![[r#"
738{
739 "dirty_reason": "fs-status-outdated",
740 "fs_status": "stale-item",
741 "stale_item": "file-size-changed",
742 "path": "src/lib.rs",
743 "old_size": 1024,
744 "new_size": 2048
745}
746"#]]
747 );
748 }
749
750 #[test]
751 fn fs_status_missing_checksum() {
752 let reason =
753 DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::MissingChecksum {
754 path: "src/lib.rs".into(),
755 }));
756 assert_data_eq!(
757 to_json(&reason),
758 str![[r#"
759{
760 "dirty_reason": "fs-status-outdated",
761 "fs_status": "stale-item",
762 "stale_item": "missing-checksum",
763 "path": "src/lib.rs"
764}
765"#]]
766 );
767 }
768
769 #[test]
770 fn fs_status_changed_env() {
771 let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleItem(StaleItem::ChangedEnv {
772 var: "VAR".into(),
773 previous: Some("old".into()),
774 current: Some("new".into()),
775 }));
776 assert_data_eq!(
777 to_json(&reason),
778 str![[r#"
779{
780 "dirty_reason": "fs-status-outdated",
781 "fs_status": "stale-item",
782 "stale_item": "changed-env",
783 "var": "VAR",
784 "previous": "old",
785 "current": "new"
786}
787"#]]
788 );
789 }
790
791 #[test]
792 fn fingerprint_method_changed() {
793 let reason = DirtyReason::FingerprintMethodChanged {
794 old: FingerprintMethod::Mtime,
795 new: FingerprintMethod::Content,
796 };
797 assert_data_eq!(
798 to_json(&reason),
799 str![[r#"
800{
801 "dirty_reason": "fingerprint-method-changed",
802 "new": "content",
803 "old": "mtime"
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}