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#[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 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 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 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 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#[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}