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

492 lines
22 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.
//! 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::report::extract_not_callable_subkey;
use crate::runner::read_async_output;
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", "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)");
}
}