mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-25 11:11:11 +08:00
363 lines
16 KiB
Rust
363 lines
16 KiB
Rust
//! 单测试执行管线: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)
|
||
}
|