mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
refactor: test262 runner 判定与分类独立成模块并迁回测试
This commit is contained in:
@@ -0,0 +1,491 @@
|
||||
//! test262 单测试判定与失败分类:`TestOutcome`/`TestResult` 判定模型、compile/vm 错误三态判定、未绑定
|
||||
//! 标识符解析与缺失全局白名单、异步 `$DONE` 判定、panic 载荷文本化、失败类别分类。5 个跨模块被调
|
||||
//! 判定/分类函数与判定模型放宽 `pub(crate)`;`classify_vm_error`/`parse_undefined_ident`/`KNOWN_MISSING_GLOBALS` 保持私有。
|
||||
|
||||
use crate::meta::{Negative, TestMeta};
|
||||
use crate::read_async_output;
|
||||
use crate::report::extract_not_callable_subkey;
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 单个测试的判定结果:通过 / 失败 / 跳过(各带说明消息)。
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum TestOutcome {
|
||||
Pass(String),
|
||||
Fail(String),
|
||||
Skip(String),
|
||||
}
|
||||
|
||||
/// 单个测试的运行结果:路径、判定与耗时(毫秒)。
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct TestResult {
|
||||
pub(crate) path: PathBuf,
|
||||
pub(crate) outcome: TestOutcome,
|
||||
pub(crate) duration_ms: u64,
|
||||
}
|
||||
|
||||
impl TestResult {
|
||||
/// 构造一个通过结果。
|
||||
pub(crate) fn pass(path: PathBuf, dur: u64, msg: impl Into<String>) -> Self {
|
||||
Self {
|
||||
path,
|
||||
outcome: TestOutcome::Pass(msg.into()),
|
||||
duration_ms: dur,
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造一个失败结果。
|
||||
pub(crate) fn fail(path: PathBuf, dur: u64, msg: impl Into<String>) -> Self {
|
||||
Self {
|
||||
path,
|
||||
outcome: TestOutcome::Fail(msg.into()),
|
||||
duration_ms: dur,
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造一个跳过结果(不计耗时)。
|
||||
pub(crate) fn skip(path: PathBuf, msg: String) -> Self {
|
||||
Self {
|
||||
path,
|
||||
outcome: TestOutcome::Skip(msg),
|
||||
duration_ms: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 catch_unwind 的 panic payload 提取可读文本:`&str` 与 `String` 两种
|
||||
/// 常见形态,其余返回占位文本。
|
||||
pub(crate) fn panic_payload_str(payload: &Box<dyn std::any::Any + Send>) -> String {
|
||||
if let Some(s) = payload.downcast_ref::<&str>() {
|
||||
return format!("engine panic: {s}");
|
||||
}
|
||||
if let Some(s) = payload.downcast_ref::<String>() {
|
||||
return format!("engine panic: {s}");
|
||||
}
|
||||
"engine panic (non-string payload)".into()
|
||||
}
|
||||
|
||||
/// 异步测试判定:`$DONE` 把结果写入捕获串,run() 结束时微任务队列已 drain。
|
||||
///
|
||||
/// 失败 marker 优先(`Test262:AsyncTestFailure:<name>: <msg>`);其次成功 marker
|
||||
/// (`Test262:AsyncTestComplete`);两者都无则测试未在 run() 内同步完成
|
||||
/// (需 setTimeout 等异步 runner 的测试在此失败),回退到 run 结果判定。
|
||||
pub(crate) 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行期错误的判定(negative 匹配优先 + 未实现特性跳过分类)。
|
||||
pub(crate) 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`
|
||||
/// (可带 `vm error: ` 前缀,即 runner Fail 消息的生产形态)
|
||||
/// - 编译期形态:`Identifier '{name}' is not defined`(可带 `compile error: ` 前缀)
|
||||
/// - 两种形态都不匹配返回 `None`(不属未绑定标识符错误)。
|
||||
fn parse_undefined_ident(e: &str) -> Option<&str> {
|
||||
let e = e.strip_prefix("vm error: ").unwrap_or(e);
|
||||
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。
|
||||
pub(crate) 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("memory 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}"))
|
||||
}
|
||||
|
||||
/// 把失败消息归类为可聚合的失败类别与 subkey,返回 `(category, subkey)`。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 类别大类名口径不变(心跳序列化 / 既有基线对比兼容);碎片桶
|
||||
/// (`compile: other` / `vm: other` / `other`)不再携带消息尾巴
|
||||
/// (全文已在 FailRecord.message)。
|
||||
/// - subkey 仅 `not callable`(调用点,提取失败归 `(none)`)与
|
||||
/// `not defined`(标识符)两类非空,其余恒空串;
|
||||
/// `IC_GET_PROP on non-object` 独立成桶且 subkey 恒空(靠目录分组区分)。
|
||||
pub(crate) fn categorize_fail(msg: &str) -> (String, 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(), String::new())
|
||||
} else if reason.contains("not yet implemented") {
|
||||
("compile: not yet implemented".into(), String::new())
|
||||
} else if reason.contains("not yet supported") {
|
||||
("compile: not yet supported".into(), String::new())
|
||||
} else if reason.contains("unsupported") {
|
||||
("compile: unsupported".into(), String::new())
|
||||
} else if reason.contains("is not defined") {
|
||||
("compile: not defined".into(), parse_undefined_ident(msg).unwrap_or("").to_string())
|
||||
} else {
|
||||
("compile: other".into(), String::new())
|
||||
}
|
||||
} else if msg.contains("parse error:") {
|
||||
("parse error".into(), String::new())
|
||||
} 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(), String::new())
|
||||
} else if reason.contains("not callable") {
|
||||
("vm: not callable".into(), extract_not_callable_subkey(msg))
|
||||
} else if reason.contains("IC_GET_PROP on non-object") {
|
||||
("vm: IC_GET_PROP on non-object".into(), String::new())
|
||||
} else if reason.contains("not yet implemented") {
|
||||
("vm: not yet implemented".into(), String::new())
|
||||
} else if reason.contains("step limit") {
|
||||
("vm: step limit".into(), String::new())
|
||||
} else if reason.contains("memory limit") {
|
||||
("vm: memory limit".into(), String::new())
|
||||
} else if reason.contains("not defined") {
|
||||
("vm: not defined".into(), parse_undefined_ident(msg).unwrap_or("").to_string())
|
||||
} else if reason.contains("unsupported") {
|
||||
("vm: unsupported".into(), String::new())
|
||||
} else {
|
||||
("vm: other".into(), String::new())
|
||||
}
|
||||
} else if msg.contains("engine panic") {
|
||||
("engine panic".into(), String::new())
|
||||
} else if msg.contains("out-of-scope harness:") {
|
||||
("harness: blacklisted".into(), String::new())
|
||||
} else if msg.contains("unknown harness:") {
|
||||
("harness: unknown".into(), String::new())
|
||||
} else if msg.contains("harness compile error:") {
|
||||
("harness: compile error".into(), String::new())
|
||||
} else if msg.contains("harness runtime error:") {
|
||||
("harness: runtime error".into(), String::new())
|
||||
} else if msg.contains("expected runtime error") {
|
||||
("expected runtime error".into(), String::new())
|
||||
} else if let Some(ident) = parse_undefined_ident(msg) {
|
||||
// 无前缀形态的未绑定标识符错误兜底(生产路径均带 vm error: 前缀走 vm 臂):
|
||||
// 形态门——parse 出标识符才入 not-defined 桶;negative mismatch 等
|
||||
// 解析不出的含 `is not defined` 形态回落 other,不污染 not-defined 计数。
|
||||
("vm: not defined".into(), ident.to_string())
|
||||
} else {
|
||||
("other".into(), String::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[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 memory_limit_errors_skip_by_default() {
|
||||
let e = "vm error: VM memory limit 536870912 exceeded (used 536871936) at pc=14";
|
||||
assert_outcome(e, None, false, &TestOutcome::Skip("".into()));
|
||||
assert_outcome(e, None, true, &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` 前缀(含 `vm error: `
|
||||
/// 生产前缀)、编译期 `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("vm error: uncaught ReferenceError: foo is not defined"),
|
||||
Some("foo")
|
||||
);
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
/// categorize_fail 双返回:类别大类名口径不变(碎片桶去消息尾巴),
|
||||
/// subkey 仅 not callable / not defined 三类非空,IC_GET_PROP 独立成桶。
|
||||
/// not defined 覆盖 `vm error: ` 生产形态(subkey 非空)与无前缀裸形态
|
||||
/// (形态门:parse 出标识符才入桶);negative mismatch 形态归 other。
|
||||
#[test]
|
||||
fn categorize_fail_returns_category_and_subkey() {
|
||||
let cases = [
|
||||
("vm error: TypeError: CALL target is not callable", ("vm: not callable", "CALL target")),
|
||||
("vm error: TypeError: accessor is not callable", ("vm: not callable", "accessor")),
|
||||
("vm error: TypeError: x is not callable", ("vm: not callable", "x")),
|
||||
("vm error: uncaught ReferenceError: foo is not defined", ("vm: not defined", "foo")),
|
||||
("uncaught ReferenceError: foo is not defined", ("vm: not defined", "foo")),
|
||||
("compile error: Identifier 'x' is not defined", ("compile: not defined", "x")),
|
||||
("vm error: IC_GET_PROP on non-object", ("vm: IC_GET_PROP on non-object", "")),
|
||||
(
|
||||
"vm error: VM memory limit 536870912 exceeded (used 536871936) at pc=14",
|
||||
("vm: memory limit", ""),
|
||||
),
|
||||
("vm error: TypeError: method called on incompatible receiver", ("vm: other", "")),
|
||||
("engine panic: boom", ("engine panic", "")),
|
||||
(
|
||||
"expected TypeError error, got: uncaught ReferenceError: foo is not defined",
|
||||
("other", ""),
|
||||
),
|
||||
("random string", ("other", "")),
|
||||
];
|
||||
for (msg, (want_cat, want_sub)) in cases {
|
||||
assert_eq!(categorize_fail(msg), (want_cat.to_string(), want_sub.to_string()), "消息 {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
/// panic payload 三类形态:&str / String / 其它类型占位文本。
|
||||
#[test]
|
||||
fn panic_payload_str_extracts_string_and_str() {
|
||||
let payload: Box<dyn std::any::Any + Send> = Box::new("boom");
|
||||
assert_eq!(panic_payload_str(&payload), "engine panic: boom");
|
||||
let payload: Box<dyn std::any::Any + Send> = Box::new("boom".to_string());
|
||||
assert_eq!(panic_payload_str(&payload), "engine panic: boom");
|
||||
let payload: Box<dyn std::any::Any + Send> = Box::new(42u32);
|
||||
assert_eq!(panic_payload_str(&payload), "engine panic (non-string payload)");
|
||||
}
|
||||
}
|
||||
@@ -14,17 +14,22 @@ use std::time::{Duration, Instant};
|
||||
mod config;
|
||||
mod discovery;
|
||||
mod harness;
|
||||
mod judge;
|
||||
mod meta;
|
||||
mod report;
|
||||
mod test262_log;
|
||||
use config::RunConfig;
|
||||
use discovery::{discover_tests, eval_family_excluded, is_skipped, pre_existing_excluded};
|
||||
use harness::{append_source_chunk, get_harness_prefix, HarnessPrefixCache, HarnessSources, HARNESS};
|
||||
use meta::{parse_meta, strip_meta, Negative, TestMeta};
|
||||
use judge::{
|
||||
categorize_fail, classify_compile_error, judge_async_result, judge_vm_error, panic_payload_str, TestOutcome,
|
||||
TestResult,
|
||||
};
|
||||
use meta::{parse_meta, strip_meta, TestMeta};
|
||||
use oxide_log::{Level, LogConfig, Output, SUBSYSTEM_COUNT};
|
||||
use report::{
|
||||
append_fail_log, extract_not_callable_subkey, first_line, format_fail_categories, format_fail_groupings,
|
||||
format_fail_list, parse_fail_log, FailRecord, MAX_FAIL_MSG_CHARS, MAX_FAIL_RECORD_BYTES,
|
||||
append_fail_log, first_line, format_fail_categories, format_fail_groupings, format_fail_list, parse_fail_log,
|
||||
FailRecord, MAX_FAIL_MSG_CHARS, MAX_FAIL_RECORD_BYTES,
|
||||
};
|
||||
|
||||
// 记录当前正在执行的测试路径(thread-local);每个测试执行前写入,
|
||||
@@ -33,53 +38,6 @@ std::thread_local! {
|
||||
static CURRENT_TEST_PATH: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
|
||||
}
|
||||
|
||||
/// 单个测试的判定结果:通过 / 失败 / 跳过(各带说明消息)。
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 全部已运行测试的累计统计:通过/失败/跳过计数、总耗时、失败原因分类
|
||||
/// 与逐路径失败记录(fail_records,供汇总区聚合报告)。
|
||||
#[derive(Default)]
|
||||
@@ -194,18 +152,6 @@ fn run_test(
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 catch_unwind 的 panic payload 提取可读文本:`&str` 与 `String` 两种
|
||||
/// 常见形态,其余返回占位文本。
|
||||
fn panic_payload_str(payload: &Box<dyn std::any::Any + Send>) -> String {
|
||||
if let Some(s) = payload.downcast_ref::<&str>() {
|
||||
return format!("engine panic: {s}");
|
||||
}
|
||||
if let Some(s) = payload.downcast_ref::<String>() {
|
||||
return format!("engine panic: {s}");
|
||||
}
|
||||
"engine panic (non-string payload)".into()
|
||||
}
|
||||
|
||||
/// test262 模块用例的依赖加载器:以测试文件父目录为基准解析相对导入,
|
||||
/// 读取磁盘上的 fixture 源码;json/text/bytes 经 import attributes 判定。
|
||||
struct Test262ModuleLoader;
|
||||
@@ -344,52 +290,6 @@ fn run_test_inner(
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步测试判定:`$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;
|
||||
@@ -403,122 +303,6 @@ fn read_async_output(vm: &Vm) -> String {
|
||||
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`
|
||||
/// (可带 `vm error: ` 前缀,即 runner Fail 消息的生产形态)
|
||||
/// - 编译期形态:`Identifier '{name}' is not defined`(可带 `compile error: ` 前缀)
|
||||
/// - 两种形态都不匹配返回 `None`(不属未绑定标识符错误)。
|
||||
fn parse_undefined_ident(e: &str) -> Option<&str> {
|
||||
let e = e.strip_prefix("vm error: ").unwrap_or(e);
|
||||
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("memory 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}"))
|
||||
}
|
||||
|
||||
/// 从子进程 stdout 中解析形如 `label : N` 的汇总行。
|
||||
fn parse_summary_count(stdout: &str, label: &str) -> Option<usize> {
|
||||
stdout.lines().find_map(|line| {
|
||||
@@ -1108,76 +892,6 @@ fn build_runner_kernel() -> Arc<KernelCore> {
|
||||
KernelCore::new(kernel_config)
|
||||
}
|
||||
|
||||
/// 把失败消息归类为可聚合的失败类别与 subkey,返回 `(category, subkey)`。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 类别大类名口径不变(心跳序列化 / 既有基线对比兼容);碎片桶
|
||||
/// (`compile: other` / `vm: other` / `other`)不再携带消息尾巴
|
||||
/// (全文已在 FailRecord.message)。
|
||||
/// - subkey 仅 `not callable`(调用点,提取失败归 `(none)`)与
|
||||
/// `not defined`(标识符)两类非空,其余恒空串;
|
||||
/// `IC_GET_PROP on non-object` 独立成桶且 subkey 恒空(靠目录分组区分)。
|
||||
fn categorize_fail(msg: &str) -> (String, 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(), String::new())
|
||||
} else if reason.contains("not yet implemented") {
|
||||
("compile: not yet implemented".into(), String::new())
|
||||
} else if reason.contains("not yet supported") {
|
||||
("compile: not yet supported".into(), String::new())
|
||||
} else if reason.contains("unsupported") {
|
||||
("compile: unsupported".into(), String::new())
|
||||
} else if reason.contains("is not defined") {
|
||||
("compile: not defined".into(), parse_undefined_ident(msg).unwrap_or("").to_string())
|
||||
} else {
|
||||
("compile: other".into(), String::new())
|
||||
}
|
||||
} else if msg.contains("parse error:") {
|
||||
("parse error".into(), String::new())
|
||||
} 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(), String::new())
|
||||
} else if reason.contains("not callable") {
|
||||
("vm: not callable".into(), extract_not_callable_subkey(msg))
|
||||
} else if reason.contains("IC_GET_PROP on non-object") {
|
||||
("vm: IC_GET_PROP on non-object".into(), String::new())
|
||||
} else if reason.contains("not yet implemented") {
|
||||
("vm: not yet implemented".into(), String::new())
|
||||
} else if reason.contains("step limit") {
|
||||
("vm: step limit".into(), String::new())
|
||||
} else if reason.contains("memory limit") {
|
||||
("vm: memory limit".into(), String::new())
|
||||
} else if reason.contains("not defined") {
|
||||
("vm: not defined".into(), parse_undefined_ident(msg).unwrap_or("").to_string())
|
||||
} else if reason.contains("unsupported") {
|
||||
("vm: unsupported".into(), String::new())
|
||||
} else {
|
||||
("vm: other".into(), String::new())
|
||||
}
|
||||
} else if msg.contains("engine panic") {
|
||||
("engine panic".into(), String::new())
|
||||
} else if msg.contains("out-of-scope harness:") {
|
||||
("harness: blacklisted".into(), String::new())
|
||||
} else if msg.contains("unknown harness:") {
|
||||
("harness: unknown".into(), String::new())
|
||||
} else if msg.contains("harness compile error:") {
|
||||
("harness: compile error".into(), String::new())
|
||||
} else if msg.contains("harness runtime error:") {
|
||||
("harness: runtime error".into(), String::new())
|
||||
} else if msg.contains("expected runtime error") {
|
||||
("expected runtime error".into(), String::new())
|
||||
} else if let Some(ident) = parse_undefined_ident(msg) {
|
||||
// 无前缀形态的未绑定标识符错误兜底(生产路径均带 vm error: 前缀走 vm 臂):
|
||||
// 形态门——parse 出标识符才入 not-defined 桶;negative mismatch 等
|
||||
// 解析不出的含 `is not defined` 形态回落 other,不污染 not-defined 计数。
|
||||
("vm: not defined".into(), ident.to_string())
|
||||
} else {
|
||||
("other".into(), String::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// 程序入口:安装带当前测试路径的 panic hook,并在大栈线程上运行测试。
|
||||
fn main() {
|
||||
// 安装 panic hook,打印崩溃发生时正在运行的测试。
|
||||
@@ -1524,186 +1238,12 @@ fn run_tests() -> bool {
|
||||
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 memory_limit_errors_skip_by_default() {
|
||||
let e = "vm error: VM memory limit 536870912 exceeded (used 536871936) at pc=14";
|
||||
assert_outcome(e, None, false, &TestOutcome::Skip("".into()));
|
||||
assert_outcome(e, None, true, &TestOutcome::Fail("".into()));
|
||||
}
|
||||
|
||||
/// 分配上限常量与错误串单测的字面值机器耦合:改 cap 本钉必红。
|
||||
#[test]
|
||||
fn runner_alloc_cap_matches_error_string_tests() {
|
||||
assert_eq!(build_runner_kernel().config.max_alloc_bytes, Some(512 * 1024 * 1024));
|
||||
}
|
||||
|
||||
/// 能力未实现形态保留 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` 前缀(含 `vm error: `
|
||||
/// 生产前缀)、编译期 `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("vm error: uncaught ReferenceError: foo is not defined"),
|
||||
Some("foo")
|
||||
);
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
/// categorize_fail 双返回:类别大类名口径不变(碎片桶去消息尾巴),
|
||||
/// subkey 仅 not callable / not defined 三类非空,IC_GET_PROP 独立成桶。
|
||||
/// not defined 覆盖 `vm error: ` 生产形态(subkey 非空)与无前缀裸形态
|
||||
/// (形态门:parse 出标识符才入桶);negative mismatch 形态归 other。
|
||||
#[test]
|
||||
fn categorize_fail_returns_category_and_subkey() {
|
||||
let cases = [
|
||||
("vm error: TypeError: CALL target is not callable", ("vm: not callable", "CALL target")),
|
||||
("vm error: TypeError: accessor is not callable", ("vm: not callable", "accessor")),
|
||||
("vm error: TypeError: x is not callable", ("vm: not callable", "x")),
|
||||
("vm error: uncaught ReferenceError: foo is not defined", ("vm: not defined", "foo")),
|
||||
("uncaught ReferenceError: foo is not defined", ("vm: not defined", "foo")),
|
||||
("compile error: Identifier 'x' is not defined", ("compile: not defined", "x")),
|
||||
("vm error: IC_GET_PROP on non-object", ("vm: IC_GET_PROP on non-object", "")),
|
||||
(
|
||||
"vm error: VM memory limit 536870912 exceeded (used 536871936) at pc=14",
|
||||
("vm: memory limit", ""),
|
||||
),
|
||||
("vm error: TypeError: method called on incompatible receiver", ("vm: other", "")),
|
||||
("engine panic: boom", ("engine panic", "")),
|
||||
(
|
||||
"expected TypeError error, got: uncaught ReferenceError: foo is not defined",
|
||||
("other", ""),
|
||||
),
|
||||
("random string", ("other", "")),
|
||||
];
|
||||
for (msg, (want_cat, want_sub)) in cases {
|
||||
assert_eq!(categorize_fail(msg), (want_cat.to_string(), want_sub.to_string()), "消息 {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
/// record 把真 subkey 写入 FailRecord(not callable 调用点透传)。
|
||||
#[test]
|
||||
fn record_stores_real_subkey() {
|
||||
@@ -1796,51 +1336,6 @@ mod tests {
|
||||
assert_eq!(a.fail_record_bytes, a.fail_records.iter().map(|r| r.message.len()).sum::<usize>());
|
||||
}
|
||||
|
||||
/// FAIL 清单每类别只列样本条数,其余按折叠行计数。
|
||||
#[test]
|
||||
fn format_fail_list_folds_over_limit() {
|
||||
let mut stats = RunStats::default();
|
||||
let paths: Vec<PathBuf> = (0..7).map(|i| PathBuf::from(format!("p{i}.js"))).collect();
|
||||
for (i, p) in paths.iter().enumerate() {
|
||||
stats.record(i, &TestResult::fail(p.clone(), 1, "vm error: x is not callable"));
|
||||
}
|
||||
let out = format_fail_list(&stats, &paths);
|
||||
let fail_lines = out.lines().filter(|l| l.starts_with(" FAIL ")).count();
|
||||
assert_eq!(fail_lines, 5);
|
||||
assert!(out.contains("(+2 more in vm: not callable)"), "应含折叠行,实际:\n{out}");
|
||||
}
|
||||
|
||||
/// 碎片桶附样本行:路径 + 消息首行,类别带截断尾巴也能前缀匹配。
|
||||
#[test]
|
||||
fn format_fail_categories_attaches_other_bucket_samples() {
|
||||
let mut stats = RunStats::default();
|
||||
let paths = vec![PathBuf::from("p.js")];
|
||||
stats.record(0, &TestResult::fail(paths[0].clone(), 1, "vm error: weird message one two"));
|
||||
let out = format_fail_categories(&stats, &paths);
|
||||
assert!(out.contains("sample:"), "应含样本行,实际:\n{out}");
|
||||
assert!(out.contains("weird message one"), "样本应含消息首行,实际:\n{out}");
|
||||
}
|
||||
|
||||
/// 无失败记录时 FAIL 清单段为空串(调用处静默)。
|
||||
#[test]
|
||||
fn format_fail_list_empty_without_records() {
|
||||
let stats = RunStats::default();
|
||||
let paths: Vec<PathBuf> = Vec::new();
|
||||
assert_eq!(format_fail_list(&stats, &paths), "");
|
||||
assert_eq!(format_fail_categories(&stats, &paths), "");
|
||||
}
|
||||
|
||||
/// panic payload 三类形态:&str / String / 其它类型占位文本。
|
||||
#[test]
|
||||
fn panic_payload_str_extracts_string_and_str() {
|
||||
let payload: Box<dyn std::any::Any + Send> = Box::new("boom");
|
||||
assert_eq!(panic_payload_str(&payload), "engine panic: boom");
|
||||
let payload: Box<dyn std::any::Any + Send> = Box::new("boom".to_string());
|
||||
assert_eq!(panic_payload_str(&payload), "engine panic: boom");
|
||||
let payload: Box<dyn std::any::Any + Send> = Box::new(42u32);
|
||||
assert_eq!(panic_payload_str(&payload), "engine panic (non-string payload)");
|
||||
}
|
||||
|
||||
/// 心跳第 6 字段(hb_write_errors)写读往返。
|
||||
#[test]
|
||||
fn heartbeat_round_trips_write_errors_field() {
|
||||
@@ -1921,28 +1416,6 @@ mod tests {
|
||||
assert_eq!(a.timeout_crashes, vec![(0, 100), (1, 200)]);
|
||||
}
|
||||
|
||||
/// 旁路失败行往返:append 后 parse 恢复全部字段;残缺/畸形尾行静默跳过。
|
||||
#[test]
|
||||
fn fail_log_round_trips_and_skips_truncated() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join(format!("oxide_t262_fails_test_{}.log", std::process::id()));
|
||||
append_fail_log(&path, 3, "vm: not callable", "", "x is not callable").expect("追加失败");
|
||||
append_fail_log(&path, 4, "compile: unsupported", "foo", "a\tb\nc").expect("追加失败");
|
||||
let mut content = std::fs::read_to_string(&path).expect("读取失败");
|
||||
content.push_str("5\tvm: x\n"); // 残缺:仅 2 字段(SIGKILL 半写形态)
|
||||
content.push_str("x\tb\tc\td\n"); // 畸形:index 非数字
|
||||
std::fs::write(&path, content).expect("写回失败");
|
||||
let content = std::fs::read_to_string(&path).expect("读取失败");
|
||||
let rows = parse_fail_log(&content);
|
||||
assert_eq!(rows.len(), 2, "残缺/畸形行应被跳过,实际 {rows:?}");
|
||||
assert_eq!(
|
||||
rows[0],
|
||||
(3, "vm: not callable".to_string(), "".to_string(), "x is not callable".to_string())
|
||||
);
|
||||
assert_eq!(rows[1], (4, "compile: unsupported".to_string(), "foo".to_string(), "a b c".to_string()));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
/// merge_fail_log 重建 fail_records:类别经 id 表重映射、消息 cap;
|
||||
/// fail_categories 保持空(计数不双计契约)。
|
||||
#[test]
|
||||
|
||||
@@ -228,6 +228,7 @@ pub fn format_fail_groupings(stats: &RunStats, paths: &[PathBuf]) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::judge::TestResult;
|
||||
|
||||
#[test]
|
||||
fn first_line_trims_at_newline() {
|
||||
@@ -295,4 +296,60 @@ mod tests {
|
||||
assert!(out.lines().any(|l| l.trim_start() == "b/c/d : 1"), "实际:\n{out}");
|
||||
assert!(out.lines().any(|l| l.trim_start() == ": 1"), "实际:\n{out}");
|
||||
}
|
||||
|
||||
/// FAIL 清单每类别只列样本条数,其余按折叠行计数。
|
||||
#[test]
|
||||
fn format_fail_list_folds_over_limit() {
|
||||
let mut stats = RunStats::default();
|
||||
let paths: Vec<PathBuf> = (0..7).map(|i| PathBuf::from(format!("p{i}.js"))).collect();
|
||||
for (i, p) in paths.iter().enumerate() {
|
||||
stats.record(i, &TestResult::fail(p.clone(), 1, "vm error: x is not callable"));
|
||||
}
|
||||
let out = format_fail_list(&stats, &paths);
|
||||
let fail_lines = out.lines().filter(|l| l.starts_with(" FAIL ")).count();
|
||||
assert_eq!(fail_lines, 5);
|
||||
assert!(out.contains("(+2 more in vm: not callable)"), "应含折叠行,实际:\n{out}");
|
||||
}
|
||||
|
||||
/// 碎片桶附样本行:路径 + 消息首行,类别带截断尾巴也能前缀匹配。
|
||||
#[test]
|
||||
fn format_fail_categories_attaches_other_bucket_samples() {
|
||||
let mut stats = RunStats::default();
|
||||
let paths = vec![PathBuf::from("p.js")];
|
||||
stats.record(0, &TestResult::fail(paths[0].clone(), 1, "vm error: weird message one two"));
|
||||
let out = format_fail_categories(&stats, &paths);
|
||||
assert!(out.contains("sample:"), "应含样本行,实际:\n{out}");
|
||||
assert!(out.contains("weird message one"), "样本应含消息首行,实际:\n{out}");
|
||||
}
|
||||
|
||||
/// 无失败记录时 FAIL 清单段为空串(调用处静默)。
|
||||
#[test]
|
||||
fn format_fail_list_empty_without_records() {
|
||||
let stats = RunStats::default();
|
||||
let paths: Vec<PathBuf> = Vec::new();
|
||||
assert_eq!(format_fail_list(&stats, &paths), "");
|
||||
assert_eq!(format_fail_categories(&stats, &paths), "");
|
||||
}
|
||||
|
||||
/// 旁路失败行往返:append 后 parse 恢复全部字段;残缺/畸形尾行静默跳过。
|
||||
#[test]
|
||||
fn fail_log_round_trips_and_skips_truncated() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join(format!("oxide_t262_fails_test_{}.log", std::process::id()));
|
||||
append_fail_log(&path, 3, "vm: not callable", "", "x is not callable").expect("追加失败");
|
||||
append_fail_log(&path, 4, "compile: unsupported", "foo", "a\tb\nc").expect("追加失败");
|
||||
let mut content = std::fs::read_to_string(&path).expect("读取失败");
|
||||
content.push_str("5\tvm: x\n"); // 残缺:仅 2 字段(SIGKILL 半写形态)
|
||||
content.push_str("x\tb\tc\td\n"); // 畸形:index 非数字
|
||||
std::fs::write(&path, content).expect("写回失败");
|
||||
let content = std::fs::read_to_string(&path).expect("读取失败");
|
||||
let rows = parse_fail_log(&content);
|
||||
assert_eq!(rows.len(), 2, "残缺/畸形行应被跳过,实际 {rows:?}");
|
||||
assert_eq!(
|
||||
rows[0],
|
||||
(3, "vm: not callable".to_string(), "".to_string(), "x is not callable".to_string())
|
||||
);
|
||||
assert_eq!(rows[1], (4, "compile: unsupported".to_string(), "foo".to_string(), "a b c".to_string()));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user