1use std::{
2 env, fmt,
3 fs::{self, create_dir_all, File},
4 io::Read,
5 ops::Deref,
6 path::Path,
7};
8
9use serde::Serialize;
10use tracing::debug;
11
12use crate::paths;
13
14#[must_use]
15pub struct TestOutput<R> {
16 pub errors: StdErr,
18 pub result: R,
19}
20
21pub type StdErr = NormalizedOutput;
22
23#[derive(Debug, Clone, Hash)]
24pub struct Diff {
25 pub actual: NormalizedOutput,
26 pub expected: NormalizedOutput,
28}
29
30#[derive(Clone, Ord, PartialOrd, PartialEq, Eq, Default, Hash)]
42pub struct NormalizedOutput(String);
43
44impl fmt::Display for NormalizedOutput {
45 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
46 fmt::Display::fmt(&self.0, f)
47 }
48}
49
50impl fmt::Debug for NormalizedOutput {
51 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
52 fmt::Display::fmt(&self.0, f)
53 }
54}
55
56fn normalize_input(input: String, skip_last_newline: bool) -> String {
57 let manifest_dirs = [
58 adjust_canonicalization(paths::manifest_dir()),
59 paths::manifest_dir().to_string_lossy().to_string(),
60 ]
61 .into_iter()
62 .flat_map(|dir| [dir.replace('\\', "\\\\"), dir.replace('\\', "/"), dir])
63 .collect::<Vec<_>>();
64
65 let input = input.replace("\r\n", "\n");
66
67 let mut buf = String::new();
68
69 for line in input.lines() {
70 if manifest_dirs.iter().any(|dir| line.contains(&**dir)) {
71 let mut s = line.to_string();
72
73 for dir in &manifest_dirs {
74 s = s.replace(&**dir, "$DIR");
75 }
76 s = s.replace("\\\\", "\\").replace('\\', "/");
77 let s = if cfg!(target_os = "windows") {
78 s.replace("//?/$DIR", "$DIR").replace("/?/$DIR", "$DIR")
79 } else {
80 s
81 };
82 buf.push_str(&s)
83 } else {
84 buf.push_str(line);
85 }
86
87 buf.push('\n');
88 }
89
90 if skip_last_newline && !matches!(input.chars().last(), Some('\n')) {
91 buf.truncate(buf.len() - 1);
92 }
93
94 buf
95}
96
97impl NormalizedOutput {
98 pub fn new_raw(s: String) -> Self {
99 if s.is_empty() {
100 return NormalizedOutput(s);
101 }
102
103 NormalizedOutput(normalize_input(s, true))
104 }
105
106 pub fn compare_json_to_file<T>(actual: &T, path: &Path)
107 where
108 T: Serialize,
109 {
110 let actual_value =
111 serde_json::to_value(actual).expect("failed to serialize the actual value to json");
112
113 if let Ok(expected) = fs::read_to_string(path) {
114 let expected_value = serde_json::from_str::<serde_json::Value>(&expected)
115 .expect("failed to deserialize the expected value from json");
116
117 if expected_value == actual_value {
118 return;
119 }
120 }
121
122 let actual_json_string = serde_json::to_string_pretty(&actual_value)
123 .expect("failed to serialize the actual value to json");
124
125 let _ = NormalizedOutput::from(actual_json_string).compare_to_file(path);
126 }
127
128 pub fn compare_to_file<P>(self, path: P) -> Result<(), Diff>
132 where
133 P: AsRef<Path>,
134 {
135 let path = path.as_ref();
136 let path = path.canonicalize().unwrap_or_else(|err| {
137 debug!(
138 "compare_to_file: failed to canonicalize outfile path `{}`: {:?}",
139 path.display(),
140 err
141 );
142 path.to_path_buf()
143 });
144
145 let expected: NormalizedOutput = NormalizedOutput(
146 File::open(&path)
147 .map(|mut file| {
148 let mut buf = String::new();
149 file.read_to_string(&mut buf).unwrap();
150 buf
151 })
152 .unwrap_or_else(|_| {
153 String::new()
155 })
156 .replace("\r\n", "\n"),
157 );
158
159 if expected == self {
160 return Ok(());
161 }
162
163 debug!("Comparing output to {}", path.display());
164 create_dir_all(path.parent().unwrap()).expect("failed to run `mkdir -p`");
165
166 let update = std::env::var("UPDATE").unwrap_or_default() == "1";
167 if update {
168 crate::write_to_file(&path, &self.0);
169
170 debug!("Updating file {}", path.display());
171 return Ok(());
172 }
173
174 if !update && self.0.lines().count() <= 5 {
175 assert_eq!(expected, self, "Actual:\n{self}");
176 }
177
178 let diff = Diff {
179 expected,
180 actual: self,
181 };
182
183 if env::var("DIFF").unwrap_or_default() == "0" {
184 assert_eq!(diff.expected, diff.actual, "Actual:\n{}", diff.actual);
185 } else {
186 pretty_assertions::assert_eq!(diff.expected, diff.actual, "Actual:\n{}", diff.actual);
187 }
188
189 Err(diff)
191 }
192}
193
194impl From<String> for NormalizedOutput {
195 fn from(s: String) -> Self {
196 if s.is_empty() {
197 return NormalizedOutput(s);
198 }
199
200 NormalizedOutput(normalize_input(s, false))
201 }
202}
203
204impl Deref for NormalizedOutput {
205 type Target = str;
206
207 fn deref(&self) -> &str {
208 &self.0
209 }
210}
211
212pub type StdOut = NormalizedOutput;
213
214impl<R> TestOutput<Option<R>> {
215 pub fn expect_err(self, _path: &Path) {}
218}
219
220#[cfg(not(target_os = "windows"))]
221fn adjust_canonicalization<P: AsRef<Path>>(p: P) -> String {
222 p.as_ref().display().to_string()
223}
224
225#[cfg(target_os = "windows")]
226fn adjust_canonicalization<P: AsRef<Path>>(p: P) -> String {
227 const VERBATIM_PREFIX: &str = r#"\\?\"#;
228 let p = p.as_ref().display().to_string();
229 if let Some(stripped) = p.strip_prefix(VERBATIM_PREFIX) {
230 stripped.to_string()
231 } else {
232 p
233 }
234}