All checks were successful
ci / fast (linux) (push) Successful in 6m19s
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>
453 lines
14 KiB
Rust
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"));
|
|
}
|
|
}
|