#![allow(clippy::arc_with_non_send_sync)] use oxide_compiler::compiler::Compiler; use oxide_kernel::kernel::{KernelConfig, KernelCore}; use oxide_types::value::JsValue; use oxide_vm::vm::Vm; use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, OnceLock, RwLock}; use std::time::{Duration, Instant}; use walkdir::WalkDir; mod test262_log; use oxide_log::{Level, LogConfig, Output, SUBSYSTEM_COUNT}; // 记录当前正在执行的测试路径(thread-local);每个测试执行前写入, // panic hook 据此定位崩溃所在的测试文件。 std::thread_local! { static CURRENT_TEST_PATH: std::cell::RefCell = const { std::cell::RefCell::new(String::new()) }; } /// 测试头部 YAML 元数据中的 `negative` 段:声明期望的失败阶段与错误类型。 #[derive(Debug, Deserialize)] #[allow(dead_code)] struct Negative { phase: String, #[serde(rename = "type")] error_type: String, } /// test262 测试文件头部 `/*--- ... ---*/` 段解析出的元数据 /// (description / flags / includes / features / negative 等)。 #[derive(Debug, Deserialize)] #[allow(dead_code)] struct TestMeta { #[serde(default)] description: String, #[serde(default)] flags: Vec, #[serde(default)] includes: Vec, #[serde(default)] features: Vec, #[serde(default)] negative: Option, #[serde(default)] es5id: String, #[serde(default)] es6id: String, #[serde(default)] esid: String, } /// 单个测试的判定结果:通过 / 失败 / 跳过(各带说明消息)。 #[derive(Debug, PartialEq)] #[allow(dead_code)] enum TestOutcome { Pass(String), Fail(String), Skip(String), } /// 单个测试的运行结果:路径、判定与耗时(毫秒)。 #[derive(Debug)] #[allow(dead_code)] struct TestResult { path: PathBuf, outcome: TestOutcome, duration_ms: u64, } impl TestResult { /// 构造一个通过结果。 fn pass(path: PathBuf, dur: u64, msg: impl Into) -> Self { Self { path, outcome: TestOutcome::Pass(msg.into()), duration_ms: dur, } } /// 构造一个失败结果。 fn fail(path: PathBuf, dur: u64, msg: impl Into) -> Self { Self { path, outcome: TestOutcome::Fail(msg.into()), duration_ms: dur, } } /// 构造一个跳过结果(不计耗时)。 fn skip(path: PathBuf, msg: String) -> Self { Self { path, outcome: TestOutcome::Skip(msg), duration_ms: 0, } } } /// 从测试源码头部解析 `/*--- YAML ---*/` 元数据;无该头部返回 None。 fn parse_meta(source: &str) -> Option { let header_start = source.find("/*---")?; let header = &source[header_start..]; let header = header.strip_prefix("/*---")?; let end = header.find("---*/")?; let yaml_body = &header[..end]; let yaml_body = yaml_body.trim(); serde_yaml::from_str::(yaml_body).ok() } /// 剥离测试源码头部的 YAML 元数据段,返回纯 JS 代码。 fn strip_meta(source: &str) -> &str { if let Some(pos) = source.find("---*/") { return source[pos + 5..].trim_start(); } source } /// 全部已运行测试的累计统计:通过/失败/跳过计数、总耗时与失败原因分类。 #[derive(Default)] struct RunStats { pass: usize, fail: usize, skip: usize, total_ms: u64, fail_categories: HashMap, } impl RunStats { /// 把另一个 worker 的部分统计并入本对象。用于并行执行后把各 worker 的 /// 结果合并回单一总计。 fn merge(&mut self, other: RunStats) { self.pass += other.pass; self.fail += other.fail; self.skip += other.skip; self.total_ms += other.total_ms; for (cat, count) in other.fail_categories { *self.fail_categories.entry(cat).or_insert(0) += count; } } /// 把单个测试结果记入运行累计。 fn record(&mut self, result: &TestResult) { match &result.outcome { TestOutcome::Pass(_) => self.pass += 1, TestOutcome::Fail(msg) => { let cat = categorize_fail(msg); *self.fail_categories.entry(cat).or_insert(0) += 1; self.fail += 1; } TestOutcome::Skip(_) => self.skip += 1, } self.total_ms += result.duration_ms; } } /// 运行配置:test262 根目录、路径过滤器及各选项开关。 #[derive(Debug, Default)] struct RunConfig { test262_root: Option, filter: Option, no_skip: bool, supervise: bool, leak_check: bool, leak_check_interval: usize, /// 关闭 liveness/精确 DCE/RegAlloc 链(on/off 对比基础设施)。 no_regalloc: bool, /// 逐测试打印 PASS/FAIL/SKIP(on/off 结果集合对比用)。 verbose: bool, } /// 内嵌的 test262 harness 辅助脚本注册表(编译期 include_str! 打包)。 struct HarnessSources { sources: HashMap<&'static str, &'static str>, } /// harness 前缀缓存键:由测试 `includes` 列表唯一确定。 type HarnessPrefixCache = HashMap, String>; impl HarnessSources { /// 构建 harness 源注册表(键为文件名,值为编译期内嵌源码)。 fn new() -> Self { let mut sources = HashMap::new(); sources.insert("sta.js", include_str!("../../../tests/test262/harness/sta.js")); sources.insert("assert.js", include_str!("../../../tests/test262/harness/assert.js")); sources.insert("propertyHelper.js", include_str!("../../../tests/test262/harness/propertyHelper.js")); sources.insert("compareArray.js", include_str!("../../../tests/test262/harness/compareArray.js")); sources.insert("fnGlobalObject.js", include_str!("../../../tests/test262/harness/fnGlobalObject.js")); sources.insert("nans.js", include_str!("../../../tests/test262/harness/nans.js")); sources.insert("dateConstants.js", include_str!("../../../tests/test262/harness/dateConstants.js")); sources.insert( "decimalToHexString.js", include_str!("../../../tests/test262/harness/decimalToHexString.js"), ); sources.insert("isConstructor.js", include_str!("../../../tests/test262/harness/isConstructor.js")); sources.insert("nativeErrors.js", include_str!("../../../tests/test262/harness/nativeErrors.js")); sources.insert( "nativeFunctionMatcher.js", include_str!("../../../tests/test262/harness/nativeFunctionMatcher.js"), ); sources.insert("regExpUtils.js", include_str!("../../../tests/test262/harness/regExpUtils.js")); sources.insert( "assertRelativeDateMs.js", include_str!("../../../tests/test262/harness/assertRelativeDateMs.js"), ); sources.insert( "wellKnownIntrinsicObjects.js", include_str!("../../../tests/test262/harness/wellKnownIntrinsicObjects.js"), ); sources.insert("typeCoercion.js", include_str!("../../../tests/test262/harness/typeCoercion.js")); sources.insert("deepEqual.js", include_str!("../../../tests/test262/harness/deepEqual.js")); sources.insert("testTypedArray.js", include_str!("../../../tests/test262/harness/testTypedArray.js")); sources.insert("temporalHelpers.js", include_str!("../../../tests/test262/harness/temporalHelpers.js")); sources.insert("asyncHelpers.js", include_str!("../../../tests/test262/harness/asyncHelpers.js")); sources.insert("doneprintHandle.js", include_str!("../../../tests/test262/harness/doneprintHandle.js")); sources.insert("promiseHelper.js", include_str!("../../../tests/test262/harness/promiseHelper.js")); Self { sources } } /// 按文件名取 harness 源码。 fn get(&self, name: &str) -> Option<&'static str> { self.sources.get(name).copied() } } static HARNESS: OnceLock = OnceLock::new(); /// 判断 harness 文件是否在支持范围之外(依赖 Proxy/Intl/async 等未实现特性)。 fn is_blacklisted_harness(name: &str) -> bool { matches!( name, "testIntl.js" | "testAtomics.js" | "atomicsHelper.js" | "proxyTrapsHelper.js" | "tcoHelper.js" | "detachArrayBuffer.js" | "resizableArrayBufferUtils.js" | "byteConversionValues.js" | "compareIterator.js" | "iteratorZipUtils.js" ) } /// 生成 `Test262Error` 的 JS prelude(供测试脚本 `assert` 失败时抛出)。 /// 向拼接源码追加一段带注释标记的 harness 代码块。 fn append_source_chunk(out: &mut String, name: &str, source: &str) { out.push_str("\n// ---- test262 harness: "); out.push_str(name); out.push_str(" ----\n"); out.push_str(source); out.push('\n'); } /// 取测试元数据的 harness 缓存键(即其 `includes` 列表)。 fn harness_key(meta: &TestMeta) -> Vec { let mut key = meta.includes.clone(); if meta.flags.iter().any(|f| f == "onlyStrict") { key.push("__onlyStrict__".into()); } key } /// 按测试元数据拼接完整的 harness 前缀源码(prelude + sta/assert + 各 include)。 fn build_harness_source(meta: &TestMeta, harness: &HarnessSources) -> Result { let mut source = String::new(); // onlyStrict 测试在严格模式下运行:指令必须位于脚本最前(harness 之前)。 if meta.flags.iter().any(|f| f == "onlyStrict") { append_source_chunk(&mut source, "use strict", "\"use strict\";"); } // Test262Error 由 sta.js 提供;此处不再重复定义(重复函数声明会引发 // 引擎 prototype 语义错乱,导致 assert.throws 的 constructor 比对失败)。 append_source_chunk( &mut source, "sta.js", harness.get("sta.js").ok_or_else(|| String::from("unknown harness: sta.js"))?, ); append_source_chunk( &mut source, "assert.js", harness .get("assert.js") .ok_or_else(|| String::from("unknown harness: assert.js"))?, ); for include in &meta.includes { if is_blacklisted_harness(include) { return Err(format!("out-of-scope harness: {include}")); } let include_source = harness.get(include).ok_or_else(|| format!("unknown harness: {include}"))?; append_source_chunk(&mut source, include, include_source); } Ok(source) } /// 取得(并缓存)测试所需的 harness 前缀源码;缓存键为 includes 组合。 fn get_harness_prefix( meta: &TestMeta, harness: &HarnessSources, cache: &Arc>, ) -> Result { let key = harness_key(meta); { let guard = cache.read().unwrap(); if let Some(prefix) = guard.get(&key) { return Ok(prefix.clone()); } } let source = build_harness_source(meta, harness)?; cache.write().unwrap().insert(key, source.clone()); Ok(source) } impl RunConfig { /// 默认运行配置。 fn new() -> Self { Self { test262_root: None, filter: None, no_skip: false, supervise: false, leak_check: false, leak_check_interval: 1000, no_regalloc: false, verbose: false, } } /// 解析命令行参数为运行配置;未知选项或参数过多返回错误。 fn parse(args: &[String]) -> Result { let mut config = Self::new(); let mut positional = Vec::new(); for arg in args.iter().skip(1) { match arg.as_str() { "--no-skip" => config.no_skip = true, "--no-regalloc" => config.no_regalloc = true, "--verbose" => config.verbose = true, "--supervise" => config.supervise = true, "--leak-check" => config.leak_check = true, "--help" | "-h" => return Err(Self::usage()), _ if arg.starts_with("--leak-check-interval=") => { config.leak_check_interval = arg.strip_prefix("--leak-check-interval=").unwrap().parse().unwrap_or(1000); } _ if arg.starts_with("--") => return Err(format!("unknown option: {arg}\n\n{}", Self::usage())), _ => positional.push(arg.clone()), } } if let Some(root) = positional.first() { config.test262_root = Some(PathBuf::from(root)); } if let Some(filter) = positional.get(1) { config.filter = Some(filter.clone()); } if positional.len() > 2 { return Err(format!("too many positional arguments\n\n{}", Self::usage())); } Ok(config) } /// 打印用法说明。 fn usage() -> String { "usage: test262-runner [--no-skip] [--no-regalloc] [--verbose] [--supervise] [--leak-check] [--leak-check-interval=N] [test262-root] [path-filter]\n\ \n\ --no-skip Run capability-excluded tests and count unsupported compile/runtime results as failures.\n\ --no-regalloc Disable the liveness/precise-DCE/RegAlloc compiler chain (vregs stay as physical numbers).\n\ --verbose Print one PASS/FAIL/SKIP line per test (for on/off result-set comparison).\n\ --supervise Run the suite as single-worker child-process windows with a hard per-test timeout and\n\ \x20 automatic resume past any hanging/crashing test. A hang or crash is reported by path.\n\ --leak-check Monitor session_object_ptrs, session_bytes, code_forge.len(), symbol_registry.len() every\n\ \x20 --leak-check-interval tests (default 1000). Flags sustained linear growth (R^2>0.9).\n\ \n\ supervised-mode env tunables:\n\ \x20 OXIDE_TEST262_TIMEOUT_SECS per-test wall-clock timeout (default 10)\n\ \x20 OXIDE_TEST262_WINDOW tests per window (default 5000)\n\ \x20 OXIDE_TEST262_SUPERVISORS concurrent windows (default = available parallelism)\n\ \x20 OXIDE_TEST262_STARTUP_GRACE_SECS grace for a child's first heartbeat (default 60)" .into() } } /// 按测试元数据的 flags/features 判断是否应跳过,返回跳过原因(None 表示不跳过)。 fn is_skipped(meta: &TestMeta) -> Option { for flag in &meta.flags { if flag.as_str() == "raw" { return Some("raw tests excluded".into()); } // noStrict 测试放行——很多在严格模式下仍可通过;运行时跳过逻辑会捕获失败。 } // 保持大范围已实现 feature tag 可运行;只排除真正未实现的子特性。 // 其余一切让测试实际运行,依赖运行时跳过逻辑 // ("too many registers"、"not yet implemented" 等)判定失败。 let excluded_features = [ "Proxy", "Intl", "Atomics", "SharedArrayBuffer", "cross-realm", // await-dictionary(Promise.allKeyed/allSettledKeyed)是 2025 proposal,未实现。 "await-dictionary", ]; for feat in &meta.features { if excluded_features.contains(&feat.as_str()) || feat.starts_with("Intl") { return Some(format!("excluded feature: {feat}")); } } None } /// 在 catch_unwind 保护下运行单个测试,把引擎 panic 记为失败。 #[expect(clippy::too_many_arguments)] fn run_test( path: &Path, source: &str, meta: &TestMeta, kernel: &Arc, harness: &HarnessSources, harness_cache: &Arc>, no_skip: bool, no_regalloc: bool, ) -> TestResult { let start = std::time::Instant::now(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { run_test_inner(path, source, meta, kernel, harness, harness_cache, no_skip, no_regalloc) })); match result { Ok(r) => r, Err(_panic) => { let dur = start.elapsed().as_millis() as u64; TestResult::fail(path.to_path_buf(), dur, "engine panic (unsupported feature)") } } } /// test262 模块用例的依赖加载器:以测试文件父目录为基准解析相对导入, /// 读取磁盘上的 fixture 源码;json/text/bytes 经 import attributes 判定。 struct Test262ModuleLoader; impl oxide_emit::module::ModuleSourceLoader for Test262ModuleLoader { fn resolve( &mut self, base_dir: &str, specifier: &str, attributes: &[(&str, &str)], ) -> Result { use oxide_emit::module::{ModuleKind, ResolvedModule}; let kind = attributes .iter() .find(|(k, _)| *k == "type") .map(|(_, v)| match *v { "json" => ModuleKind::Json, "text" => ModuleKind::Text, "bytes" => ModuleKind::Bytes, _ => ModuleKind::Js, }) .unwrap_or(ModuleKind::Js); let base = Path::new(base_dir); let full = if specifier.starts_with('/') { PathBuf::from(specifier.trim_start_matches('/')) } else { base.join(specifier) }; let canonical = full .canonicalize() .map_err(|e| format!("cannot resolve module {specifier}: {e}"))?; let content = std::fs::read_to_string(&canonical) .map_err(|e| format!("cannot read module {specifier}: {e}"))?; Ok(ResolvedModule { source: content, path: canonical.to_string_lossy().to_string(), kind, }) } } /// 单测执行主流程:拼 harness 前缀 → parse → compile → run; /// 依据 `negative` 元数据校验期望错误,未实现特性按 no_skip 选择跳过或失败。 #[expect(clippy::too_many_arguments)] fn run_test_inner( path: &Path, source: &str, meta: &TestMeta, kernel: &Arc, harness: &HarnessSources, harness_cache: &Arc>, no_skip: bool, no_regalloc: bool, ) -> TestResult { let start = std::time::Instant::now(); let is_async = meta.flags.iter().any(|f| f == "async"); let is_module = meta.flags.iter().any(|f| f == "module"); let code = match get_harness_prefix(meta, harness, harness_cache) { Ok(prefix) => { let mut code = prefix; if is_async { // 异步测试:注入 print 捕获 + $DONE(asyncTests 用 $DONE 报告结果)。 // 结果写入 globalThis 全局字符串,run() 结束后由 runner 读取并按 marker 判定。 // print 为顶层函数声明,经全局对象反射落 globalThis;此处仅初始化累加器。 append_source_chunk( &mut code, "async capture", "globalThis.$__test262_async_result = \"\";\n\ function print(msg) { globalThis.$__test262_async_result += String(msg) + \"\\n\"; }", ); if let Some(done_src) = harness.get("doneprintHandle.js") { append_source_chunk(&mut code, "doneprintHandle.js", done_src); } } append_source_chunk(&mut code, "test source", strip_meta(source)); code } Err(e) => { let dur = start.elapsed().as_millis() as u64; if no_skip { return TestResult::fail(path.to_path_buf(), dur, e); } return TestResult::skip(path.to_path_buf(), e); } }; let alloc = oxide_parser::Allocator::default(); let program = match if is_module { oxide_parser::parse_module(&alloc, &code) } else { oxide_parser::parse(&alloc, &code) } { Ok(p) => p, Err(errs) => { let dur = start.elapsed().as_millis() as u64; let msg = format!("parse error: {}", errs[0].message); if meta.negative.is_some() { return TestResult::pass(path.to_path_buf(), dur, msg); } return TestResult::fail(path.to_path_buf(), dur, msg); } }; let compiler = if no_regalloc { Compiler::new().with_regalloc(false) } else { Compiler::new() }; let module = match if is_module { let mut loader = Test262ModuleLoader; compiler.compile_module(&program, path.to_string_lossy().as_ref(), &mut loader) } else { compiler.compile(&program) } { Ok(m) => m, Err(e) => { let dur = start.elapsed().as_millis() as u64; let msg = format!("compile error: {e}"); match classify_compile_error(&msg, meta.negative.is_some(), no_skip) { TestOutcome::Pass(m) => return TestResult::pass(path.to_path_buf(), dur, m), TestOutcome::Fail(m) => return TestResult::fail(path.to_path_buf(), dur, m), TestOutcome::Skip(m) => return TestResult::skip(path.to_path_buf(), m), } } }; let mut vm = Vm::with_kernel_core(Arc::clone(kernel)); let run_result = vm.run(&module); let dur = start.elapsed().as_millis() as u64; if is_async { return judge_async_result(path, run_result, &mut vm, meta, dur, no_skip); } match run_result { Ok(result) => { if let Some(neg) = meta.negative.as_ref() { return TestResult::fail( path.to_path_buf(), dur, format!("expected runtime error ({}), got: {result}", neg.error_type), ); } TestResult::pass(path.to_path_buf(), dur, format!("ok: {result}")) } Err(e) => judge_vm_error(path, &e, meta, dur, no_skip), } } /// 异步测试判定:`$DONE` 把结果写入捕获串,run() 结束时微任务队列已 drain。 /// /// 失败 marker 优先(`Test262:AsyncTestFailure:: `);其次成功 marker /// (`Test262:AsyncTestComplete`);两者都无则测试未在 run() 内同步完成 /// (需 setTimeout 等异步 runner 的测试在此失败),回退到 run 结果判定。 fn judge_async_result( path: &Path, run_result: Result, vm: &mut Vm, meta: &TestMeta, dur: u64, no_skip: bool, ) -> TestResult { let output = read_async_output(vm); if let Some(pos) = output.find("Test262:AsyncTestFailure:") { let detail = output[pos..].trim(); if let Some(neg) = meta.negative.as_ref() { let name = detail .strip_prefix("Test262:AsyncTestFailure:") .and_then(|rest| rest.split(':').next()) .unwrap_or(""); if name == neg.error_type { return TestResult::pass(path.to_path_buf(), dur, format!("expected: {detail}")); } return TestResult::fail( path.to_path_buf(), dur, format!("expected {} error, got: {detail}", neg.error_type), ); } return TestResult::fail(path.to_path_buf(), dur, detail); } if output.contains("Test262:AsyncTestComplete") { if let Some(neg) = meta.negative.as_ref() { return TestResult::fail( path.to_path_buf(), dur, format!( "expected runtime error ({}), got: async complete", neg.error_type ), ); } return TestResult::pass(path.to_path_buf(), dur, "async ok"); } // $DONE 未被调用:run() 结果决定成败(顶层抛错走普通判定,未抛错视为未完成)。 match run_result { Ok(_) => TestResult::fail(path.to_path_buf(), dur, "async test did not call $DONE synchronously"), Err(e) => judge_vm_error(path, &e, meta, dur, no_skip), } } /// 读取异步测试捕获的 `$DONE` 输出字符串。 fn read_async_output(vm: &Vm) -> String { let si = vm.kernel_core().perm_interner().intern("$__test262_async_result").0; let global = vm.session().global_object(); let val = vm .kernel_core() .shape_forge() .lookup_position(global.shape_id(), si) .map(|pos| global.get_prop_at(pos)) .unwrap_or(JsValue::undefined()); vm.lookup_str(val).unwrap_or_default() } /// 运行期错误的判定(negative 匹配优先 + 未实现特性跳过分类)。 fn judge_vm_error(path: &Path, e: &str, meta: &TestMeta, dur: u64, no_skip: bool) -> TestResult { match classify_vm_error(e, meta.negative.as_ref(), no_skip) { TestOutcome::Pass(msg) => TestResult::pass(path.to_path_buf(), dur, msg), TestOutcome::Fail(msg) => TestResult::fail(path.to_path_buf(), dur, msg), TestOutcome::Skip(msg) => TestResult::skip(path.to_path_buf(), msg), } } /// 从 `is not defined` 错误消息中解析未绑定标识符名。 /// /// # 边界与前提 /// - 运行期形态:`uncaught ReferenceError: {name} is not defined` /// - 编译期形态:`Identifier '{name}' is not defined`(可带 `compile error: ` 前缀) /// - 两种形态都不匹配返回 `None`(不属未绑定标识符错误)。 fn parse_undefined_ident(e: &str) -> Option<&str> { let e = e.strip_prefix("compile error: ").unwrap_or(e); let e = e.strip_prefix("uncaught ").unwrap_or(e); if let Some(rest) = e.strip_prefix("ReferenceError: ") { if let Some(ident) = rest.strip_suffix(" is not defined") { let ident = ident.trim(); return (!ident.is_empty()).then_some(ident); } } if let Some(rest) = e.strip_prefix("Identifier '") { if let Some(ident) = rest.strip_suffix("' is not defined") { let ident = ident.trim(); return (!ident.is_empty()).then_some(ident); } } None } /// 已知缺失的宿主/标准全局白名单:这些标识符未绑定是能力缺失(skip), /// 其它 `is not defined` 是引擎回归或语义缺口(fail)。 const KNOWN_MISSING_GLOBALS: &[&str] = &["$262", "structuredClone", "queueMicrotask"]; /// 判定编译期错误结果:compile 错误无条件放行 negative;能力未实现形态 /// (含白名单内的未绑定标识符)→ Skip(`--no-skip` 下为 Fail);其余一律 Fail。 fn classify_compile_error(e: &str, has_negative: bool, no_skip: bool) -> TestOutcome { if has_negative { return TestOutcome::Pass(format!("compile error: {e}")); } let unimplemented = e.contains("not yet implemented") || e.contains("not yet supported") || e.contains("not supported") || e.contains("unsupported") || e.contains("SpreadElement") || e.contains("already been declared") || e.contains("parser panicked") || e.contains("too many registers"); // `is not defined` 仅当标识符在缺失全局白名单内才 skip;其余(引擎该有 // 而未提供的标识符、回归)按真实失败计入,保证修复可见性。 let whitelisted_undefined = e.contains("is not defined") && parse_undefined_ident(e).is_some_and(|i| KNOWN_MISSING_GLOBALS.contains(&i)); if unimplemented || whitelisted_undefined { if no_skip { return TestOutcome::Fail(format!("compile error: {e}")); } return TestOutcome::Skip(format!("compile error: {e}")); } TestOutcome::Fail(format!("compile error: {e}")) } /// 判定运行期错误结果:negative 期望匹配 → Pass;能力未实现形态 → Skip /// (`--no-skip` 下为 Fail);其余一律 Fail。 /// /// # 边界与前提 /// - receiver 校验、栈溢出、不可调用、ToPrimitive 缺口等错误是引擎语义与测试 /// 期望不符的真实失败,不再被 skip 子串吞没(引擎做对了反而计 skip 属误判)。 /// - `is not defined` 是否 skip 由标识符白名单判定(见 [`parse_undefined_ident`])。 fn classify_vm_error(e: &str, neg: Option<&Negative>, no_skip: bool) -> TestOutcome { if let Some(neg) = neg { if e.contains("TypeError") && neg.error_type == "TypeError" { return TestOutcome::Pass(format!("expected: {e}")); } if e.contains("ReferenceError") && neg.error_type == "ReferenceError" { return TestOutcome::Pass(format!("expected: {e}")); } if e.contains("SyntaxError") && neg.error_type == "SyntaxError" { return TestOutcome::Pass(format!("expected: {e}")); } if e.contains(&neg.error_type) { return TestOutcome::Pass(format!("expected: {e}")); } return TestOutcome::Fail(format!("expected {} error, got: {e}", neg.error_type)); } let unimplemented = e.contains("not yet implemented") || e.contains("not yet supported") || e.contains("not supported") || e.contains("unsupported") || e.contains("step limit") || e.contains("NEW_EXPRESSION") || e.contains("IC_GET_PROP on non-object") || e.contains("GET_PROP_DYNAMIC on non-object") || e.contains("SET_PROP_DYNAMIC on non-object") || e.contains("private field brand check") || e.contains("CALL_NATIVE target") || e.contains("is not implemented") || e.contains("unexpected tail call") || e.contains("__proto__ must be an object"); // `is not defined` 不再一票吞 skip:仅在标识符属缺失全局白名单时按能力 // 缺失跳过;否则是真实 ReferenceError(含修复后应转 PASS 的回归)。 let whitelisted_undefined = e.contains("is not defined") && parse_undefined_ident(e).is_some_and(|i| KNOWN_MISSING_GLOBALS.contains(&i)); if unimplemented || whitelisted_undefined { if no_skip { return TestOutcome::Fail(format!("vm error: {e}")); } return TestOutcome::Skip(format!("vm: {e}")); } TestOutcome::Fail(format!("vm error: {e}")) } /// 递归发现 test262 根目录下全部 `.js` 测试文件(排序后返回)。 fn discover_tests(test262_root: &Path) -> Vec { let mut paths: Vec = WalkDir::new(test262_root) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.path().extension().is_some_and(|ext| ext == "js")) .map(|e| e.path().to_path_buf()) .collect(); paths.sort(); paths } /// 从子进程 stdout 中解析形如 `label : N` 的汇总行。 fn parse_summary_count(stdout: &str, label: &str) -> Option { stdout.lines().find_map(|line| { let trimmed = line.trim_start(); let rest = trimmed.strip_prefix(label)?; let value = rest.trim().split(' ').next()?; value.parse::().ok() }) } /// 分块模式:按 chunk_size 把测试区间切块,逐块以子进程执行并汇总结果。 fn run_chunked(args: &[String], skip_until: usize, end_index: usize, chunk_size: usize) -> bool { let exe = match std::env::current_exe() { Ok(path) => path, Err(err) => { eprintln!("failed to resolve current executable for chunked mode: {err}"); return false; } }; let mut aggregate_pass = 0usize; let mut aggregate_fail = 0usize; let mut aggregate_skip = 0usize; let mut chunk_start = skip_until; let mut chunk_id = 1usize; while chunk_start < end_index { let chunk_len = (end_index - chunk_start).min(chunk_size); eprintln!("chunk {chunk_id}: tests [{chunk_start}, {})", chunk_start + chunk_len); let output = match Command::new(&exe) .args(args.iter().skip(1)) .env("OXIDE_SKIP_UNTIL", chunk_start.to_string()) .env("OXIDE_MAX_TESTS", chunk_len.to_string()) .env("OXIDE_TEST262_CHILD_CHUNK", "1") .env("OXIDE_TEST262_ALLOW_FAIL_EXIT", "1") .output() { Ok(output) => output, Err(err) => { eprintln!("failed to run chunk {chunk_id}: {err}"); return false; } }; let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); print!("{stdout}"); eprint!("{stderr}"); if !output.status.success() { eprintln!("chunk {chunk_id} crashed or aborted"); return false; } aggregate_pass += parse_summary_count(&stdout, "pass :").unwrap_or(0); aggregate_fail += parse_summary_count(&stdout, "fail :").unwrap_or(0); aggregate_skip += parse_summary_count(&stdout, "skip :").unwrap_or(0); chunk_start += chunk_len; chunk_id += 1; } let aggregate_total = aggregate_pass + aggregate_fail + aggregate_skip; println!(); println!("═══════════════════════════════════════"); println!(" test262 chunked aggregate"); println!("═══════════════════════════════════════"); println!(" total : {}", aggregate_total); println!(" pass : {}", aggregate_pass); println!(" fail : {}", aggregate_fail); println!(" skip : {}", aggregate_skip); println!("═══════════════════════════════════════"); aggregate_fail == 0 } /// 监督模式下子进程写入、父进程轮询的一条心跳记录。 /// `index` 是子进程即将运行的全局测试下标(`START`)或其完成窗口的 /// 结束下标(`DONE`);计数始终覆盖 `index` *之前* 已完成的测试, /// 因此进行中的测试不会被计入。`categories` 为子进程失败分类计数快照 /// (首行之后按 `类别\t计数` 逐行写出),使父进程在 supervise 下也能按 /// 根因拆分失败。 struct Heartbeat { phase: String, index: usize, pass: usize, fail: usize, skip: usize, categories: HashMap, } /// 用单行心跳头 + 失败类别行覆写心跳文件。错误被忽略:漏写心跳只是把停滞 /// 检测推迟一个轮询间隔。假定单 worker(监督器强制 `OXIDE_TEST262_WORKERS=1`); /// 多 worker 时运行下标有歧义且文件存在竞争。 fn write_heartbeat( path: &Path, phase: &str, index: usize, pass: usize, fail: usize, skip: usize, categories: &HashMap, ) { let mut content = format!("{phase} {index} {pass} {fail} {skip}\n"); for (cat, count) in categories { // 类别文本内的制表符/换行会破坏行格式,写盘前压平。 let cat = cat.replace(['\t', '\n', '\r'], " "); content.push_str(&format!("{cat}\t{count}\n")); } let _ = std::fs::write(path, content); } /// 读取最新心跳(含失败类别行)。任何缺失/残缺/畸形内容均返回 `None`, /// 使轮询循环可直接在下一拍重试。 fn read_heartbeat(path: &Path) -> Option { let content = std::fs::read_to_string(path).ok()?; let mut lines = content.lines(); let line = lines.next()?; let mut parts = line.split_whitespace(); let phase = parts.next()?.to_string(); let index = parts.next()?.parse().ok()?; let pass = parts.next()?.parse().ok()?; let fail = parts.next()?.parse().ok()?; let skip = parts.next()?.parse().ok()?; let mut categories = HashMap::new(); for l in lines { if let Some((cat, count)) = l.split_once('\t') { if let Ok(c) = count.trim().parse() { categories.insert(cat.to_string(), c); } } } Some(Heartbeat { phase, index, pass, fail, skip, categories, }) } /// 把心跳快照并入累计统计(含失败类别)。 fn merge_heartbeat(stats: &mut RunStats, hb: &Heartbeat) { stats.pass += hb.pass; stats.fail += hb.fail; stats.skip += hb.skip; for (cat, count) in &hb.categories { *stats.fail_categories.entry(cat.clone()).or_insert(0) += count; } } /// 记一笔超时/崩溃结果:默认计入 skip,`--no-skip` 下计入 fail(归入 /// `timeout/crash` 类别,父进程无法进一步拆分根因)。 fn record_timeout_or_crash(stats: &mut RunStats, no_skip: bool) { if no_skip { stats.fail += 1; *stats.fail_categories.entry("timeout/crash".into()).or_insert(0) += 1; } else { stats.skip += 1; } } /// 在监督下运行一个窗口 `[wstart, wend)`,返回经过多次子进程重启 /// 聚合的 `RunStats`(含失败类别)。 /// /// 单 worker 子进程运行常规 in-process 路径(预热 kernel + harness 前缀缓存) /// 并在每个测试前发出心跳。若运行下标停滞超过 `timeout`,子进程被杀死、 /// 按路径报告肇事者,并由全新子进程从 `culprit + 1` 续跑。子进程在测试中途 /// 崩溃也经同路径恢复。超时/崩溃默认计为 skip,`--no-skip` 下计为失败。 #[expect(clippy::too_many_arguments)] fn supervise_window( exe: &Path, args: &[String], no_skip: bool, wstart: usize, wend: usize, timeout: Duration, startup_grace: Duration, paths: &[PathBuf], window_id: usize, ) -> RunStats { let mut stats = RunStats::default(); let mut cur = wstart; let hb_path = std::env::temp_dir().join(format!("oxide_t262_hb_{}_{}.txt", std::process::id(), window_id)); let describe = |idx: usize| -> String { paths .get(idx) .map(|p| p.display().to_string()) .unwrap_or_else(|| format!("#{idx}")) }; while cur < wend { let _ = std::fs::remove_file(&hb_path); let max_tests = wend - cur; let mut child = match Command::new(exe) .args(args.iter().skip(1)) .env("OXIDE_SKIP_UNTIL", cur.to_string()) .env("OXIDE_MAX_TESTS", max_tests.to_string()) .env("OXIDE_TEST262_WORKERS", "1") .env("OXIDE_TEST262_HEARTBEAT", &hb_path) .env("OXIDE_TEST262_CHILD_CHUNK", "1") .env("OXIDE_TEST262_ALLOW_FAIL_EXIT", "1") .env_remove("OXIDE_TEST262_CHUNK_SIZE") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn() { Ok(child) => child, Err(err) => { eprintln!(" window {window_id}: failed to spawn child at index {cur}: {err}"); record_timeout_or_crash(&mut stats, no_skip); cur += 1; continue; } }; let spawn_time = Instant::now(); let mut last_index: Option = None; let mut last_change = Instant::now(); loop { match child.try_wait() { Ok(Some(status)) => { match read_heartbeat(&hb_path) { Some(hb) if hb.phase == "DONE" => { merge_heartbeat(&mut stats, &hb); cur = wend; } Some(hb) => { merge_heartbeat(&mut stats, &hb); eprintln!( " [warn] window {window_id}: child exited ({status}) mid-test #{}: {}", hb.index, describe(hb.index) ); record_timeout_or_crash(&mut stats, no_skip); cur = hb.index + 1; } None => { eprintln!( " [warn] window {window_id}: child exited ({status}) with no heartbeat at index {cur}; skipping one" ); record_timeout_or_crash(&mut stats, no_skip); cur += 1; } } break; } Ok(None) => {} Err(err) => { eprintln!(" window {window_id}: try_wait error: {err}"); let _ = child.kill(); let _ = child.wait(); cur += 1; break; } } if let Some(hb) = read_heartbeat(&hb_path) { if Some(hb.index) != last_index { last_index = Some(hb.index); last_change = Instant::now(); } } let (deadline, elapsed) = if last_index.is_some() { (timeout, last_change.elapsed()) } else { (startup_grace, spawn_time.elapsed()) }; if elapsed > deadline { let hb = read_heartbeat(&hb_path); let culprit = hb.as_ref().map(|h| h.index).unwrap_or(cur); if let Some(h) = &hb { merge_heartbeat(&mut stats, h); } eprintln!( " [timeout] window {window_id}: TIMEOUT ({}s) on test #{culprit}: {}", deadline.as_secs(), describe(culprit) ); let _ = child.kill(); let _ = child.wait(); record_timeout_or_crash(&mut stats, no_skip); cur = culprit + 1; break; } std::thread::sleep(Duration::from_millis(200)); } } let _ = std::fs::remove_file(&hb_path); stats } /// 编排一次监督式全量运行:把 `[skip_until, end_index)` 切分为窗口,至多 /// `supervisors` 个窗口并发。监督线程只派生/轮询/杀死子进程并读写文件—— /// 从不持有 `KernelCore`,因此按引用共享 `paths`/`args` 是安全的。 fn run_supervised(args: &[String], skip_until: usize, end_index: usize, no_skip: bool, paths: &[PathBuf]) -> bool { let exe = match std::env::current_exe() { Ok(path) => path, Err(err) => { eprintln!("failed to resolve current executable for supervised mode: {err}"); return false; } }; let window = std::env::var("OXIDE_TEST262_WINDOW") .ok() .and_then(|s| s.parse::().ok()) .filter(|&n| n > 0) .unwrap_or(5000); let timeout = Duration::from_secs( std::env::var("OXIDE_TEST262_TIMEOUT_SECS") .ok() .and_then(|s| s.parse::().ok()) .filter(|&n| n > 0) .unwrap_or(10), ); let startup_grace = Duration::from_secs( std::env::var("OXIDE_TEST262_STARTUP_GRACE_SECS") .ok() .and_then(|s| s.parse::().ok()) .filter(|&n| n > 0) .unwrap_or(60), ); let mut windows: Vec<(usize, usize, usize)> = Vec::new(); let mut start = skip_until; let mut id = 0usize; while start < end_index { let end = (start + window).min(end_index); windows.push((id, start, end)); start = end; id += 1; } let supervisors = std::env::var("OXIDE_TEST262_SUPERVISORS") .ok() .and_then(|s| s.parse::().ok()) .filter(|&n| n > 0) .unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)) .min(windows.len().max(1)); eprintln!( "supervised mode: {} window(s) of up to {window} test(s), {supervisors} supervisor(s), per-test timeout {}s", windows.len(), timeout.as_secs() ); let next = AtomicUsize::new(0); let next = &next; let windows = &windows; let exe = &exe; let partials: Vec = std::thread::scope(|scope| { let handles: Vec<_> = (0..supervisors) .map(|_| { scope.spawn(move || { let mut stats = RunStats::default(); loop { let wi = next.fetch_add(1, Ordering::Relaxed); if wi >= windows.len() { break; } let (window_id, wstart, wend) = windows[wi]; let window_stats = supervise_window( exe, args, no_skip, wstart, wend, timeout, startup_grace, paths, window_id, ); stats.merge(window_stats); } stats }) }) .collect(); handles .into_iter() .map(|h| h.join().expect("supervisor thread panicked")) .collect() }); let mut stats = RunStats::default(); for partial_stats in partials { stats.merge(partial_stats); } let total = stats.pass + stats.fail + stats.skip; println!(); println!("═══════════════════════════════════════"); println!(" test262 supervised aggregate"); println!("═══════════════════════════════════════"); println!(" total : {total}"); println!(" pass : {}", stats.pass); println!(" fail : {}", stats.fail); println!( " skip : {} (timeouts/crashes here by default; --no-skip counts them as fail)", stats.skip ); print_fail_categories(&stats); println!("═══════════════════════════════════════"); stats.fail == 0 } /// 串行与并行执行路径共享的每测试管线: /// 读文件、解析元数据、应用跳过过滤,然后运行。恰好返回一个 `TestResult`。 /// worker 自有状态(`kernel`、`harness_sources`、`harness_cache`)永不跨线程。 fn process_path( path: &Path, filter: &Option, no_skip: bool, no_regalloc: bool, kernel: &Arc, harness_sources: &HarnessSources, harness_cache: &Arc>, ) -> TestResult { let source = match std::fs::read_to_string(path) { Ok(s) => s, Err(e) => return TestResult::skip(path.to_path_buf(), format!("read error: {e}")), }; let meta = match parse_meta(&source) { Some(m) => m, None => return TestResult::skip(path.to_path_buf(), String::from("no YAML metadata")), }; if let Some(filter_str) = filter { let path_str = path.to_string_lossy().replace('\\', "/"); if !path_str.contains(filter_str.as_str()) { return TestResult::skip(path.to_path_buf(), "filtered".into()); } } if path.to_string_lossy().contains("staging") { return TestResult::skip(path.to_path_buf(), "staging tests excluded".into()); } if !no_skip { let path_str = path.to_string_lossy().replace('\\', "/"); if path_str.contains("/eval/") || path_str.contains("/function-ctor/") || path_str.contains("/realm/") { return TestResult::skip(path.to_path_buf(), "unsupported class/eval feature excluded".into()); } if let Some(reason) = is_skipped(&meta) { return TestResult::skip(path.to_path_buf(), reason); } } run_test(path, &source, &meta, kernel, harness_sources, harness_cache, no_skip, no_regalloc) } /// 构建带步数上限的 runner kernel。每个并行 worker 拥有自己的 kernel, /// 因为 `KernelCore` + session 状态是 `!Send`(持有 `P` = /// `Arc`,而 `JsObject` 存有裸 `*mut u8` 属性指针)。任何 kernel /// 形态的对象都不能跨线程边界,因此共享不可能;每 worker 自建是唯一正确设计。 fn build_runner_kernel() -> Arc { // 约束每个测试的执行步数,使单个死循环 / 未支持特性循环失败(或跳过) // 而非拖垮整个运行。VM 超限时抛 "VM step limit exceeded" 错误, // runner 将其归类为 step-limit 结果(默认 skip,--no-skip 下 fail)。 // 只覆盖 runner 本地配置;KernelConfig::minimal() 对其它 crate 保持无界。 let mut kernel_config = KernelConfig::minimal(); kernel_config.max_steps = Some(50_000_000); // 防止 test262 递归测试在 VM 把深层 JS 调用转成可捕获的 RangeError 之前 // 触及 Rust 原生栈。 kernel_config.max_call_depth = 256; kernel_config.min_pool_size = 1; kernel_config.max_pool_size = Some(1); KernelCore::new(kernel_config) } /// 把失败消息归类为可聚合的失败类别(compile/vm/parse/harness 等前缀)。 fn categorize_fail(msg: &str) -> String { if msg.contains("compile error:") { let reason = msg.trim_start_matches("compile error: ").trim(); if reason.contains("already been declared") { "compile: already declared".into() } else if reason.contains("not yet implemented") { "compile: not yet implemented".into() } else if reason.contains("not yet supported") { "compile: not yet supported".into() } else if reason.contains("unsupported") { "compile: unsupported".into() } else if reason.contains("is not defined") { "compile: not defined".into() } else { format!("compile: other ({})", reason.chars().take(60).collect::()) } } else if msg.contains("parse error:") { "parse error".into() } else if msg.contains("vm error:") { let reason = msg.trim_start_matches("vm error: ").trim(); if reason.contains("CALL_NATIVE target") { "vm: CALL_NATIVE no target".into() } else if reason.contains("not callable") { "vm: not callable".into() } else if reason.contains("not yet implemented") { "vm: not yet implemented".into() } else if reason.contains("step limit") { "vm: step limit".into() } else if reason.contains("not defined") { "vm: not defined".into() } else if reason.contains("unsupported") { "vm: unsupported".into() } else { format!("vm: other ({})", reason.chars().take(60).collect::()) } } else if msg.contains("engine panic") { "engine panic".into() } else if msg.contains("out-of-scope harness:") { "harness: blacklisted".into() } else if msg.contains("unknown harness:") { "harness: unknown".into() } else if msg.contains("harness compile error:") { "harness: compile error".into() } else if msg.contains("harness runtime error:") { "harness: runtime error".into() } else if msg.contains("expected runtime error") { "expected runtime error".into() } else { format!("other: {}", msg.chars().take(80).collect::()) } } /// 程序入口:安装带当前测试路径的 panic hook,并在大栈线程上运行测试。 fn main() { // 安装 panic hook,打印崩溃发生时正在运行的测试。 // 覆盖 Rust panic;OS 级崩溃(ACCESS_VIOLATION)由测试前的 eprintln! 捕获—— // 硬崩溃前打印的最后一行即标识文件。 std::panic::set_hook(Box::new(|info| { let path = CURRENT_TEST_PATH.with(|p| p.borrow().clone()); if !path.is_empty() { test262_error!("CRASH in test: {}", path); eprintln!("CRASH in test: {path}"); } test262_error!("panic: {}", info); eprintln!("panic: {info}"); })); let result = std::thread::Builder::new() .stack_size(16 * 1024 * 1024) .name("test262-runner".into()) .spawn(run_tests) .expect("failed to spawn test262 runner thread") .join() .expect("test262 runner thread panicked"); if !result { std::process::exit(1); } } /// 按失败类别计数降序打印 `--- FAIL categories ---` 段(无类别时静默)。 fn print_fail_categories(stats: &RunStats) { if stats.fail_categories.is_empty() { return; } println!(" --- FAIL categories ---"); let mut cats: Vec<_> = stats.fail_categories.iter().collect(); cats.sort_by_key(|(_, c)| -(**c as isize)); for (cat, count) in cats { println!(" {:>4} {}", count, cat); } } /// 测试主流程:解析配置 → 发现测试 → 按 supervise/chunked/并行 三种模式执行 → /// 汇总并打印统计;任何失败使返回值为 false(进程退出码 1)。 fn run_tests() -> bool { let args: Vec = std::env::args().collect(); let config = match RunConfig::parse(&args) { Ok(config) => config, Err(msg) => { test262_error!("config error: {}", msg); eprintln!("{msg}"); return false; } }; let mut log_level = Level::Info; if let Ok(s) = std::env::var("OXIDE_TEST262_LOG_LEVEL") { if let Some(l) = oxide_log::subsystem::parse_level(&s) { log_level = l; } } eprintln!("[LOG] level={log_level:?}"); oxide_log::init(&LogConfig { output: Output::Stderr, levels: [log_level; SUBSYSTEM_COUNT], }); let test262_root = if let Some(root) = config.test262_root.clone() { root } else { let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.pop(); p.pop(); p.push("tests"); p.push("test262"); p.push("test"); p }; if !test262_root.exists() { test262_error!("test262 not found at {}", test262_root.display()); eprintln!( "test262 not found at: {}\n\ Run: git submodule add https://github.com/tc39/test262.git tests/test262\n\ Then: cd tests/test262 && git checkout \n\ Or pass path as argument: cargo run -- ", test262_root.display() ); std::process::exit(1); } let filter = config.filter.clone().map(|f| f.replace('\\', "/")); test262_info!("discovering tests in: {}", test262_root.display()); eprintln!("discovering tests in: {}", test262_root.display()); if config.no_skip { test262_info!("no-skip mode enabled"); eprintln!("no-skip mode: capability filters disabled; unsupported results count as failures"); } let paths = discover_tests(&test262_root); test262_info!("found {} test files", paths.len()); eprintln!("found {} test files", paths.len()); let total = paths.len(); // 确定 worker 数。`KernelCore` + session 状态是 `!Send`(它持有 // 经 Arc 共享的一个 kernel;相反每个 worker 构建并拥有自己的 // kernel + harness 源注册表 + 前缀缓存。只有 `PathBuf` 和 // `TestResult`(均 `Send`)跨线程。worker 从共享原子游标取测试下标, // 实现动态负载均衡。 let default_workers = if config.no_skip { 4 } else { std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4) }; let workers = std::env::var("OXIDE_TEST262_WORKERS") .ok() .and_then(|s| s.parse::().ok()) .filter(|&n| n > 0) .unwrap_or(default_workers) .min(total.max(1)); let log_running_tests = std::env::var_os("OXIDE_TEST262_RUNNING_LOG").is_some(); let heartbeat_path: Option = std::env::var_os("OXIDE_TEST262_HEARTBEAT").map(PathBuf::from); test262_info!("running on {} worker thread(s)", workers); eprintln!("running on {workers} worker thread(s)"); let skip_until = std::env::var("OXIDE_SKIP_UNTIL") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(0) .min(total); let end_index = std::env::var("OXIDE_MAX_TESTS") .ok() .and_then(|s| s.parse::().ok()) .and_then(|n| skip_until.checked_add(n)) .map(|n| n.min(total)) .unwrap_or(total); let kernel_batch = std::env::var("OXIDE_TEST262_KERNEL_BATCH") .ok() .and_then(|s| s.parse::().ok()) .filter(|&n| n > 0) .unwrap_or(if config.no_skip { 1000 } else { 5000 }); let chunk_size = std::env::var("OXIDE_TEST262_CHUNK_SIZE") .ok() .and_then(|s| s.parse::().ok()) .filter(|&n| n > 0); let is_chunk_child = std::env::var_os("OXIDE_TEST262_CHILD_CHUNK").is_some(); let allow_fail_exit = std::env::var_os("OXIDE_TEST262_ALLOW_FAIL_EXIT").is_some(); if config.supervise && !is_chunk_child && filter.is_none() { test262_info!("supervised mode enabled"); eprintln!("supervised mode enabled: child-window execution with per-test timeout + auto-resume"); return run_supervised(&args, skip_until, end_index, config.no_skip, &paths); } if let Some(chunk_size) = chunk_size { if !is_chunk_child && filter.is_none() { test262_info!("chunked mode enabled: {} test(s) per child", chunk_size); eprintln!("chunked mode enabled: {chunk_size} test(s) per child process"); return run_chunked(&args, skip_until, end_index, chunk_size); } } test262_info!("kernel reset batch: {} test(s)", kernel_batch); eprintln!("kernel reset batch: {kernel_batch} test(s)"); let cursor = AtomicUsize::new(skip_until); let progress = AtomicUsize::new(skip_until); let filter = &filter; let no_skip = config.no_skip; let no_regalloc = config.no_regalloc; let verbose = config.verbose; let paths_ref = &paths; let heartbeat_ref = &heartbeat_path; let harness_cache = Arc::new(RwLock::new(HarnessPrefixCache::new())); // 保持内存平稳:worker 只返回聚合统计。保留数万个 `TestResult` 会使 // `--no-skip` 运行在套件末尾附近累积 path/error 字符串直至进程 OOM。 let partials: Vec = std::thread::scope(|scope| { let handles: Vec<_> = (0..workers) .map(|_| { let cursor = &cursor; let progress = &progress; let harness_cache = Arc::clone(&harness_cache); // 与 main() 的 16MB 栈一致:VM 在深层嵌套测试程序上递归, // 默认 worker 栈会溢出。 std::thread::Builder::new() .stack_size(16 * 1024 * 1024) .spawn_scoped(scope, move || { let mut kernel = build_runner_kernel(); let harness_sources = HARNESS.get_or_init(HarnessSources::new); let mut stats = RunStats::default(); let mut tests_since_kernel_reset = 0usize; let tid = std::thread::current().id(); loop { let i = cursor.fetch_add(1, Ordering::Relaxed); if i >= end_index { break; } let path_str = paths_ref[i].display().to_string(); // 始终记录当前测试路径,使 panic hook 能标识 Rust panic。 // 诊断需要最后一行 stderr 的 OS 级崩溃时设置 // OXIDE_TEST262_RUNNING_LOG=1。 CURRENT_TEST_PATH.with(|p| *p.borrow_mut() = path_str.clone()); if log_running_tests { test262_debug!("running: {}", path_str); eprintln!(" [{tid:?}] running: {path_str}"); } if let Some(hb) = heartbeat_ref { write_heartbeat( hb, "START", i, stats.pass, stats.fail, stats.skip, &stats.fail_categories, ); } let result = process_path( &paths_ref[i], filter, no_skip, no_regalloc, &kernel, harness_sources, &harness_cache, ); stats.record(&result); if verbose { let tag = match &result.outcome { TestOutcome::Pass(_) => "PASS", TestOutcome::Fail(_) => "FAIL", TestOutcome::Skip(_) => "SKIP", }; println!("{tag} {}", paths_ref[i].display()); } tests_since_kernel_reset += 1; let done = progress.fetch_add(1, Ordering::Relaxed) + 1; if done % 500 == 0 { kernel.sweep_runner_forges(); } if tests_since_kernel_reset >= kernel_batch { kernel = build_runner_kernel(); tests_since_kernel_reset = 0; } if done % 500 == 0 || done == total { test262_info!("progress: {}/{} ({}%)", done, total, done * 100 / total); eprintln!(" progress: {done}/{total} ({}%)", done * 100 / total); } } stats }) .expect("failed to spawn test262 worker thread") }) .collect(); handles .into_iter() .map(|h| h.join().expect("test262 worker thread panicked")) .collect() }); // 归约各 worker 的部分统计,不物化每个测试结果到内存。 let mut stats = RunStats::default(); for partial_stats in partials { stats.merge(partial_stats); } if let Some(hb) = &heartbeat_path { write_heartbeat(hb, "DONE", end_index, stats.pass, stats.fail, stats.skip, &stats.fail_categories); } eprintln!(); let ran = stats.pass + stats.fail; let executed_total = end_index.saturating_sub(skip_until); println!(); println!("═══════════════════════════════════════"); println!(" test262 results"); println!("═══════════════════════════════════════"); println!(" total : {}", executed_total); let total = executed_total as f64; println!(" pass : {} ({:.1}%)", stats.pass, stats.pass as f64 / total * 100.0); println!(" fail : {} ({:.1}%)", stats.fail, stats.fail as f64 / total * 100.0); println!(" skip : {} ({:.1}%)", stats.skip, stats.skip as f64 / total * 100.0); println!(" time : {:?}", Duration::from_millis(stats.total_ms)); println!( " pass% : {:.1}% (of ran: {:.1}%)", stats.pass as f64 / total * 100.0, if ran > 0 { stats.pass as f64 / ran as f64 * 100.0 } else { 0.0 } ); print_fail_categories(&stats); println!("═══════════════════════════════════════"); if stats.fail > 0 && !allow_fail_exit { return false; } true } #[cfg(test)] mod tests { use super::*; /// 便捷构造:按预期判定构造 Negative 元数据。 fn neg(error_type: &str) -> Negative { Negative { phase: "runtime".into(), error_type: error_type.into(), } } /// 断言 classify_vm_error 对给定错误消息返回指定结果变体。 fn assert_outcome(e: &str, neg: Option<&Negative>, no_skip: bool, want: &TestOutcome) { let got = classify_vm_error(e, neg, no_skip); let same = matches!( (want, &got), (TestOutcome::Pass(_), TestOutcome::Pass(_)) | (TestOutcome::Fail(_), TestOutcome::Fail(_)) | (TestOutcome::Skip(_), TestOutcome::Skip(_)) ); assert!(same, "error `{e}` (neg={neg:?}, no_skip={no_skip}) 期望 {want:?},实际 {got:?}"); } /// receiver 校验类错误:引擎已实现 receiver 检查并正确抛错,非 negative 测试 /// 抛此错即真实失败,不得计入 skip。 #[test] fn receiver_validation_errors_are_real_failures() { for e in [ "TypeError: Array.prototype.map method called on null", "TypeError: method called on incompatible receiver", "TypeError: called on non-Set object", "TypeError: called on non-Map object", "TypeError: called on non-ArrayBuffer object", "TypeError: called on non-TypedArray object", ] { assert_outcome(e, None, false, &TestOutcome::Fail("".into())); } } /// 真实 bug 形态(栈溢出 / 不可调用 / ToPrimitive / 属性写入缺口):语义与 /// 测试期望不符,移出 skip 列表后按真实失败计入。 #[test] fn engine_bug_shape_errors_are_real_failures() { for e in [ "RangeError: Maximum call stack size exceeded", "TypeError: x is not callable", "TypeError: Cannot convert object to primitive value", "TypeError: Cannot create property on non-object", "TypeError: Property description must be an object", ] { assert_outcome(e, None, false, &TestOutcome::Fail("".into())); } } /// 能力未实现形态保留 skip;`--no-skip` 下转 fail。 #[test] fn unimplemented_shapes_stay_skipped() { for e in [ "not yet implemented: Proxy", "feature is not supported", "unsupported syntax", "vm error: NEW_EXPRESSION not supported", "IC_GET_PROP on non-object", "GET_PROP_DYNAMIC on non-object", "SET_PROP_DYNAMIC on non-object", "private field brand check", "CALL_NATIVE target is null", "is not implemented", "unexpected tail call", "__proto__ must be an object", ] { assert_outcome(e, None, false, &TestOutcome::Skip("".into())); assert_outcome(e, None, true, &TestOutcome::Fail("".into())); } } /// 非 negative 且不含能力缺失标记的错误 → 真实失败。 #[test] fn unrelated_runtime_errors_are_failures() { assert_outcome("TypeError: value out of range", None, false, &TestOutcome::Fail("".into())); assert_outcome("ReferenceError: unexpected token", None, false, &TestOutcome::Fail("".into())); } /// negative 期望匹配 → Pass;不匹配 → Fail。 #[test] fn negative_meta_matching_wins_over_skip() { assert_outcome("TypeError: boom", Some(&neg("TypeError")), false, &TestOutcome::Pass("".into())); assert_outcome("ReferenceError: boom", Some(&neg("ReferenceError")), false, &TestOutcome::Pass("".into())); assert_outcome("TypeError: boom", Some(&neg("RangeError")), false, &TestOutcome::Fail("".into())); } /// `is not defined` 标识符解析:运行期 `uncaught` 前缀、编译期 `Identifier '..'` /// 两种形态都能取出标识符名;非该形态返回 None。 #[test] fn parse_undefined_ident_extracts_name() { assert_eq!(parse_undefined_ident("uncaught ReferenceError: $262 is not defined"), Some("$262")); assert_eq!(parse_undefined_ident("ReferenceError: foo is not defined"), Some("foo")); assert_eq!( parse_undefined_ident("compile error: Identifier 'structuredClone' is not defined"), Some("structuredClone") ); assert_eq!( parse_undefined_ident("Identifier 'queueMicrotask' is not defined"), Some("queueMicrotask") ); assert_eq!(parse_undefined_ident("TypeError: x is not callable"), None); assert_eq!(parse_undefined_ident("uncaught ReferenceError: boom"), None); } /// `is not defined` 白名单化:白名单内标识符(缺失全局)→ skip;白名单外 → /// 真实失败(保证修复后应转 PASS 的测试可见)。 #[test] fn undefined_identifier_whitelisted_skip() { for e in [ "uncaught ReferenceError: $262 is not defined", "compile error: Identifier '$262' is not defined", "uncaught ReferenceError: structuredClone is not defined", "compile error: Identifier 'queueMicrotask' is not defined", ] { assert_outcome(e, None, false, &TestOutcome::Skip("".into())); assert_outcome(e, None, true, &TestOutcome::Fail("".into())); } } #[test] fn undefined_identifier_outside_whitelist_is_failure() { for e in [ "uncaught ReferenceError: foo is not defined", "compile error: Identifier 'whatever' is not defined", ] { assert_outcome(e, None, false, &TestOutcome::Fail("".into())); } } /// 心跳写读往返:类别行随心跳头一起持久化并完整还原(含制表符/换行压平)。 #[test] fn heartbeat_round_trips_categories() { let dir = std::env::temp_dir(); let path = dir.join(format!("oxide_t262_hb_test_{}.txt", std::process::id())); let mut categories = HashMap::new(); categories.insert("vm: not defined".to_string(), 3); categories.insert("compile: unsupported".to_string(), 1); write_heartbeat(&path, "DONE", 42, 30, 4, 8, &categories); let hb = read_heartbeat(&path).expect("心跳应可读回"); assert_eq!(hb.phase, "DONE"); assert_eq!(hb.index, 42); assert_eq!(hb.pass, 30); assert_eq!(hb.fail, 4); assert_eq!(hb.skip, 8); assert_eq!(hb.categories.get("vm: not defined"), Some(&3)); assert_eq!(hb.categories.get("compile: unsupported"), Some(&1)); let _ = std::fs::remove_file(&path); } /// 类别文本含制表符/换行时写入压平,读取不破坏行结构。 #[test] fn heartbeat_flattens_category_separators() { let dir = std::env::temp_dir(); let path = dir.join(format!("oxide_t262_hb_test2_{}.txt", std::process::id())); let mut categories = HashMap::new(); categories.insert("vm: other (multi\nline\tmessage)".to_string(), 2); write_heartbeat(&path, "START", 7, 1, 2, 3, &categories); let hb = read_heartbeat(&path).expect("心跳应可读回"); assert_eq!(hb.fail, 2); assert_eq!(hb.categories.len(), 1); let key = hb.categories.keys().next().unwrap(); assert!(!key.contains('\t') && !key.contains('\n'), "类别键应已压平,实际 {key:?}"); assert_eq!(hb.categories.get(key), Some(&2)); let _ = std::fs::remove_file(&path); } }