This repository has been archived on 2026-09-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
OxideJS/crates/oxide_test262/src/main.rs
T

1758 lines
71 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![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<String> = 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<String>,
#[serde(default)]
includes: Vec<String>,
#[serde(default)]
features: Vec<String>,
#[serde(default)]
negative: Option<Negative>,
#[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<String>) -> Self {
Self {
path,
outcome: TestOutcome::Pass(msg.into()),
duration_ms: dur,
}
}
/// 构造一个失败结果。
fn fail(path: PathBuf, dur: u64, msg: impl Into<String>) -> 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<TestMeta> {
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::<TestMeta>(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<String, usize>,
}
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<PathBuf>,
filter: Option<String>,
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<Vec<String>, 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<HarnessSources> = 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<String> {
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<String, String> {
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<RwLock<HarnessPrefixCache>>,
) -> Result<String, String> {
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<Self, String> {
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<String> {
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<KernelCore>, harness: &HarnessSources,
harness_cache: &Arc<RwLock<HarnessPrefixCache>>, 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<oxide_emit::module::ResolvedModule, String> {
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<KernelCore>, harness: &HarnessSources,
harness_cache: &Arc<RwLock<HarnessPrefixCache>>, 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:<name>: <msg>`);其次成功 marker
/// (`Test262:AsyncTestComplete`);两者都无则测试未在 run() 内同步完成
/// (需 setTimeout 等异步 runner 的测试在此失败),回退到 run 结果判定。
fn judge_async_result(
path: &Path, run_result: Result<JsValue, String>, 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("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<PathBuf> {
let mut paths: Vec<PathBuf> = 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<usize> {
stdout.lines().find_map(|line| {
let trimmed = line.trim_start();
let rest = trimmed.strip_prefix(label)?;
let value = rest.trim().split(' ').next()?;
value.parse::<usize>().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<String, usize>,
}
/// 用单行心跳头 + 失败类别行覆写心跳文件。错误被忽略:漏写心跳只是把停滞
/// 检测推迟一个轮询间隔。假定单 worker(监督器强制 `OXIDE_TEST262_WORKERS=1`);
/// 多 worker 时运行下标有歧义且文件存在竞争。
fn write_heartbeat(
path: &Path, phase: &str, index: usize, pass: usize, fail: usize, skip: usize, categories: &HashMap<String, usize>,
) {
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<Heartbeat> {
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<usize> = 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::<usize>().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::<u64>().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::<u64>().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::<usize>().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<RunStats> = 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<String>, no_skip: bool, no_regalloc: bool, kernel: &Arc<KernelCore>,
harness_sources: &HarnessSources, harness_cache: &Arc<RwLock<HarnessPrefixCache>>,
) -> 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<JsObject>` =
/// `Arc<JsObject>`,而 `JsObject` 存有裸 `*mut u8` 属性指针)。任何 kernel
/// 形态的对象都不能跨线程边界,因此共享不可能;每 worker 自建是唯一正确设计。
fn build_runner_kernel() -> Arc<KernelCore> {
// 约束每个测试的执行步数,使单个死循环 / 未支持特性循环失败(或跳过)
// 而非拖垮整个运行。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::<String>())
}
} 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::<String>())
}
} 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::<String>())
}
}
/// 程序入口:安装带当前测试路径的 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<String> = 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 <tag>\n\
Or pass path as argument: cargo run -- <path-to-test262/test>",
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::<usize>().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<PathBuf> = 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::<usize>().ok())
.unwrap_or(0)
.min(total);
let end_index = std::env::var("OXIDE_MAX_TESTS")
.ok()
.and_then(|s| s.parse::<usize>().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::<usize>().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::<usize>().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<RunStats> = 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",
"IC_GET_PROP on non-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",
"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);
}
}