testing/
lib.rs

1use std::{
2    env,
3    fmt::{self, Debug, Display, Formatter},
4    fs::{create_dir_all, rename, File},
5    io::Write,
6    path::{Component, Path, PathBuf},
7    process::Command,
8    str::FromStr,
9    sync::RwLock,
10    thread,
11};
12
13use difference::Changeset;
14use once_cell::sync::Lazy;
15pub use pretty_assertions::{assert_eq, assert_ne};
16use regex::Regex;
17use rustc_hash::FxHashMap;
18use swc_common::{
19    errors::{Diagnostic, Handler, HANDLER},
20    sync::Lrc,
21    FilePathMapping, SourceMap,
22};
23pub use testing_macros::fixture;
24use tracing_subscriber::EnvFilter;
25
26pub use self::output::{NormalizedOutput, StdErr, StdOut, TestOutput};
27
28mod errors;
29pub mod json;
30#[macro_use]
31mod macros;
32mod diag_errors;
33mod output;
34mod paths;
35mod string_errors;
36
37/// Configures logger
38#[must_use]
39pub fn init() -> tracing::subscriber::DefaultGuard {
40    let log_env = env::var("RUST_LOG").unwrap_or_else(|_| "debug".to_string());
41
42    let logger = tracing_subscriber::FmtSubscriber::builder()
43        .without_time()
44        .with_target(false)
45        .with_ansi(true)
46        .with_env_filter(EnvFilter::from_str(&log_env).unwrap())
47        .with_test_writer()
48        .pretty()
49        .finish();
50
51    tracing::subscriber::set_default(logger)
52}
53
54pub fn find_executable(name: &str) -> Option<PathBuf> {
55    static CACHE: Lazy<RwLock<FxHashMap<String, PathBuf>>> = Lazy::new(Default::default);
56
57    {
58        let locked = CACHE.read().unwrap();
59        if let Some(cached) = locked.get(name) {
60            return Some(cached.clone());
61        }
62    }
63
64    let mut path = env::var_os("PATH").and_then(|paths| {
65        env::split_paths(&paths)
66            .filter_map(|dir| {
67                let full_path = dir.join(name);
68                if full_path.is_file() {
69                    Some(full_path)
70                } else {
71                    None
72                }
73            })
74            .next()
75    });
76
77    if path.is_none() {
78        // Run yarn bin $name
79
80        path = Command::new("yarn")
81            .arg("bin")
82            .arg(name)
83            .output()
84            .ok()
85            .and_then(|output| {
86                if output.status.success() {
87                    let path = String::from_utf8(output.stdout).ok()?;
88                    let path = path.trim();
89                    let path = PathBuf::from(path);
90                    if path.is_file() {
91                        return Some(path);
92                    }
93                }
94
95                None
96            });
97    }
98
99    if let Some(path) = path.clone() {
100        let mut locked = CACHE.write().unwrap();
101        locked.insert(name.to_string(), path);
102    }
103
104    path
105}
106
107/// Run test and print errors.
108pub fn run_test<F, Ret>(treat_err_as_bug: bool, op: F) -> Result<Ret, StdErr>
109where
110    F: FnOnce(Lrc<SourceMap>, &Handler) -> Result<Ret, ()>,
111{
112    let _log = init();
113
114    let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
115    let (handler, errors) = self::string_errors::new_handler(cm.clone(), treat_err_as_bug);
116    let result = swc_common::GLOBALS.set(&swc_common::Globals::new(), || {
117        HANDLER.set(&handler, || op(cm, &handler))
118    });
119
120    match result {
121        Ok(res) => Ok(res),
122        Err(()) => Err(errors.into()),
123    }
124}
125
126/// Run test and print errors.
127pub fn run_test2<F, Ret>(treat_err_as_bug: bool, op: F) -> Result<Ret, StdErr>
128where
129    F: FnOnce(Lrc<SourceMap>, Handler) -> Result<Ret, ()>,
130{
131    let _log = init();
132
133    let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
134    let (handler, errors) = self::string_errors::new_handler(cm.clone(), treat_err_as_bug);
135    let result = swc_common::GLOBALS.set(&swc_common::Globals::new(), || op(cm, handler));
136
137    match result {
138        Ok(res) => Ok(res),
139        Err(()) => Err(errors.into()),
140    }
141}
142
143pub struct Tester {
144    pub cm: Lrc<SourceMap>,
145    pub globals: swc_common::Globals,
146    treat_err_as_bug: bool,
147}
148
149impl Tester {
150    #[allow(clippy::new_without_default)]
151    pub fn new() -> Self {
152        Tester {
153            cm: Lrc::new(SourceMap::new(FilePathMapping::empty())),
154            globals: swc_common::Globals::new(),
155            treat_err_as_bug: false,
156        }
157    }
158
159    pub fn no_error(mut self) -> Self {
160        self.treat_err_as_bug = true;
161        self
162    }
163
164    /// Run test and print errors.
165    pub fn print_errors<F, Ret>(&self, op: F) -> Result<Ret, StdErr>
166    where
167        F: FnOnce(Lrc<SourceMap>, Handler) -> Result<Ret, ()>,
168    {
169        let _log = init();
170
171        let (handler, errors) =
172            self::string_errors::new_handler(self.cm.clone(), self.treat_err_as_bug);
173        let result = swc_common::GLOBALS.set(&self.globals, || op(self.cm.clone(), handler));
174
175        match result {
176            Ok(res) => Ok(res),
177            Err(()) => Err(errors.into()),
178        }
179    }
180
181    /// Run test and collect errors.
182    pub fn errors<F, Ret>(&self, op: F) -> Result<Ret, Vec<Diagnostic>>
183    where
184        F: FnOnce(Lrc<SourceMap>, Handler) -> Result<Ret, ()>,
185    {
186        let _log = init();
187
188        let (handler, errors) =
189            self::diag_errors::new_handler(self.cm.clone(), self.treat_err_as_bug);
190        let result = swc_common::GLOBALS.set(&self.globals, || op(self.cm.clone(), handler));
191
192        let mut errs: Vec<_> = errors.into();
193        errs.sort_by_key(|d| {
194            let span = d.span.primary_span().unwrap();
195            let cp = self.cm.lookup_char_pos(span.lo());
196
197            let line = cp.line;
198            let column = cp.col.0 + 1;
199
200            line * 10000 + column
201        });
202
203        match result {
204            Ok(res) => Ok(res),
205            Err(()) => Err(errs),
206        }
207    }
208}
209
210fn write_to_file(path: &Path, content: &str) {
211    File::create(path)
212        .unwrap_or_else(|err| {
213            panic!(
214                "failed to create file ({}) for writing data of the failed assertion: {}",
215                path.display(),
216                err
217            )
218        })
219        .write_all(content.as_bytes())
220        .expect("failed to write data of the failed assertion")
221}
222
223pub fn print_left_right(left: &dyn Debug, right: &dyn Debug) -> String {
224    fn print(t: &dyn Debug) -> String {
225        let s = format!("{t:#?}");
226
227        // Replace 'Span { lo: BytePos(0), hi: BytePos(0), ctxt: #0 }' with '_'
228        let s = {
229            static RE: Lazy<Regex> =
230                Lazy::new(|| Regex::new("Span \\{[\\a-zA-Z0#:\\(\\)]*\\}").unwrap());
231
232            &RE
233        }
234        .replace_all(&s, "_");
235        // Remove 'span: _,'
236        let s = {
237            static RE: Lazy<Regex> = Lazy::new(|| Regex::new("span: _[,]?\\s*").unwrap());
238
239            &RE
240        }
241        .replace_all(&s, "");
242
243        s.into()
244    }
245
246    let (left, right) = (print(left), print(right));
247
248    let cur = thread::current();
249    let test_name = cur
250        .name()
251        .expect("rustc sets test name as the name of thread");
252
253    // ./target/debug/tests/${test_name}/
254    let target_dir = {
255        let mut buf = paths::test_results_dir().to_path_buf();
256        for m in test_name.split("::") {
257            buf.push(m)
258        }
259
260        create_dir_all(&buf).unwrap_or_else(|err| {
261            panic!(
262                "failed to create directory ({}) for writing data of the failed assertion: {}",
263                buf.display(),
264                err
265            )
266        });
267
268        buf
269    };
270
271    write_to_file(&target_dir.join("left"), &left);
272    write_to_file(&target_dir.join("right"), &right);
273
274    format!("----- {test_name}\n    left:\n{left}\n    right:\n{right}")
275}
276
277#[macro_export]
278macro_rules! assert_eq_ignore_span {
279    ($l:expr, $r:expr) => {{
280        println!("{}", module_path!());
281        let (l, r) = ($crate::drop_span($l), $crate::drop_span($r));
282        if l != r {
283            panic!("assertion failed\n{}", $crate::print_left_right(&l, &r));
284        }
285    }};
286}
287
288pub fn diff(l: &str, r: &str) -> String {
289    let cs = Changeset::new(l, r, "\n");
290
291    format!("{cs}")
292}
293
294/// Used for assertions.
295///
296/// Prints string without escaping special characters on failure.
297#[derive(PartialEq, Eq)]
298pub struct DebugUsingDisplay<'a>(pub &'a str);
299
300impl Debug for DebugUsingDisplay<'_> {
301    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
302        Display::fmt(self.0, f)
303    }
304}
305
306/// Rename `foo/.bar/exec.js` => `foo/bar/exec.js`
307pub fn unignore_fixture(fixture_path: &Path) {
308    if fixture_path.components().all(|c| {
309        !matches!(c, Component::Normal(..)) || !c.as_os_str().to_string_lossy().starts_with('.')
310    }) {
311        return;
312    }
313    //
314
315    let mut new_path = PathBuf::new();
316
317    for c in fixture_path.components() {
318        if let Component::Normal(s) = c {
319            if let Some(s) = s.to_string_lossy().strip_prefix('.') {
320                new_path.push(s);
321
322                continue;
323            }
324        }
325        new_path.push(c);
326    }
327
328    create_dir_all(new_path.parent().unwrap()).expect("failed to create parent dir");
329
330    rename(fixture_path, &new_path).expect("failed to rename");
331}
332
333pub static CARGO_TARGET_DIR: Lazy<PathBuf> = Lazy::new(|| {
334    cargo_metadata::MetadataCommand::new()
335        .no_deps()
336        .exec()
337        .unwrap()
338        .target_directory
339        .into()
340});
341
342pub static CARGO_WORKSPACE_ROOT: Lazy<PathBuf> = Lazy::new(|| {
343    cargo_metadata::MetadataCommand::new()
344        .no_deps()
345        .exec()
346        .unwrap()
347        .workspace_root
348        .into()
349});