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