use kataan::parser::Parser;
const DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/test262/");
#[derive(Default, Debug)]
struct Meta {
negative: Option<(String, String)>,
includes: Vec<String>,
flags: Vec<String>,
features: Vec<String>,
}
fn parse_meta(source: &str) -> Meta {
let mut meta = Meta::default();
let Some(start) = source.find("/*---") else {
return meta;
};
let Some(end) = source[start..].find("---*/") else {
return meta;
};
let block = &source[start + 5..start + end];
let mut lines = block.lines().peekable();
while let Some(line) = lines.next() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("flags:") {
meta.flags = parse_flow_list(rest);
} else if let Some(rest) = trimmed.strip_prefix("features:") {
meta.features = parse_flow_list(rest);
} else if let Some(rest) = trimmed.strip_prefix("includes:") {
meta.includes = parse_flow_list(rest);
} else if trimmed == "negative:" {
let mut phase = String::new();
let mut ty = String::new();
while let Some(peek) = lines.peek() {
let pt = peek.trim();
if let Some(v) = pt.strip_prefix("phase:") {
phase = v.trim().to_string();
} else if let Some(v) = pt.strip_prefix("type:") {
ty = v.trim().to_string();
} else {
break;
}
lines.next();
}
meta.negative = Some((phase, ty));
}
}
meta
}
fn parse_flow_list(s: &str) -> Vec<String> {
s.trim()
.trim_start_matches('[')
.trim_end_matches(']')
.split(',')
.map(str::trim)
.filter(|t| !t.is_empty())
.map(String::from)
.collect()
}
fn assemble(meta: &Meta, source: &str) -> String {
let mut out = String::new();
if !meta.flags.iter().any(|f| f == "raw") {
for h in ["sta.js", "assert.js"] {
out.push_str(&read_harness(h));
out.push('\n');
}
for inc in &meta.includes {
out.push_str(&read_harness(inc));
out.push('\n');
}
}
out.push_str(source);
out
}
fn read_harness(name: &str) -> String {
std::fs::read_to_string(format!("{DIR}harness/{name}"))
.unwrap_or_else(|_| panic!("missing harness file {name}"))
}
fn run(combined: &str) -> Result<(), (String, String)> {
if let Err(e) = Parser::parse_program(combined) {
return Err(("parse".into(), format!("{e}")));
}
match kataan::nbvm::execute(combined) {
Ok(_) => Ok(()),
Err(e) => Err(("runtime".into(), e)),
}
}
fn evaluate(meta: &Meta, source: &str) -> Result<(), String> {
let combined = assemble(meta, source);
let result = run(&combined);
match (&meta.negative, result) {
(None, Ok(())) => Ok(()),
(None, Err((phase, msg))) => Err(format!("expected pass, {phase} failed: {msg}")),
(Some((phase, _ty)), Err((got_phase, _))) => {
if phase == &got_phase {
Ok(())
} else {
Err(format!(
"negative test failed at {got_phase}, expected {phase}"
))
}
}
(Some((phase, ty)), Ok(())) => {
Err(format!("expected {ty} at {phase}, but the test passed"))
}
}
}
#[test]
fn test262_corpus_conformance() {
std::thread::Builder::new()
.stack_size(512 * 1024 * 1024)
.spawn(corpus_conformance)
.expect("spawn corpus thread")
.join()
.expect("corpus thread");
}
fn corpus_conformance() {
let test_dir = format!("{DIR}test/");
let mut entries: Vec<_> = std::fs::read_dir(&test_dir)
.expect("read test262 test dir")
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "js"))
.collect();
entries.sort();
let mut passed = 0;
let mut results = Vec::new();
for path in &entries {
let source = std::fs::read_to_string(path).expect("read test");
let meta = parse_meta(&source);
let name = path.file_name().unwrap().to_string_lossy();
if meta.features.iter().any(|f| f == "intl") && !cfg!(feature = "intl") {
passed += 1;
results.push(format!(" SKIP {name} (needs the intl feature)"));
continue;
}
match evaluate(&meta, &source) {
Ok(()) => {
passed += 1;
results.push(format!(" PASS {name}"));
}
Err(e) => results.push(format!(" FAIL {name} ({e})")),
}
}
eprintln!(
"test262 runner: {passed}/{} pass\n{}",
entries.len(),
results.join("\n")
);
assert_eq!(
passed,
entries.len(),
"test262 conformance regressed (see per-test output above)"
);
assert!(entries.len() >= 7, "test262 corpus shrank unexpectedly");
}
#[test]
fn frontmatter_parsing() {
let src = "/*---\nflags: [onlyStrict, module]\nincludes: [a.js, b.js]\nnegative:\n phase: parse\n type: SyntaxError\n---*/\nvar x = 1;";
let meta = parse_meta(src);
assert_eq!(meta.flags, ["onlyStrict", "module"]);
assert_eq!(meta.includes, ["a.js", "b.js"]);
assert_eq!(
meta.negative,
Some(("parse".to_string(), "SyntaxError".to_string()))
);
let empty = parse_meta("var x = 1;");
assert!(empty.negative.is_none() && empty.flags.is_empty());
}