Files
rust_browser/tests/js262_harness/manifest.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

453 lines
14 KiB
Rust

use crate::types::{ExpectedError, Js262Case, Js262Mode, Js262Status, Step};
use std::fs;
use std::path::{Path, PathBuf};
pub fn load_manifest(root: &Path) -> Result<Vec<Js262Case>, String> {
let manifest_path = root.join("js262_manifest.toml");
let content = fs::read_to_string(&manifest_path)
.map_err(|e| format!("failed to read {}: {e}", manifest_path.display()))?;
parse_manifest(&content)
.and_then(validate_cases)
.map(|cases| {
cases
.into_iter()
.map(|case| Js262Case {
input: root.join(case.input),
expected_output: case.expected_output.map(|p| root.join(p)),
..case
})
.collect()
})
}
pub fn parse_manifest(input: &str) -> Result<Vec<Js262Case>, String> {
let mut cases = Vec::new();
let mut current: Option<CaseBuilder> = None;
for (idx, raw_line) in input.lines().enumerate() {
let line_no = idx + 1;
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line == "[[case]]" {
if let Some(builder) = current.take() {
cases.push(builder.build()?);
}
current = Some(CaseBuilder::default());
continue;
}
let builder = current
.as_mut()
.ok_or_else(|| format!("line {line_no}: key/value outside [[case]] block"))?;
let (key, value) = parse_key_value(line)
.ok_or_else(|| format!("line {line_no}: invalid key/value syntax"))?;
match key {
"id" => builder.id = Some(parse_string(value, line_no)?),
"input" => builder.input = Some(PathBuf::from(parse_string(value, line_no)?)),
"mode" => {
let mode = parse_string(value, line_no)?;
builder.mode = Some(match mode.as_str() {
"script" => Js262Mode::Script,
"dom" => Js262Mode::Dom,
"test262" => Js262Mode::Test262,
_ => return Err(format!("line {line_no}: unknown mode '{mode}'")),
});
}
"expected_output" => {
builder.expected_output = Some(PathBuf::from(parse_string(value, line_no)?));
}
"expected_error" => {
let err_class = parse_string(value, line_no)?;
builder.expected_error = Some(match err_class.as_str() {
"parse" => ExpectedError::Parse,
"reference" => ExpectedError::Reference,
"type" => ExpectedError::Type,
"runtime" => ExpectedError::Runtime,
_ => {
return Err(format!(
"line {line_no}: unknown expected_error '{err_class}'"
))
}
});
}
"status" => builder.status = Some(parse_string(value, line_no)?),
"reason" => builder.reason = Some(parse_string(value, line_no)?),
"feature" => builder.feature = Some(parse_string(value, line_no)?),
"steps" => builder.steps = Some(parse_string(value, line_no)?),
"flags" => builder.flags = parse_string_array(value, line_no)?,
_ => return Err(format!("line {line_no}: unknown key '{key}'")),
}
}
if let Some(builder) = current.take() {
cases.push(builder.build()?);
}
if cases.is_empty() {
return Err("manifest is empty".to_string());
}
Ok(cases)
}
fn validate_cases(cases: Vec<Js262Case>) -> Result<Vec<Js262Case>, String> {
let mut ids = std::collections::BTreeSet::new();
for case in &cases {
if !ids.insert(case.id.clone()) {
return Err(format!(
"duplicate case id '{}'; ids must be unique",
case.id
));
}
if let Js262Status::KnownFail { reason } | Js262Status::Skip { reason } = &case.status {
if reason.trim().is_empty() {
return Err(format!("case '{}' has empty reason", case.id));
}
}
match case.status {
Js262Status::Pass => {
// Test262 pass = script completes without throwing; no expected_output needed.
if case.mode != Js262Mode::Test262 {
if case.expected_output.is_none() && case.expected_error.is_none() {
return Err(format!(
"case '{}' with status=pass must define expected_output or expected_error",
case.id
));
}
if case.expected_output.is_some() && case.expected_error.is_some() {
return Err(format!(
"case '{}' must not define both expected_output and expected_error",
case.id
));
}
}
}
Js262Status::KnownFail { .. } | Js262Status::Skip { .. } => {}
}
if (case.mode == Js262Mode::Script || case.mode == Js262Mode::Test262)
&& !case.steps.is_empty()
{
return Err(format!(
"case '{}' in {} mode must not define steps",
case.id,
match case.mode {
Js262Mode::Script => "script",
Js262Mode::Test262 => "test262",
_ => unreachable!(),
}
));
}
}
Ok(cases)
}
fn parse_steps(steps_str: &str) -> Result<Vec<Step>, String> {
let mut result = Vec::new();
for part in steps_str.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if part == "tick" {
result.push(Step::Tick);
} else if let Some(ms) = part.strip_prefix("advance_ms:") {
let ms: u64 = ms
.parse()
.map_err(|_| format!("invalid advance_ms value: '{ms}'"))?;
result.push(Step::AdvanceMs(ms));
} else if let Some(id) = part.strip_prefix("dispatch_click:") {
result.push(Step::DispatchClick(id.to_string()));
} else {
return Err(format!("unknown step: '{part}'"));
}
}
Ok(result)
}
fn parse_key_value(line: &str) -> Option<(&str, &str)> {
let mut parts = line.splitn(2, '=');
let key = parts.next()?.trim();
let value = parts.next()?.trim();
if key.is_empty() || value.is_empty() {
return None;
}
Some((key, value))
}
/// Parse a quoted string value. Does not handle escape sequences (e.g., `\"`).
fn parse_string(value: &str, line_no: usize) -> Result<String, String> {
if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
return Ok(value[1..value.len() - 1].to_string());
}
Err(format!("line {line_no}: expected quoted string"))
}
fn parse_string_array(value: &str, line_no: usize) -> Result<Vec<String>, String> {
if !value.starts_with('[') || !value.ends_with(']') {
return Err(format!("line {line_no}: expected array syntax [..]"));
}
let inner = value[1..value.len() - 1].trim();
if inner.is_empty() {
return Ok(Vec::new());
}
inner
.split(',')
.map(|entry| parse_string(entry.trim(), line_no))
.collect()
}
#[derive(Default)]
struct CaseBuilder {
id: Option<String>,
input: Option<PathBuf>,
mode: Option<Js262Mode>,
expected_output: Option<PathBuf>,
expected_error: Option<ExpectedError>,
status: Option<String>,
reason: Option<String>,
feature: Option<String>,
steps: Option<String>,
flags: Vec<String>,
}
impl CaseBuilder {
fn build(self) -> Result<Js262Case, String> {
let id = self
.id
.ok_or_else(|| "missing required key: id".to_string())?;
let input = self
.input
.ok_or_else(|| format!("case '{id}' missing required key: input"))?;
let mode = self
.mode
.ok_or_else(|| format!("case '{id}' missing required key: mode"))?;
let feature = self
.feature
.ok_or_else(|| format!("case '{id}' missing required key: feature"))?;
let status = match self.status.as_deref().unwrap_or("pass") {
"pass" => Js262Status::Pass,
"known_fail" => Js262Status::KnownFail {
reason: self.reason.unwrap_or_default(),
},
"skip" => Js262Status::Skip {
reason: self.reason.unwrap_or_default(),
},
other => {
return Err(format!(
"case '{id}' has invalid status '{other}' (expected pass|known_fail|skip)"
));
}
};
let steps = match self.steps {
Some(ref s) => {
parse_steps(s).map_err(|e| format!("case '{id}' has invalid steps: {e}"))?
}
None => Vec::new(),
};
Ok(Js262Case {
id,
input,
mode,
expected_output: self.expected_output,
expected_error: self.expected_error,
status,
feature,
steps,
flags: self.flags,
})
}
}
#[cfg(test)]
mod tests {
use super::{parse_manifest, validate_cases};
use crate::types::{ExpectedError, Js262Mode, Js262Status, Step};
/// Helper: parse + validate in one step.
fn parse_and_validate(input: &str) -> Result<Vec<crate::types::Js262Case>, String> {
parse_manifest(input).and_then(validate_cases)
}
#[test]
fn parses_minimal_script_case() {
let input = r#"
[[case]]
id = "basic"
input = "fixtures/basic.js"
mode = "script"
expected_output = "expected/basic.txt"
feature = "basics"
"#;
let cases = parse_manifest(input).expect("manifest parse should succeed");
assert_eq!(cases.len(), 1);
assert_eq!(cases[0].id, "basic");
assert_eq!(cases[0].mode, Js262Mode::Script);
assert!(matches!(cases[0].status, Js262Status::Pass));
}
#[test]
fn parses_error_expectation_case() {
let input = r#"
[[case]]
id = "ref-err"
input = "fixtures/ref-err.js"
mode = "script"
expected_error = "reference"
feature = "errors"
"#;
let cases = parse_manifest(input).expect("manifest parse should succeed");
assert_eq!(cases[0].expected_error, Some(ExpectedError::Reference));
}
#[test]
fn parses_dom_mode_with_steps() {
let input = r#"
[[case]]
id = "timer"
input = "fixtures/timer.js"
mode = "dom"
expected_output = "expected/timer.txt"
feature = "scheduling"
steps = "advance_ms:100,tick"
"#;
let cases = parse_manifest(input).expect("manifest parse should succeed");
assert_eq!(cases[0].mode, Js262Mode::Dom);
assert_eq!(cases[0].steps, vec![Step::AdvanceMs(100), Step::Tick]);
}
#[test]
fn parses_known_fail_case() {
let input = r#"
[[case]]
id = "for-loop"
input = "fixtures/for-loop.js"
mode = "script"
status = "known_fail"
reason = "for loop not yet supported"
feature = "for-loops"
"#;
let cases = parse_manifest(input).expect("manifest parse should succeed");
assert!(matches!(
cases[0].status,
Js262Status::KnownFail { ref reason } if reason == "for loop not yet supported"
));
}
#[test]
fn rejects_key_outside_case_block() {
let input = r#"
id = "bad"
"#;
let err = parse_manifest(input).expect_err("manifest should fail");
assert!(err.contains("outside [[case]]"));
}
#[test]
fn rejects_duplicate_ids() {
let input = r#"
[[case]]
id = "dup"
input = "fixtures/a.js"
mode = "script"
expected_output = "expected/a.txt"
feature = "basics"
[[case]]
id = "dup"
input = "fixtures/b.js"
mode = "script"
expected_output = "expected/b.txt"
feature = "basics"
"#;
let err = parse_and_validate(input).expect_err("manifest should fail");
assert!(err.contains("duplicate"));
}
#[test]
fn rejects_pass_without_expectation() {
let input = r#"
[[case]]
id = "no-expect"
input = "fixtures/no-expect.js"
mode = "script"
feature = "basics"
"#;
let err = parse_and_validate(input).expect_err("manifest should fail");
assert!(err.contains("expected_output or expected_error"));
}
#[test]
fn parses_test262_mode_case() {
let input = r#"
[[case]]
id = "t262-typeof"
input = "test262/language/types/typeof/basic.js"
mode = "test262"
feature = "typeof"
flags = []
"#;
let cases =
parse_and_validate(input).expect("should parse test262 mode with no expected_output");
assert_eq!(cases.len(), 1);
assert_eq!(cases[0].mode, Js262Mode::Test262);
assert!(matches!(cases[0].status, Js262Status::Pass));
}
#[test]
fn test262_mode_allows_expected_error() {
let input = r#"
[[case]]
id = "t262-ref-err"
input = "test262/language/types/typeof/err.js"
mode = "test262"
expected_error = "reference"
feature = "typeof"
flags = []
"#;
let cases = parse_and_validate(input).expect("should parse negative test262 case");
assert_eq!(cases[0].expected_error, Some(ExpectedError::Reference));
}
#[test]
fn test262_mode_rejects_steps() {
let input = r#"
[[case]]
id = "t262-bad"
input = "test262/language/types/typeof/bad.js"
mode = "test262"
feature = "typeof"
steps = "tick"
flags = []
"#;
let err = parse_and_validate(input).expect_err("test262 with steps should fail");
assert!(err.contains("must not define steps"));
}
}