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