Files
rust_browser/tests/js262_harness/runner.rs
Zachary D. Rowitsch 9e0bbe37fc
All checks were successful
ci / fast (linux) (push) Successful in 6m19s
Add Test262 integration (Phase A): function properties, harness, 50 real tests
Add function property support to the JS engine (fn.prop = val, fn.prop,
fn.method()) enabling the Test262 assert harness pattern. Implement Test262
execution mode in the JS262 harness with simplified sta.js/assert.js shims.
Vendor 50 real tc39/test262 tests via curation script (35 pass, 15 known_fail).

Engine changes:
- JsObject: add Debug impl and has() method for presence checks
- JsFunction: add properties field (Rc-shared JsObject)
- Interpreter: handle function property get/set/call/compound-assign/update
  with user-property-over-builtin precedence

Harness changes:
- Add Test262 variant to Js262Mode with relaxed manifest validation
- Implement run_test262_case with harness prepending and error classification
- Create sta.js (Test262Error shim) and assert.js (assert.sameValue etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:28:47 -05:00

628 lines
21 KiB
Rust

use crate::types::{ExpectedError, Js262Case, Js262Mode, Js262Outcome, Js262Status, Step};
use js::{JsEngine, JsError, OutputSink};
use std::fs;
use std::sync::{Arc, Mutex};
use web_api::scheduling::Clock;
use web_api::WebApiFacade;
const TEST262_STA: &str = include_str!("../external/js262/test262/harness/sta.js");
const TEST262_ASSERT: &str = include_str!("../external/js262/test262/harness/assert.js");
/// 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()),
};
}
};
match case.mode {
Js262Mode::Script => run_script_case(case, &script),
Js262Mode::Dom => run_dom_case(case, &script),
Js262Mode::Test262 => run_test262_case(case, &script),
}
}
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 files: sta.js first, then assert.js, then the test script
let combined = format!("{TEST262_STA}\n{TEST262_ASSERT}\n{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![],
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![],
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![],
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![],
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![],
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![],
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![],
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![],
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:?}"
);
}
}