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