testing/
output.rs

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    /// Errors produced by `swc_common::error::Handler`.
17    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    /// Output stored in file.
27    pub expected: NormalizedOutput,
28}
29
30/// Normalized stdout/stderr.
31///
32/// # Normalization
33///
34/// See https://github.com/rust-lang/rust/blob/b224fc84e3/src/test/COMPILER_TESTS.md#normalization
35///
36/// - The `CARGO_MANIFEST_DIR` directory is replaced with `$DIR`.
37/// - All backslashes (\) within same line as `$DIR` are converted to forward
38///   slashes (/) (for Windows) - All CR LF newlines are converted to LF
39///
40/// - `normalize-stdout` is not implemented (yet?).
41#[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    /// If output differs, prints actual stdout/stderr to
129    /// `CARGO_MANIFEST_DIR/target/swc-test-results/ui/$rel_path` where
130    /// `$rel_path`: `path.strip_prefix(CARGO_MANIFEST_DIR)`
131    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                    // If xxx.stderr file does not exist, stderr should be empty.
154                    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        // Actually unreachable.
190        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    /// Expects **`result`** to be `None` and **`errors`** to be match content
216    /// of `${path}.stderr`.
217    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}