#![allow(clippy::arc_with_non_send_sync)] use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, RwLock}; use std::time::Duration; mod chunked; mod config; mod discovery; mod harness; mod heartbeat; mod judge; mod meta; mod report; mod runner; mod stats; mod supervise; mod test262_log; use chunked::run_chunked; use config::RunConfig; use discovery::discover_tests; use harness::{HarnessModuleCache, HarnessPrefixCache, HarnessSources, HARNESS}; use heartbeat::write_heartbeat; use judge::{categorize_fail, TestOutcome}; use oxide_log::{Level, LogConfig, Output, SUBSYSTEM_COUNT}; use report::{append_fail_log, first_line, format_fail_categories, format_fail_groupings, format_fail_list}; use runner::{build_runner_engine, process_path, CURRENT_TEST_PATH}; use stats::RunStats; use supervise::run_supervised; /// kernel 重建批次的上界:realm 编号受属性键符号空间约束须小于 512, /// 单个 kernel 生命周期至多创建 512 个 Vm;用户配置超此值时钳到该上界。 const MAX_KERNEL_BATCH: usize = 512; /// 程序入口:安装带当前测试路径的 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 ---` 段(无类别时静默);格式委托 report 模块。 fn print_fail_categories(stats: &RunStats, paths: &[PathBuf]) { let out = format_fail_categories(stats, paths); if !out.is_empty() { print!("{out}"); } } /// 测试主流程:解析配置 → 发现测试 → 按 supervise/chunked/并行 三种模式执行 → /// 汇总并打印统计;任何失败使返回值为 false(进程退出码 1)。 fn run_tests() -> bool { let args: Vec = std::env::args().collect(); // 帮助走 stdout 并成功退出,与错误路径(stderr + 退出码 1)分离。 if args.iter().any(|a| a == "--help" || a == "-h") { println!("{}", RunConfig::usage()); return true; } // 版本自报走 stdout 并成功退出;哈希由构建期注入,无 git 环境缺省 "unknown"。 if args.iter().any(|a| a == "--version") { println!("test262-runner {} ({})", env!("CARGO_PKG_VERSION"), env!("GIT_COMMIT")); return true; } 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_FILE 指向目录时日志按日滚动落盘该目录(同镜像 stderr),缺省仅 stderr。 let output = match std::env::var("OXIDE_LOG_FILE") { Ok(dir) if !dir.is_empty() => Output::File(PathBuf::from(dir)), _ => Output::Stderr, }; oxide_log::init(&LogConfig { output, 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 构建并拥有自己的 // 引擎 + 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); 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); // 缺省 256:每 kernel 生命周期 realm 计数不超 512 上界(属性键符号空间约束), // 留一倍余量防重建边界 off-by-one 触发 debug 断言;用户配置超上界时钳到 // MAX_KERNEL_BATCH(断言要求编号 < 512,即每 kernel 生命周期至多 512 个 Vm)。 let kernel_batch = std::env::var("OXIDE_TEST262_KERNEL_BATCH") .ok() .and_then(|s| s.parse::().ok()) .filter(|&n| n > 0) .map(|n| n.min(MAX_KERNEL_BATCH)) .unwrap_or(256); 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.supervised(is_chunk_child) { 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); } } // 并行模式专属:监督/分块模式在此前已返回,不派生 worker 线程。 test262_info!("running on {} worker thread(s)", workers); eprintln!("running on {workers} worker thread(s)"); 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())); let harness_module_cache = Arc::new(RwLock::new(HarnessModuleCache::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); let harness_module_cache = Arc::clone(&harness_module_cache); // 与 main() 的 16MB 栈一致:VM 在深层嵌套测试程序上递归, // 默认 worker 栈会溢出。 std::thread::Builder::new() .stack_size(16 * 1024 * 1024) .spawn_scoped(scope, move || { let mut engine = build_runner_engine(); 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}"); } let result = process_path( &paths_ref[i], filter, no_skip, no_regalloc, &engine, harness_sources, &harness_cache, &harness_module_cache, ); stats.record(i, &result); // COMPLETED 心跳:先写心跳再追加旁路失败行(崩溃恢复 // 无漏项、无双计的写序契约)。 if let Some(hb) = heartbeat_ref { if let Err(e) = write_heartbeat( hb, "COMPLETED", i, stats.pass, stats.fail, stats.skip, &stats.fail_categories, stats.hb_write_errors, ) { stats.hb_write_errors += 1; eprintln!(" [warn] heartbeat write failed at #{i}: {e}"); } if let TestOutcome::Fail(msg) = &result.outcome { // 旁路行携带真 subkey,父进程并入 fail_records 后直接生效 subkey 分组。 let (cat, subkey) = categorize_fail(msg); if let Err(e) = append_fail_log(&hb.with_extension("fails"), i, &cat, &subkey, msg) { stats.hb_write_errors += 1; eprintln!(" [warn] fail log append failed at #{i}: {e}"); } } } if verbose { match &result.outcome { TestOutcome::Pass(_) => println!("PASS {}", paths_ref[i].display()), TestOutcome::Fail(msg) => { let (cat, _) = categorize_fail(msg); println!("FAIL {} [{}] {}", paths_ref[i].display(), cat, first_line(msg)); } TestOutcome::Skip(_) => println!("SKIP {}", paths_ref[i].display()), } } tests_since_kernel_reset += 1; let done = progress.fetch_add(1, Ordering::Relaxed) + 1; if tests_since_kernel_reset >= kernel_batch { // 重建即全新 forge(结构性必清):旧核连同其表整体丢弃, // 先 sweep 是对 doomed 核白做一次 O(50k) 清理。 // 重建边界契约:旧核上无存活 VM(VM 每测试局部,此处作用域外)。 debug_assert!(engine.kernel().active_vms() == 0, "kernel rebuild requires no live VMs"); engine = build_runner_engine(); tests_since_kernel_reset = 0; } else if tests_since_kernel_reset % 500 == 0 { engine.kernel().sweep_runner_forges(); // 批内 50k 兜底(数据依赖) } 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 { if let Err(e) = write_heartbeat( hb, "DONE", end_index, stats.pass, stats.fail, stats.skip, &stats.fail_categories, stats.hb_write_errors, ) { stats.hb_write_errors += 1; eprintln!(" [warn] final DONE heartbeat write failed: {e}"); } } 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, &paths); if !config.no_fail_list { let fail_list = format_fail_list(&stats, &paths); if !fail_list.is_empty() { print!("{fail_list}"); } } // 分组是聚合汇总段,恒打印(不受 --no-fail-list 控制);无 fail 数据时静默。 let groupings = format_fail_groupings(&stats, &paths); if !groupings.is_empty() { print!("{groupings}"); } println!("═══════════════════════════════════════"); if stats.fail > 0 && !allow_fail_exit { return false; } true }