use indexmap::IndexMap; use oxc_tasks_common::{normalize_path, project_root}; use std::{ fs::{self, File}, io::Write, path::{Path, PathBuf}, process::Command, }; use test_case::TestCaseKind; use walkdir::WalkDir; mod test_case; #[test] #[cfg(any(coverage, coverage_nightly))] fn test() { TestRunner::new(TestRunnerOptions::default()).run(); } #[derive(Default, Clone)] pub struct TestRunnerOptions { pub filter: Option, pub exec: bool, } /// The test runner which walks the babel repository and searches for transformation tests. pub struct TestRunner { options: TestRunnerOptions, } fn root() -> PathBuf { project_root().join("tasks/coverage/babel/packages") } fn snap_root() -> PathBuf { project_root().join("tasks/transform_conformance") } fn fixture_root() -> PathBuf { snap_root().join("fixtures") } const CASES: &[&str] = &[ // ES2024 "babel-plugin-transform-unicode-sets-regex", // ES2022 "babel-plugin-transform-class-properties", "babel-plugin-transform-class-static-block", "babel-plugin-transform-private-methods", "babel-plugin-transform-private-property-in-object", // [Syntax] "babel-plugin-transform-syntax-top-level-await", // ES2021 "babel-plugin-transform-logical-assignment-operators", "babel-plugin-transform-numeric-separator", // ES2020 "babel-plugin-transform-export-namespace-from", "babel-plugin-transform-dynamic-import", "babel-plugin-transform-nullish-coalescing-operator", "babel-plugin-transform-optional-chaining", // [Syntax] "babel-plugin-transform-syntax-bigint", // [Syntax] "babel-plugin-transform-syntax-dynamic-import", // [Syntax] "babel-plugin-transform-syntax-import-meta", // ES2019 "babel-plugin-transform-optional-catch-binding", "babel-plugin-transform-json-strings", // ES2018 "babel-plugin-transform-async-generator-functions", "babel-plugin-transform-object-rest-spread", // [Regex] "babel-plugin-transform-unicode-property-regex", "babel-plugin-transform-dotall-regex", // [Regex] "babel-plugin-transform-named-capturing-groups-regex", // ES2017 "babel-plugin-transform-async-to-generator", // ES2016 "babel-plugin-transform-exponentiation-operator", // ES2015 "babel-plugin-transform-arrow-functions", "babel-plugin-transform-function-name", "babel-plugin-transform-shorthand-properties", "babel-plugin-transform-sticky-regex", "babel-plugin-transform-unicode-regex", "babel-plugin-transform-template-literals", "babel-plugin-transform-duplicate-keys", "babel-plugin-transform-instanceof", "babel-plugin-transform-new-target", // ES3 "babel-plugin-transform-property-literals", // TypeScript "babel-plugin-transform-typescript", // React "babel-plugin-transform-react-jsx", // Proposal "babel-plugin-proposal-decorators", ]; const EXCLUDE_TESTS: &[&str] = &["babel-plugin-transform-typescript/test/fixtures/enum"]; const CONFORMANCE_SNAPSHOT: &str = "babel.snap.md"; const EXEC_SNAPSHOT: &str = "babel_exec.snap.md"; struct SnapshotOption { paths: IndexMap>, dest: PathBuf, } impl SnapshotOption { fn new(paths: IndexMap>, file_name: &'static str) -> Self { Self { paths, dest: snap_root().join(file_name) } } } impl TestRunner { pub fn new(options: TestRunnerOptions) -> Self { Self { options } } /// # Panics pub fn run(self) { let root = root(); let (transform_paths, exec_files) = Self::glob_files(&root, self.options.filter.as_ref()); self.generate_snapshot(SnapshotOption::new(transform_paths, CONFORMANCE_SNAPSHOT)); if self.options.exec { let fixture_root = fixture_root(); if !fixture_root.exists() { fs::create_dir(&fixture_root).unwrap(); } self.generate_snapshot(SnapshotOption::new(exec_files, EXEC_SNAPSHOT)); let _ = fs::remove_dir_all(fixture_root); } } fn glob_files( root: &Path, filter: Option<&String>, ) -> (IndexMap>, IndexMap>) { // use `IndexMap` to keep the order of the test cases the same in insert order. let mut transform_files = IndexMap::>::new(); let mut exec_files = IndexMap::>::new(); for case in CASES { let root = root.join(case).join("test/fixtures"); let (mut transform_paths, mut exec_paths): (Vec, Vec) = WalkDir::new(root) .into_iter() .filter_map(Result::ok) .filter_map(|e| { let path = e.path(); if let Some(filter) = filter { if !path.to_string_lossy().contains(filter) { return None; } } if EXCLUDE_TESTS.iter().any(|p| path.to_string_lossy().contains(p)) { return None; } let test_case = TestCaseKind::from_path(path); if let Some(test_case) = test_case { if test_case.skip_test_case() { return None; } return Some(test_case); } None }) .partition(|p| matches!(p, TestCaseKind::Transform(_))); transform_paths.sort_unstable_by(|a, b| a.path().cmp(b.path())); exec_paths.sort_unstable_by(|a, b| a.path().cmp(b.path())); transform_files.insert((*case).to_string(), transform_paths); exec_files.insert((*case).to_string(), exec_paths); } (transform_files, exec_files) } fn generate_snapshot(&self, option: SnapshotOption) { let SnapshotOption { paths, dest } = option; let mut snapshot = String::new(); let mut total = 0; let mut all_passed = vec![]; let mut all_passed_count = 0; for (case, test_cases) in paths { // Skip empty test cases, e.g. some cases do not have `exec.js` file. if test_cases.is_empty() { continue; } let case_root = root().join(&case).join("test/fixtures"); let num_of_tests = test_cases.len(); total += num_of_tests; // Run the test let (passed, failed): (Vec, Vec) = test_cases .into_iter() .partition(|test_case| test_case.test(self.options.filter.is_some())); all_passed_count += passed.len(); // Snapshot if failed.is_empty() { all_passed.push(case); } else { snapshot.push_str("# "); snapshot.push_str(&case); snapshot.push_str(&format!(" ({}/{})\n", passed.len(), num_of_tests)); for test_case in failed { snapshot.push_str("* "); snapshot.push_str(&normalize_path( test_case.path().strip_prefix(&case_root).unwrap(), )); snapshot.push('\n'); } snapshot.push('\n'); } } if self.options.filter.is_none() { let all_passed = all_passed.into_iter().map(|s| format!("* {s}")).collect::>().join("\n"); let snapshot = format!( "Passed: {all_passed_count}/{total}\n\n# All Passed:\n{all_passed}\n\n\n{snapshot}" ); let mut file = File::create(dest).unwrap(); file.write_all(snapshot.as_bytes()).unwrap(); } } } struct TestRunnerEnv; impl TestRunnerEnv { fn template(code: &str) -> String { format!( r#" import {{expect, test}} from 'bun:test'; test("exec", () => {{ {code} }}) "# ) } fn get_test_result(path: &Path) -> String { let output = Command::new("bun") .current_dir(path.parent().unwrap()) .args(["test", path.file_name().unwrap().to_string_lossy().as_ref()]) .output() .expect("Try install bun: https://bun.sh/docs/installation"); let content = if output.stderr.is_empty() { &output.stdout } else { &output.stderr }; String::from_utf8_lossy(content).to_string() } fn run_test(path: &Path) -> bool { Self::get_test_result(path).contains("1 pass") } }