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#[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 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
107pub 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
126pub 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 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 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 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 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 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#[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
306pub 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 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});