Files
Zachary D. Rowitsch b70a6bdf73 Update Test262 full suite: promote 602 tests, demote 126 regressions, add per-test timeout
- Promote 602 newly passing tests from RegExp implementation
- Demote 126 regex literal early-error tests (parse-time pattern validation not yet implemented)
- Add 30-second per-test timeout to prevent infinite loops from hanging the suite
- Add JS262_VERBOSE env var for per-test diagnostic output
- Full-suite pass rate: 3706/15431 (24%, up from 3230)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 10:57:38 -05:00

715 lines
24 KiB
Rust

use crate::types::{ExpectedError, Js262Case, Js262Mode, Js262Outcome, Js262Status, Step};
use js::{JsEngine, JsError, OutputSink};
use std::fs;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use web_api::scheduling::Clock;
use web_api::WebApiFacade;
/// Per-test timeout. Tests that take longer are killed and reported as failures.
const TEST_TIMEOUT: Duration = Duration::from_secs(30);
const TEST262_STA: &str = include_str!("../external/js262/test262/harness/sta.js");
const TEST262_ASSERT: &str = include_str!("../external/js262/test262/harness/assert.js");
/// Load a Test262 harness include file by name.
///
/// For "sta.js" and "assert.js", returns the compile-time embedded constants.
/// For anything else, reads from the upstream clone or falls back to vendored harness.
fn load_harness_include(name: &str) -> Result<String, String> {
match name {
"sta.js" => Ok(TEST262_STA.to_string()),
"assert.js" => Ok(TEST262_ASSERT.to_string()),
_ => {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
// Try upstream first
let upstream = manifest_dir
.join("tests/external/js262/upstream/harness")
.join(name);
if upstream.exists() {
return fs::read_to_string(&upstream).map_err(|e| {
format!("failed to read harness include {}: {e}", upstream.display())
});
}
// Fallback to vendored harness
let vendored = manifest_dir
.join("tests/external/js262/test262/harness")
.join(name);
if vendored.exists() {
return fs::read_to_string(&vendored).map_err(|e| {
format!("failed to read harness include {}: {e}", vendored.display())
});
}
Err(format!(
"harness include '{name}' not found in upstream or vendored harness directories"
))
}
}
}
/// A capture sink that can be shared between the engine and the test runner.
struct SharedCaptureSink {
lines: Arc<Mutex<Vec<String>>>,
}
impl SharedCaptureSink {
fn new(lines: Arc<Mutex<Vec<String>>>) -> Self {
Self { lines }
}
}
impl OutputSink for SharedCaptureSink {
fn write_line(&mut self, message: &str) {
self.lines.lock().unwrap().push(message.to_string());
}
}
pub fn run_case(case: &Js262Case) -> Js262Outcome {
if let Js262Status::Skip { reason } = &case.status {
return Js262Outcome::Skipped {
reason: reason.clone(),
};
}
let script = match fs::read_to_string(&case.input) {
Ok(v) => v,
Err(e) => {
return Js262Outcome::Fail {
reason: format!("failed to read input {}: {e}", case.input.display()),
};
}
};
// Run the actual test in a separate thread with a timeout to catch infinite loops.
let case_clone = case.clone();
let (tx, rx) = mpsc::channel();
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(move || {
let outcome = match case_clone.mode {
Js262Mode::Script => run_script_case(&case_clone, &script),
Js262Mode::Dom => run_dom_case(&case_clone, &script),
Js262Mode::Test262 => run_test262_case(&case_clone, &script),
};
let _ = tx.send(outcome);
})
.expect("failed to spawn test runner thread");
match rx.recv_timeout(TEST_TIMEOUT) {
Ok(outcome) => {
let _ = handle.join();
outcome
}
Err(mpsc::RecvTimeoutError::Timeout) => {
// Thread is stuck — abandon it (it will be cleaned up when the process exits)
Js262Outcome::Fail {
reason: format!("test timed out after {}s", TEST_TIMEOUT.as_secs()),
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => Js262Outcome::Fail {
reason: "test thread panicked".to_string(),
},
}
}
fn run_script_case(case: &Js262Case, script: &str) -> Js262Outcome {
let captured = Arc::new(Mutex::new(Vec::new()));
let sink = SharedCaptureSink::new(Arc::clone(&captured));
let mut engine = JsEngine::with_output(Box::new(sink));
engine.prime_runtime().unwrap();
let result = engine.execute(script);
if let Some(expected_error) = case.expected_error {
return match result {
Ok(_) => Js262Outcome::Fail {
reason: format!("expected {:?} error but script succeeded", expected_error),
},
Err(err) => {
if error_matches_class(&err, expected_error) {
Js262Outcome::Pass
} else {
Js262Outcome::Fail {
reason: format!("expected {:?} error but got: {err}", expected_error),
}
}
}
};
}
// Output comparison
if let Err(err) = result {
return Js262Outcome::Fail {
reason: format!("script failed: {err}"),
};
}
compare_output_from_captured(case, &captured)
}
fn run_dom_case(case: &Js262Case, script: &str) -> Js262Outcome {
let captured = Arc::new(Mutex::new(Vec::new()));
let sink = SharedCaptureSink::new(Arc::clone(&captured));
let engine = JsEngine::with_output(Box::new(sink));
let mut facade = WebApiFacade::new_with_engine(engine, Clock::new_virtual());
// Set up minimal DOM: root > html > body#body > div#target
{
let doc = facade.document_mut();
let root = doc.root().unwrap();
let html = doc.create_element("html");
doc.append_child(root, html);
let body = doc.create_element("body");
doc.set_attribute(body, "id", "body");
doc.append_child(html, body);
let target = doc.create_element("div");
doc.set_attribute(target, "id", "target");
doc.append_child(body, target);
}
if let Err(e) = facade.bootstrap() {
return Js262Outcome::Fail {
reason: format!("bootstrap failed: {e}"),
};
}
if let Err(e) = facade.execute_script(script) {
if let Some(expected_error) = case.expected_error {
// Check error class via string matching since SharedResult wraps JsError
let err_str = e.to_string();
let class_matches = match expected_error {
ExpectedError::Parse => err_str.contains("ParseError"),
ExpectedError::Reference => err_str.contains("ReferenceError"),
ExpectedError::Type => err_str.contains("TypeError"),
ExpectedError::Runtime => err_str.contains("RuntimeError"),
};
return if class_matches {
Js262Outcome::Pass
} else {
Js262Outcome::Fail {
reason: format!("expected {:?} error but got: {err_str}", expected_error),
}
};
}
return Js262Outcome::Fail {
reason: format!("script execution failed: {e}"),
};
}
// Execute steps
for step in &case.steps {
match step {
Step::Tick => {
if let Err(e) = facade.tick() {
return Js262Outcome::Fail {
reason: format!("tick failed: {e}"),
};
}
}
Step::AdvanceMs(ms) => {
facade.task_queue_mut().clock_mut().advance(*ms);
}
Step::DispatchClick(element_id) => {
let node_id = facade
.document()
.get_element_by_id(element_id)
.unwrap_or_else(|| {
panic!("dispatch_click: element with id '{element_id}' not found in DOM")
});
if let Err(e) = facade.dispatch_click(node_id) {
return Js262Outcome::Fail {
reason: format!("dispatch_click failed: {e}"),
};
}
}
}
}
// Compare output
compare_output_from_captured(case, &captured)
}
fn compare_output_from_captured(
case: &Js262Case,
captured: &Arc<Mutex<Vec<String>>>,
) -> Js262Outcome {
let expected_path = match &case.expected_output {
Some(p) => p,
None => {
// No expected output — pass if we got here without errors
return Js262Outcome::Pass;
}
};
let expected = match fs::read_to_string(expected_path) {
Ok(v) => v,
Err(e) => {
return Js262Outcome::Fail {
reason: format!(
"failed to read expected output {}: {e}",
expected_path.display()
),
};
}
};
let lines = captured.lock().unwrap();
let actual = lines.join("\n");
let actual = if actual.is_empty() {
String::new()
} else {
format!("{actual}\n")
};
let expected = normalize_output(&expected);
let actual = normalize_output(&actual);
if actual == expected {
Js262Outcome::Pass
} else {
Js262Outcome::Fail {
reason: format!(
"output mismatch for '{}':\n--- expected ---\n{expected}--- actual ---\n{actual}",
case.id
),
}
}
}
fn normalize_output(s: &str) -> String {
let mut out = String::new();
for line in s.replace("\r\n", "\n").lines() {
out.push_str(line.trim_end());
out.push('\n');
}
out
}
fn error_matches_class(err: &JsError, expected: ExpectedError) -> bool {
match expected {
ExpectedError::Parse => err.is_parse_error(),
ExpectedError::Reference => err.is_reference_error(),
ExpectedError::Type => err.is_type_error(),
ExpectedError::Runtime => err.is_runtime_error(),
}
}
fn is_test262_assertion_error(err: &JsError) -> bool {
if let JsError::UncaughtThrow { message } = err {
message.starts_with("Test262Error:")
} else {
false
}
}
fn run_test262_case(case: &Js262Case, script: &str) -> Js262Outcome {
// Prepend harness includes, then the test script
let mut combined = String::new();
for include_name in &case.includes {
match load_harness_include(include_name) {
Ok(src) => {
combined.push_str(&src);
combined.push('\n');
}
Err(e) => {
return Js262Outcome::Fail {
reason: format!("failed to load harness include: {e}"),
};
}
}
}
combined.push_str(script);
let captured = Arc::new(Mutex::new(Vec::new()));
let sink = SharedCaptureSink::new(Arc::clone(&captured));
let mut engine = JsEngine::with_output(Box::new(sink));
engine.prime_runtime().unwrap();
let result = engine.execute(&combined);
if let Some(expected_error) = case.expected_error {
// Negative test: we expect a specific error class
return match result {
Ok(_) => Js262Outcome::Fail {
reason: format!(
"negative test expected {:?} error but script succeeded",
expected_error
),
},
Err(ref err) if is_test262_assertion_error(err) => Js262Outcome::Fail {
reason: format!(
"negative test got Test262 assertion error instead of {:?}: {err}",
expected_error
),
},
Err(ref err) => {
if error_matches_class(err, expected_error) {
Js262Outcome::Pass
} else {
Js262Outcome::Fail {
reason: format!(
"negative test expected {:?} error but got: {err}",
expected_error
),
}
}
}
};
}
// Positive test: script must complete without error
match result {
Ok(_) => Js262Outcome::Pass,
Err(ref err) if is_test262_assertion_error(err) => Js262Outcome::Fail {
reason: format!("assertion failed: {err}"),
},
Err(err) => Js262Outcome::Fail {
reason: format!("unexpected error: {err}"),
},
}
}
pub fn enforce_case_policy(case: &Js262Case, outcome: Js262Outcome) -> Result<(), String> {
match (&case.status, outcome) {
(Js262Status::Pass, Js262Outcome::Pass) => Ok(()),
(Js262Status::Pass, Js262Outcome::Fail { reason }) => Err(reason),
(Js262Status::Pass, Js262Outcome::Skipped { reason }) => {
Err(format!("case '{}' unexpectedly skipped: {reason}", case.id))
}
(Js262Status::KnownFail { .. }, Js262Outcome::Fail { .. }) => Ok(()),
(Js262Status::KnownFail { .. }, Js262Outcome::Pass) => Err(format!(
"case '{}' is marked known_fail but now passes; promote it to pass in manifest",
case.id
)),
(Js262Status::KnownFail { .. }, Js262Outcome::Skipped { reason }) => Err(format!(
"case '{}' marked known_fail unexpectedly skipped: {reason}",
case.id
)),
(Js262Status::Skip { .. }, Js262Outcome::Skipped { .. }) => Ok(()),
(Js262Status::Skip { .. }, Js262Outcome::Pass) => Err(format!(
"case '{}' marked skip but unexpectedly passed",
case.id
)),
(Js262Status::Skip { .. }, Js262Outcome::Fail { reason }) => Err(format!(
"case '{}' marked skip but unexpectedly executed and failed: {reason}",
case.id
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{ExpectedError, Js262Mode, Js262Status};
use std::io::Write;
/// Build a minimal `Js262Case` backed by a temporary JS file containing `script`.
fn dom_case_with_error(
script: &str,
expected_error: ExpectedError,
) -> (Js262Case, tempfile::NamedTempFile) {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(script.as_bytes()).unwrap();
let case = Js262Case {
id: "test-dom-error".into(),
input: tmp.path().to_path_buf(),
mode: Js262Mode::Dom,
expected_output: None,
expected_error: Some(expected_error),
status: Js262Status::Pass,
feature: "errors".into(),
steps: vec![],
includes: vec![],
flags: vec![],
};
(case, tmp)
}
// --- DOM-mode expected_error: correct class → Pass ---
#[test]
fn dom_mode_reference_error_with_matching_class_is_pass() {
// A script that throws a ReferenceError when expected_error = Reference
// must produce Js262Outcome::Pass.
let (case, _tmp) = dom_case_with_error("undeclaredVar;", ExpectedError::Reference);
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Pass),
"expected Pass for matching ReferenceError, got: {outcome:?}"
);
}
#[test]
fn dom_mode_type_error_with_matching_class_is_pass() {
// A script that throws a TypeError (const reassignment) when expected_error
// = Type must produce Js262Outcome::Pass.
let (case, _tmp) = dom_case_with_error("const c = 1; c = 2;", ExpectedError::Type);
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Pass),
"expected Pass for matching TypeError, got: {outcome:?}"
);
}
// --- DOM-mode expected_error: wrong class → Fail (not vacuous Pass) ---
#[test]
fn dom_mode_wrong_error_class_produces_fail_not_pass() {
// A script that throws a ReferenceError but expected_error = Type must
// produce Js262Outcome::Fail. The old vacuous-pass bug would have
// returned Pass here.
let (case, _tmp) = dom_case_with_error("undeclaredVar;", ExpectedError::Type);
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Fail { .. }),
"expected Fail when error class does not match, got: {outcome:?}"
);
}
#[test]
fn dom_mode_wrong_error_class_fail_reason_mentions_expected_and_actual() {
// The Fail reason must identify both the expected class and the actual
// error so a developer can diagnose the mismatch.
let (case, _tmp) = dom_case_with_error("undeclaredVar;", ExpectedError::Type);
let outcome = run_case(&case);
match outcome {
Js262Outcome::Fail { reason } => {
assert!(
reason.contains("Type"),
"reason should mention expected 'Type', got: {reason}"
);
assert!(
reason.contains("ReferenceError"),
"reason should mention actual 'ReferenceError', got: {reason}"
);
}
other => panic!("expected Fail, got: {other:?}"),
}
}
// --- DOM-mode: no expected_error and script succeeds → Pass ---
#[test]
fn dom_mode_no_expected_error_succeeds() {
// A script with no expected_error that succeeds must produce Pass.
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(b"var x = 1;").unwrap();
let case = Js262Case {
id: "test-dom-ok".into(),
input: tmp.path().to_path_buf(),
mode: Js262Mode::Dom,
expected_output: None,
expected_error: None,
status: Js262Status::Pass,
feature: "basics".into(),
steps: vec![],
includes: vec![],
flags: vec![],
};
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Pass),
"expected Pass for succeeding DOM script, got: {outcome:?}"
);
}
// --- enforce_case_policy: all branches ---
#[test]
fn enforce_case_policy_pass_status_pass_outcome_is_ok() {
let case = Js262Case {
id: "x".into(),
input: std::path::PathBuf::new(),
mode: Js262Mode::Script,
expected_output: None,
expected_error: None,
status: Js262Status::Pass,
feature: "f".into(),
steps: vec![],
includes: vec![],
flags: vec![],
};
assert!(enforce_case_policy(&case, Js262Outcome::Pass).is_ok());
}
#[test]
fn enforce_case_policy_pass_status_fail_outcome_is_err() {
let case = Js262Case {
id: "x".into(),
input: std::path::PathBuf::new(),
mode: Js262Mode::Script,
expected_output: None,
expected_error: None,
status: Js262Status::Pass,
feature: "f".into(),
steps: vec![],
includes: vec![],
flags: vec![],
};
assert!(enforce_case_policy(
&case,
Js262Outcome::Fail {
reason: "oops".into()
}
)
.is_err());
}
#[test]
fn enforce_case_policy_known_fail_status_fail_outcome_is_ok() {
let case = Js262Case {
id: "x".into(),
input: std::path::PathBuf::new(),
mode: Js262Mode::Script,
expected_output: None,
expected_error: None,
status: Js262Status::KnownFail {
reason: "not yet".into(),
},
feature: "f".into(),
steps: vec![],
includes: vec![],
flags: vec![],
};
assert!(enforce_case_policy(
&case,
Js262Outcome::Fail {
reason: "still broken".into()
}
)
.is_ok());
}
#[test]
fn enforce_case_policy_known_fail_status_pass_outcome_is_err() {
let case = Js262Case {
id: "x".into(),
input: std::path::PathBuf::new(),
mode: Js262Mode::Script,
expected_output: None,
expected_error: None,
status: Js262Status::KnownFail {
reason: "not yet".into(),
},
feature: "f".into(),
steps: vec![],
includes: vec![],
flags: vec![],
};
let err = enforce_case_policy(&case, Js262Outcome::Pass).unwrap_err();
assert!(
err.contains("promote"),
"error should mention promoting the case, got: {err}"
);
}
#[test]
fn enforce_case_policy_skip_status_skipped_outcome_is_ok() {
let case = Js262Case {
id: "x".into(),
input: std::path::PathBuf::new(),
mode: Js262Mode::Script,
expected_output: None,
expected_error: None,
status: Js262Status::Skip {
reason: "needs feature".into(),
},
feature: "f".into(),
steps: vec![],
includes: vec![],
flags: vec![],
};
assert!(enforce_case_policy(
&case,
Js262Outcome::Skipped {
reason: "needs feature".into()
}
)
.is_ok());
}
// --- Test262 runner tests ---
fn test262_case(
script: &str,
expected_error: Option<ExpectedError>,
) -> (Js262Case, tempfile::NamedTempFile) {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(script.as_bytes()).unwrap();
let case = Js262Case {
id: "test-t262".into(),
input: tmp.path().to_path_buf(),
mode: Js262Mode::Test262,
expected_output: None,
expected_error,
status: Js262Status::Pass,
feature: "test".into(),
steps: vec![],
includes: vec!["sta.js".into(), "assert.js".into()],
flags: vec![],
};
(case, tmp)
}
#[test]
fn test262_positive_pass() {
let (case, _tmp) = test262_case("assert.sameValue(1, 1);", None);
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Pass),
"expected Pass, got: {outcome:?}"
);
}
#[test]
fn test262_positive_assertion_failure() {
let (case, _tmp) = test262_case("assert.sameValue(1, 2);", None);
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Fail { .. }),
"expected Fail, got: {outcome:?}"
);
}
#[test]
fn test262_positive_unexpected_error() {
let (case, _tmp) = test262_case("undeclaredVar;", None);
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Fail { .. }),
"expected Fail for unexpected error, got: {outcome:?}"
);
}
#[test]
fn test262_negative_matching_error() {
let (case, _tmp) = test262_case("undeclaredVar;", Some(ExpectedError::Reference));
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Pass),
"expected Pass for matching error class, got: {outcome:?}"
);
}
#[test]
fn test262_negative_wrong_error() {
let (case, _tmp) = test262_case("undeclaredVar;", Some(ExpectedError::Type));
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Fail { .. }),
"expected Fail for wrong error class, got: {outcome:?}"
);
}
#[test]
fn test262_negative_no_error() {
let (case, _tmp) = test262_case("var x = 1;", Some(ExpectedError::Reference));
let outcome = run_case(&case);
assert!(
matches!(outcome, Js262Outcome::Fail { .. }),
"expected Fail when negative test doesn't throw, got: {outcome:?}"
);
}
}