test: 适配 test262 测试套件

This commit is contained in:
2026-06-06 18:51:54 +08:00
parent bf3913e183
commit b0b55abbe1
6 changed files with 532 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "tests/test262"]
path = tests/test262
url = https://github.com/tc39/test262.git
Generated
+61
View File
@@ -803,6 +803,19 @@ dependencies = [
"oxc_syntax",
]
[[package]]
name = "oxide_test262"
version = "0.1.0"
dependencies = [
"oxide_compiler",
"oxide_kernel",
"oxide_parser",
"oxide_vm",
"serde",
"serde_yaml",
"walkdir",
]
[[package]]
name = "oxide_types"
version = "0.1.0"
@@ -1111,6 +1124,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -1136,6 +1158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
@@ -1171,6 +1194,19 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "siphasher"
version = "1.0.3"
@@ -1304,6 +1340,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -1325,6 +1367,16 @@ dependencies = [
"libc",
]
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
@@ -1393,6 +1445,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
+1
View File
@@ -8,6 +8,7 @@ members = [
"crates/oxide_kernel",
"crates/oxide_api",
"crates/oxide_cli",
"crates/oxide_test262",
]
[workspace.package]
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "oxide_test262"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "OxideJS - test262 ECMAScript test suite runner"
[[bin]]
name = "test262-runner"
path = "src/main.rs"
[dependencies]
oxide_compiler = { path = "../oxide_compiler" }
oxide_vm = { path = "../oxide_vm" }
oxide_parser = { path = "../oxide_parser" }
oxide_kernel = { path = "../oxide_kernel" }
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
walkdir = "2"
+446
View File
@@ -0,0 +1,446 @@
use oxide_compiler::compiler::Compiler;
use oxide_vm::vm::Vm;
use serde::Deserialize;
use std::path::{Path, PathBuf};
use std::time::Duration;
use walkdir::WalkDir;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Negative {
phase: String,
#[serde(rename = "type")]
error_type: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct TestMeta {
#[serde(default)]
description: String,
#[serde(default)]
flags: Vec<String>,
#[serde(default)]
includes: Vec<String>,
#[serde(default)]
features: Vec<String>,
#[serde(default)]
negative: Option<Negative>,
#[serde(default)]
es5id: String,
#[serde(default)]
es6id: String,
#[serde(default)]
esid: String,
}
#[derive(Debug)]
enum TestOutcome {
Pass(String),
Fail(String),
Skip(String),
}
#[derive(Debug)]
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,
}
}
}
fn parse_meta(source: &str) -> Option<TestMeta> {
let header_start = source.find("/*---")?;
let header = &source[header_start..];
let header = header.strip_prefix("/*---")?;
let end = header.find("---*/")?;
let yaml_body = &header[..end];
let yaml_body = yaml_body.trim();
serde_yaml::from_str::<TestMeta>(yaml_body).ok()
}
fn strip_meta(source: &str) -> &str {
if let Some(pos) = source.find("---*/") {
return source[pos + 5..].trim_start();
}
source
}
#[derive(Default)]
struct RunStats {
pass: usize,
fail: usize,
skip: usize,
total_ms: u64,
}
fn is_skipped(meta: &TestMeta) -> Option<String> {
for flag in &meta.flags {
match flag.as_str() {
"module" => return Some("module tests excluded".into()),
"async" => return Some("async tests excluded".into()),
"raw" => return Some("raw tests excluded".into()),
"onlyStrict" => return Some("strict-only excluded".into()),
_ => {}
}
}
if !meta.includes.is_empty() {
return Some(format!(
"requires harness includes: {}",
meta.includes.join(", ")
));
}
let excluded_features = [
"Proxy",
"Symbol",
"Symbol.match",
"BigInt",
"generators",
"generator",
"async-functions",
"default-parameters",
"destructuring-binding",
"destructuring",
"arrow-function",
"rest-parameters",
"spread",
"class",
"super",
"WeakMap",
"WeakSet",
"WeakRef",
"Reflect",
"Intl",
"Promise",
"TypedArray",
"DataView",
"ArrayBuffer",
"SharedArrayBuffer",
"Atomics",
"module",
"dynamic-import",
"tail-call-optimization",
"regexp-named-groups",
"regexp-lookbehind",
"regexp-unicode-property-escapes",
"regexp-dotall",
"json-superset",
"for-of",
"cross-realm",
"new.target",
"well-formed-json-stringify",
"optional-chaining",
];
for feat in &meta.features {
if excluded_features.contains(&feat.as_str()) {
return Some(format!("excluded feature: {feat}"));
}
}
if meta.description.contains("generator")
|| meta.description.contains("async")
|| meta.description.contains("eval ")
{
return Some("description matches excluded pattern".into());
}
None
}
fn run_test(path: &Path, source: &str, meta: &TestMeta) -> TestResult {
let start = std::time::Instant::now();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
run_test_inner(path, source, meta)
}));
match result {
Ok(r) => r,
Err(_panic) => {
let dur = start.elapsed().as_millis() as u64;
TestResult::fail(path.to_path_buf(), dur, "engine panic (unsupported feature)")
}
}
}
fn run_test_inner(path: &Path, source: &str, meta: &TestMeta) -> TestResult {
let start = std::time::Instant::now();
let code = strip_meta(source);
let alloc = oxide_parser::Allocator::default();
let program = match oxide_parser::parse(&alloc, code) {
Ok(p) => p,
Err(errs) => {
let dur = start.elapsed().as_millis() as u64;
let msg = format!("parse error: {}", errs[0].message);
if meta.negative.is_some() {
return TestResult::pass(path.to_path_buf(), dur, msg);
}
return TestResult::fail(path.to_path_buf(), dur, msg);
}
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => {
let dur = start.elapsed().as_millis() as u64;
let msg = format!("compile error: {e}");
if meta.negative.is_some() {
return TestResult::pass(path.to_path_buf(), dur, msg);
}
if e.contains("not yet implemented")
|| e.contains("not supported")
|| e.contains("compound assignment")
|| e.contains("is not defined")
|| e.contains("FunctionDeclaration")
|| e.contains("FunctionExpression")
|| e.contains("ArrowFunctionExpression")
|| e.contains("unsupported expression")
|| e.contains("unsupported statement")
|| e.contains("NewExpression")
|| e.contains("CallExpression")
|| e.contains("UpdateExpression")
|| e.contains("destructuring")
|| e.contains("SpreadElement")
{
return TestResult::skip(path.to_path_buf(), msg);
}
return TestResult::fail(path.to_path_buf(), dur, msg);
}
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => {
let dur = start.elapsed().as_millis() as u64;
if meta.negative.is_some() {
let neg = meta.negative.as_ref().unwrap();
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) => {
let dur = start.elapsed().as_millis() as u64;
if meta.negative.is_some() {
let neg = meta.negative.as_ref().unwrap();
if e.contains("TypeError") && neg.error_type == "TypeError" {
return TestResult::pass(path.to_path_buf(), dur, format!("expected: {e}"));
}
if e.contains("ReferenceError") && neg.error_type == "ReferenceError" {
return TestResult::pass(path.to_path_buf(), dur, format!("expected: {e}"));
}
if e.contains("SyntaxError") && neg.error_type == "SyntaxError" {
return TestResult::pass(path.to_path_buf(), dur, format!("expected: {e}"));
}
if e.contains(&neg.error_type) {
return TestResult::pass(path.to_path_buf(), dur, format!("expected: {e}"));
}
TestResult::fail(
path.to_path_buf(),
dur,
format!("expected {} error, got: {e}", neg.error_type),
)
} else if e.contains("not yet implemented")
|| e.contains("not supported")
|| e.contains("unsupported")
{
TestResult::skip(path.to_path_buf(), format!("vm: {e}"))
} else {
TestResult::fail(path.to_path_buf(), dur, format!("vm error: {e}"))
}
}
}
}
fn discover_tests(test262_root: &Path) -> Vec<PathBuf> {
WalkDir::new(test262_root)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "js"))
.map(|e| e.path().to_path_buf())
.collect()
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let test262_root = if args.len() > 1 {
PathBuf::from(&args[1])
} 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() {
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 <tag>\n\
Or pass path as argument: cargo run -- <path-to-test262/test>",
test262_root.display()
);
std::process::exit(1);
}
let filter = if args.len() > 2 {
Some(args[2].clone())
} else {
None
};
eprintln!("discovering tests in: {}", test262_root.display());
let paths = discover_tests(&test262_root);
eprintln!("found {} test files", paths.len());
let mut stats = RunStats::default();
let mut results: Vec<TestResult> = Vec::new();
let total = paths.len();
for (i, path) in paths.iter().enumerate() {
if i % 100 == 0 {
eprintln!(
" progress: {}/{} ({}% pass={} fail={} skip={})",
i,
total,
i * 100 / total,
stats.pass,
stats.fail,
stats.skip
);
}
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
results.push(TestResult::skip(
path.clone(),
format!("read error: {e}"),
));
stats.skip += 1;
continue;
}
};
let meta = match parse_meta(&source) {
Some(m) => m,
None => {
results.push(TestResult::skip(
path.clone(),
String::from("no YAML metadata"),
));
stats.skip += 1;
continue;
}
};
if let Some(filter_str) = &filter {
let path_str = path.to_string_lossy();
if !path_str.contains(filter_str.as_str()) {
results.push(TestResult::skip(path.clone(), "filtered".into()));
stats.skip += 1;
continue;
}
}
if let Some(reason) = is_skipped(&meta) {
results.push(TestResult::skip(path.clone(), reason));
stats.skip += 1;
continue;
}
let result = run_test(path, &source, &meta);
match &result.outcome {
TestOutcome::Pass(_) => stats.pass += 1,
TestOutcome::Fail(_) => stats.fail += 1,
TestOutcome::Skip(_) => stats.skip += 1,
}
stats.total_ms += result.duration_ms;
results.push(result);
}
eprintln!();
let mut pass_count = 0;
let mut fail_count = 0;
let mut skip_count = 0;
for r in &results {
match &r.outcome {
TestOutcome::Pass(msg) => {
pass_count += 1;
let rel = r.path.strip_prefix(&test262_root).unwrap_or(&r.path);
println!("PASS [{:>7}ms] {} -- {msg}", r.duration_ms, rel.display());
}
TestOutcome::Fail(msg) => {
fail_count += 1;
let rel = r.path.strip_prefix(&test262_root).unwrap_or(&r.path);
println!("FAIL [{:>7}ms] {} -- {msg}", r.duration_ms, rel.display());
}
TestOutcome::Skip(_msg) => {
skip_count += 1;
}
}
}
println!();
println!("═══════════════════════════════════════");
println!(" test262 results");
println!("═══════════════════════════════════════");
println!(" total : {}", results.len());
println!(" pass : {pass_count}");
println!(" fail : {fail_count}");
println!(" skip : {skip_count}");
println!(" time : {:?}", Duration::from_millis(stats.total_ms));
println!(
" pass% : {:.1}% (of ran: {:.1}%)",
pass_count as f64 / results.len() as f64 * 100.0,
if pass_count + fail_count > 0 {
pass_count as f64 / (pass_count + fail_count) as f64 * 100.0
} else {
0.0
}
);
println!("═══════════════════════════════════════");
if fail_count > 0 {
std::process::exit(1);
}
}
Submodule
+1
Submodule tests/test262 added at 05bb032907