Files
OxideJS-New/crates/oxide_test262/src/runner.rs
T

363 lines
16 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.
//! 单测试执行管线:harness 前缀拼接 → parse → compile → run → 判定,以及跳过门与每 worker kernel 构建。
//!
//! worker 自有的 kernel / harness 源 / 前缀缓存永不跨线程;CURRENT_TEST_PATH 在每个测试
//! 执行前写入,供 panic hook 定位崩溃测试文件。
#![allow(clippy::arc_with_non_send_sync)]
use crate::discovery::{eval_family_excluded, is_skipped, pre_existing_excluded};
use crate::harness::{
append_source_chunk, get_harness_module, get_harness_prefix, HarnessModuleCache, HarnessPrefixCache, HarnessSources,
};
use crate::judge::{
classify_compile_error, judge_async_result, judge_vm_error, panic_payload_str, TestOutcome, TestResult,
};
use crate::meta::{parse_meta, strip_meta, TestMeta};
use oxide_compiler::compiler::Compiler;
use oxide_engine::Engine;
use oxide_kernel::kernel::KernelConfig;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock, RwLock};
// 记录当前正在执行的测试路径(thread-local);每个测试执行前写入,
// panic hook 据此定位崩溃所在的测试文件。
std::thread_local! {
pub(crate) static CURRENT_TEST_PATH: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
}
/// 监督者经环境变量注入的 last-pc 现场文件路径(子进程单实例,每进程读一次):
/// 子进程运行期向该文件定频写现场行,监督者超时/崩溃杀子进程后读回末行。
static PC_WATCH_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
/// 读取监督者注入的 last-pc 现场文件路径(未注入返回 None)。
fn pc_watch_path() -> Option<&'static PathBuf> {
PC_WATCH_PATH
.get_or_init(|| std::env::var_os("OXIDE_TEST262_PC_WATCH").map(PathBuf::from))
.as_ref()
}
/// 在 catch_unwind 保护下运行单个测试,把引擎 panic 记为失败。
#[expect(clippy::too_many_arguments)]
fn run_test(
path: &Path, source: &str, meta: &TestMeta, engine: &Engine, harness: &HarnessSources,
harness_cache: &Arc<RwLock<HarnessPrefixCache>>, harness_module_cache: &Arc<RwLock<HarnessModuleCache>>,
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,
engine,
harness,
harness_cache,
harness_module_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, panic_payload_str(&panic))
}
}
}
/// 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};
// `<module source>` 是 test262 宿主哨兵说明符(INTERPRETING 约定):解析为
// 合成空模块,ModuleSource 对象由宿主在 `import source` 绑定时提供,不读磁盘。
// 路径回填哨兵本身(稳定规范形态),使自导入身份比较与循环检测不误判。
if specifier == "<module source>" {
return Ok(ResolvedModule {
source: String::new(),
path: "<module source>".to_string(),
kind: ModuleKind::Js,
});
}
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,
})
}
}
/// 单测执行主流程:编译 → run → 判定;依据 `negative` 元数据校验期望错误,
/// 未实现特性按 no_skip 选择跳过或失败。
///
/// module 臂保留本模块的 `parse_module` + 依赖加载器(引擎暂不吸收 module 加载器面),
/// 前缀与测试体拼为单模块编译。script 臂走两模块方案:前缀模块按 includes 组合
/// 缓存(免每测试重解析重编译),测试体模块每测试编译,顺序执行。两臂共享同一
/// compiler,`no_regalloc` 旗标对两臂同口径生效。
#[expect(clippy::too_many_arguments)]
fn run_test_inner(
path: &Path, source: &str, meta: &TestMeta, engine: &Engine, harness: &HarnessSources,
harness_cache: &Arc<RwLock<HarnessPrefixCache>>, harness_module_cache: &Arc<RwLock<HarnessModuleCache>>,
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 is_only_strict = meta.flags.iter().any(|f| f == "onlyStrict");
// compiler 由两臂共享,no_regalloc 旗标对两臂同口径生效。
let compiler = if no_regalloc { Compiler::new().with_regalloc(false) } else { Compiler::new() };
// 编译模块:module 臂单模块(前缀 + 测试体),script 臂两模块(缓存前缀模块 + 测试体模块)。
let modules: Vec<Arc<oxide_bytecode::CompiledModule>> = if is_module {
let code = match get_harness_prefix(meta, harness, harness_cache) {
Ok(prefix) => {
let mut code = prefix;
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);
}
};
// 线程本地分配器:解析与编译同闭包内完成,AST 生命周期被闭包边界约束。
// 闭包返回模块列表或提前判定的结果,外层拆包:Ok 取模块,Err 直接返回结果。
match oxide_parser::with_parse_allocator(|alloc| {
// 解析模块:解析失败按 negative 元数据判定 pass/fail,短路返回。
let program = oxide_parser::parse_module(alloc, &code).map_err(|errs| {
let dur = start.elapsed().as_millis() as u64;
let msg = format!("parse error: {}", errs[0].message);
if meta.negative.is_some() {
TestResult::pass(path.to_path_buf(), dur, msg)
} else {
TestResult::fail(path.to_path_buf(), dur, msg)
}
})?;
let mut loader = Test262ModuleLoader;
match compiler.compile_module(&program, path.to_string_lossy().as_ref(), &mut loader) {
Ok(m) => Ok(vec![Arc::new(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) => Err(TestResult::pass(path.to_path_buf(), dur, m)),
TestOutcome::Fail(m) => Err(TestResult::fail(path.to_path_buf(), dur, m)),
TestOutcome::Skip(m) => Err(TestResult::skip(path.to_path_buf(), m)),
}
}
}
}) {
Ok(modules) => modules,
Err(result) => return result,
}
} else {
// script 臂两模块方案:前缀模块按 includes 组合缓存(免每测试重解析重编译),
// 测试体模块每测试编译。onlyStrict 测试给测试体补 "use strict" 保持严格性。
let harness_module = match get_harness_module(meta, harness, engine, &compiler, harness_module_cache) {
Ok(m) => m,
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 mut test_body = String::from(strip_meta(source));
if is_only_strict {
test_body = format!("\"use strict\";\n{test_body}");
}
match engine.compile_with(&test_body, &compiler) {
Ok(test_module) => vec![harness_module, test_module],
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),
}
}
}
};
run_and_judge(path, meta, engine, &modules, is_async, no_skip, &start)
}
/// 在独立 Vm 上顺序执行各模块并判定。独立 Vm 不经池;动态编译服务由 `new_vm`
/// 同口径注入(语料含 eval / Function 动态编译面)。任一模块失败即停,不跑后续模块。
/// 完成值取最后一个模块的结果(script 臂即测试体模块)。
fn run_and_judge(
path: &Path, meta: &TestMeta, engine: &Engine, modules: &[Arc<oxide_bytecode::CompiledModule>], is_async: bool,
no_skip: bool, start: &std::time::Instant,
) -> TestResult {
let mut vm = engine.new_vm();
// 监督者注入现场文件路径时开启 VM 的 last-pc 定频写(未注入时零开销)。
if let Some(watch) = pc_watch_path() {
vm.set_pc_watch(Some(watch.clone()));
}
let mut run_result: Result<JsValue, String> = Ok(JsValue::undefined());
for module in modules {
// 按编译产物标志路由:TLA 模块走异步驱动(单次 drain 到静止、三态终结),
// 非 TLA 模块走原同步路径零 diff。判据是编译标志而非测试元数据旗标。
let result = if module.is_async { vm.run_tla(module) } else { vm.run(module) };
match result {
Ok(v) => run_result = Ok(v),
Err(e) => {
run_result = Err(e);
break;
}
}
}
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` 输出字符串。
pub(crate) fn read_async_output(vm: &Vm) -> String {
let si = vm.kernel_core().perm_interner().intern("$__test262_async_result").0;
let session = vm.session();
let global = 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()
}
/// 串行与并行执行路径共享的每测试管线:
/// 先应用只看路径串的跳过过滤,再读文件、解析元数据、应用依赖内容的跳过过滤,然后运行。
/// 恰好返回一个 `TestResult`。
/// worker 自有状态(`engine`、`harness_sources`、`harness_cache`、`harness_module_cache`)永不跨线程。
#[expect(clippy::too_many_arguments)]
pub(crate) fn process_path(
path: &Path, filter: &Option<String>, no_skip: bool, no_regalloc: bool, engine: &Engine,
harness_sources: &HarnessSources, harness_cache: &Arc<RwLock<HarnessPrefixCache>>,
harness_module_cache: &Arc<RwLock<HarnessModuleCache>>,
) -> TestResult {
let path_str = path.to_string_lossy().replace('\\', "/");
if let Some(filter_str) = filter {
if !path_str.contains(filter_str.as_str()) {
return TestResult::skip(path.to_path_buf(), "filtered".into());
}
}
if path_str.contains("staging") {
return TestResult::skip(path.to_path_buf(), "staging tests excluded".into());
}
if !no_skip {
if 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) = eval_family_excluded(&path_str) {
return TestResult::skip(path.to_path_buf(), reason.into());
}
if let Some(reason) = pre_existing_excluded(&path_str) {
return TestResult::skip(path.to_path_buf(), reason.into());
}
}
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 !no_skip {
if let Some(reason) = is_skipped(&meta) {
return TestResult::skip(path.to_path_buf(), reason);
}
}
run_test(
path,
&source,
&meta,
engine,
harness_sources,
harness_cache,
harness_module_cache,
no_skip,
no_regalloc,
)
}
/// 构建带步数上限的 runner 引擎。每个并行 worker 拥有自己的引擎,
/// 因为 `KernelCore` + session 状态是 `!Send`(持有 `P<JsObject>` =
/// `Arc<JsObject>`,而 `JsObject` 存有裸 `*mut u8` 属性指针)。任何 kernel
/// 形态的对象都不能跨线程边界,因此共享不可能;每 worker 自建是唯一正确设计。
pub(crate) fn build_runner_engine() -> Engine {
// 约束每个测试的执行步数,使单个死循环 / 未支持特性循环失败(或跳过)
// 而非拖垮整个运行。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 = 0;
kernel_config.max_pool_size = Some(1);
Engine::new(kernel_config)
}