run_make_support/diff/
mod.rs1use 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 #[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 pub fn expected_file<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
48 let path = path.as_ref();
49 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 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 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 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 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 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}