run_make_support/diff/
mod.rs

1use std::path::{Path, PathBuf};
2
3use build_helper::drop_bomb::DropBomb;
4use regex::Regex;
5use similar::TextDiff;
6
7use crate::fs;
8
9#[cfg(test)]
10mod tests;
11
12#[track_caller]
13pub fn diff() -> Diff {
14    Diff::new()
15}
16
17#[derive(Debug)]
18#[must_use]
19pub struct Diff {
20    expected: Option<String>,
21    expected_name: Option<String>,
22    expected_file: Option<PathBuf>,
23    actual: Option<String>,
24    actual_name: Option<String>,
25    normalizers: Vec<(String, String)>,
26    bless_dir: Option<String>,
27    drop_bomb: DropBomb,
28}
29
30impl Diff {
31    /// Construct a bare `diff` invocation.
32    #[track_caller]
33    pub fn new() -> Self {
34        Self {
35            expected: None,
36            expected_name: None,
37            expected_file: None,
38            actual: None,
39            actual_name: None,
40            normalizers: Vec::new(),
41            bless_dir: std::env::var("RUSTC_BLESS_TEST").ok(),
42            drop_bomb: DropBomb::arm("diff"),
43        }
44    }
45
46    /// Specify the expected output for the diff from a file.
47    pub fn expected_file<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
48        let path = path.as_ref();
49        // In `--bless` mode, create the snapshot file if it doesn't already exist.
50        // The empty file will be overwritten with the actual text.
51        if self.bless_dir.is_some()
52            && let Ok(false) = std::fs::exists(path)
53        {
54            fs::write(path, "");
55        }
56        let content = fs::read_to_string(path);
57        let name = path.to_string_lossy().to_string();
58
59        self.expected_file = Some(path.into());
60        self.expected = Some(content);
61        self.expected_name = Some(name);
62        self
63    }
64
65    /// Specify the expected output for the diff from a given text string.
66    pub fn expected_text<T: AsRef<[u8]>>(&mut self, name: &str, text: T) -> &mut Self {
67        self.expected = Some(String::from_utf8_lossy(text.as_ref()).to_string());
68        self.expected_name = Some(name.to_string());
69        self
70    }
71
72    /// Specify the actual output for the diff from a file.
73    pub fn actual_file<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
74        let path = path.as_ref();
75        let content = fs::read_to_string(path);
76        let name = path.to_string_lossy().to_string();
77
78        self.actual = Some(content);
79        self.actual_name = Some(name);
80        self
81    }
82
83    /// Specify the actual output for the diff from a given text string.
84    pub fn actual_text<T: AsRef<[u8]>>(&mut self, name: &str, text: T) -> &mut Self {
85        self.actual = Some(String::from_utf8_lossy(text.as_ref()).to_string());
86        self.actual_name = Some(name.to_string());
87        self
88    }
89
90    /// Specify a regex that should replace text in the "actual" text that will be compared.
91    pub fn normalize<R: Into<String>, I: Into<String>>(
92        &mut self,
93        regex: R,
94        replacement: I,
95    ) -> &mut Self {
96        self.normalizers.push((regex.into(), replacement.into()));
97        self
98    }
99
100    fn run_common(&self) -> (&str, &str, String, String) {
101        let expected = self.expected.as_ref().expect("expected text not set");
102        let mut actual = self.actual.as_ref().expect("actual text not set").to_string();
103        let expected_name = self.expected_name.as_ref().unwrap();
104        let actual_name = self.actual_name.as_ref().unwrap();
105        for (regex, replacement) in &self.normalizers {
106            let re = Regex::new(regex).expect("bad regex in custom normalization rule");
107            actual = re.replace_all(&actual, replacement).into_owned();
108        }
109
110        let output = TextDiff::from_lines(expected, &actual)
111            .unified_diff()
112            .header(expected_name, actual_name)
113            .to_string();
114
115        (expected_name, actual_name, output, actual)
116    }
117
118    #[track_caller]
119    pub fn run(&mut self) {
120        self.drop_bomb.defuse();
121        let (expected_name, actual_name, output, actual) = self.run_common();
122
123        if !output.is_empty() {
124            if self.maybe_bless_expected_file(&actual) {
125                return;
126            }
127            panic!(
128                "test failed: `{}` is different from `{}`\n\n{}",
129                expected_name, actual_name, output
130            )
131        }
132    }
133
134    #[track_caller]
135    pub fn run_fail(&mut self) {
136        self.drop_bomb.defuse();
137        let (expected_name, actual_name, output, actual) = self.run_common();
138
139        if output.is_empty() {
140            if self.maybe_bless_expected_file(&actual) {
141                return;
142            }
143            panic!(
144                "test failed: `{}` is not different from `{}`\n\n{}",
145                expected_name, actual_name, output
146            )
147        }
148    }
149
150    /// If we have an expected file to write into, and `RUSTC_BLESS_TEST` is
151    /// set, then write the actual output into the file and return `true`.
152    ///
153    /// We assume that `RUSTC_BLESS_TEST` contains the path to the original test's
154    /// source directory. That lets us bless the original snapshot file in the
155    /// source tree, not the copy in `rmake_out` that we would normally use.
156    fn maybe_bless_expected_file(&self, actual: &str) -> bool {
157        let Some(ref expected_file) = self.expected_file else {
158            return false;
159        };
160        let Some(ref bless_dir) = self.bless_dir else {
161            return false;
162        };
163
164        let bless_file = Path::new(&bless_dir).join(expected_file);
165        println!("Blessing `{}`", bless_file.display());
166        fs::write(bless_file, actual);
167        true
168    }
169}