we (web engine): Experimental web browser project to understand the limits of Claude
at string-number-boolean-symbol 729 lines 22 kB view raw
1//! Test262 test harness. 2//! 3//! Walks the Test262 test suite and runs each test case against our JavaScript 4//! engine. Reports pass/fail/skip counts grouped by category. 5//! 6//! Run with: `cargo test -p we-js --test test262 -- --nocapture` 7 8/// Workspace root relative to the crate directory. 9const WORKSPACE_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../"); 10 11/// Minimal JS preamble that defines the Test262 harness helpers using only 12/// features our engine supports. This replaces the standard `sta.js` and 13/// `assert.js` harness files which require built-ins we don't have yet. 14const HARNESS_PREAMBLE: &str = r#" 15function Test262Error(message) { 16 return "Test262Error: " + message; 17} 18 19function $DONOTEVALUATE() { 20 throw "Test262: This statement should not be evaluated."; 21} 22 23function assert(mustBeTrue, message) { 24 if (mustBeTrue === true) { 25 return; 26 } 27 if (message === undefined) { 28 message = "Expected true but got " + mustBeTrue; 29 } 30 throw message; 31} 32 33assert._isSameValue = function(a, b) { 34 if (a === b) { 35 if (a !== 0) { return true; } 36 return 1 / a === 1 / b; 37 } 38 if (a !== a && b !== b) { return true; } 39 return false; 40}; 41 42assert.sameValue = function(actual, expected, message) { 43 if (assert._isSameValue(actual, expected)) { 44 return; 45 } 46 if (message === undefined) { 47 message = ""; 48 } else { 49 message = message + " "; 50 } 51 message = message + "Expected SameValue(" + actual + ", " + expected + ") to be true"; 52 throw message; 53}; 54 55assert.notSameValue = function(actual, unexpected, message) { 56 if (!assert._isSameValue(actual, unexpected)) { 57 return; 58 } 59 if (message === undefined) { 60 message = ""; 61 } else { 62 message = message + " "; 63 } 64 message = message + "Expected not SameValue(" + actual + ", " + unexpected + ")"; 65 throw message; 66}; 67 68assert.throws = function(expectedErrorConstructor, func, message) { 69 if (typeof func !== "function") { 70 throw "assert.throws requires a function argument"; 71 } 72 var threw = false; 73 try { 74 func(); 75 } catch (e) { 76 threw = true; 77 } 78 if (!threw) { 79 if (message === undefined) { 80 message = "Expected an exception to be thrown"; 81 } 82 throw message; 83 } 84}; 85 86function print() {} 87"#; 88 89/// Features that our engine does not yet support. Tests requiring any of these 90/// are skipped rather than counted as failures. 91const UNSUPPORTED_FEATURES: &[&str] = &[ 92 // Type system extensions 93 "BigInt", 94 "Symbol", 95 "Symbol.asyncIterator", 96 "Symbol.hasInstance", 97 "Symbol.isConcatSpreadable", 98 "Symbol.iterator", 99 "Symbol.match", 100 "Symbol.matchAll", 101 "Symbol.replace", 102 "Symbol.search", 103 "Symbol.species", 104 "Symbol.split", 105 "Symbol.toPrimitive", 106 "Symbol.toStringTag", 107 "Symbol.unscopables", 108 // Collections and buffers 109 "ArrayBuffer", 110 "DataView", 111 "Float16Array", 112 "Float32Array", 113 "Float64Array", 114 "Int8Array", 115 "Int16Array", 116 "Int32Array", 117 "Map", 118 "Set", 119 "SharedArrayBuffer", 120 "TypedArray", 121 "Uint8Array", 122 "Uint8ClampedArray", 123 "Uint16Array", 124 "Uint32Array", 125 "WeakMap", 126 "WeakRef", 127 "WeakSet", 128 "FinalizationRegistry", 129 // Async 130 "Promise", 131 "async-functions", 132 "async-iteration", 133 "top-level-await", 134 // Generators and iterators 135 "generators", 136 "async-generators", 137 // Modules 138 "import-assertions", 139 "import-attributes", 140 "dynamic-import", 141 "import.meta", 142 // Proxy and Reflect 143 "Proxy", 144 "Reflect", 145 "Reflect.construct", 146 "Reflect.set", 147 "Reflect.setPrototypeOf", 148 // Regex features 149 "regexp-dotall", 150 "regexp-lookbehind", 151 "regexp-named-groups", 152 "regexp-unicode-property-escapes", 153 "regexp-v-flag", 154 "regexp-match-indices", 155 "regexp-duplicate-named-groups", 156 "regexp-modifiers", 157 // Modern syntax 158 "class-fields-private", 159 "class-fields-private-in", 160 "class-fields-public", 161 "class-methods-private", 162 "class-static-block", 163 "class-static-fields-private", 164 "class-static-fields-public", 165 "class-static-methods-private", 166 "decorators", 167 "hashbang", 168 // Intl 169 "Intl-enumeration", 170 "Intl.DateTimeFormat", 171 "Intl.DisplayNames", 172 "Intl.ListFormat", 173 "Intl.Locale", 174 "Intl.NumberFormat", 175 "Intl.PluralRules", 176 "Intl.RelativeTimeFormat", 177 "Intl.Segmenter", 178 // Built-in methods we don't have 179 "Array.fromAsync", 180 "Array.prototype.at", 181 "Array.prototype.flat", 182 "Array.prototype.flatMap", 183 "Array.prototype.includes", 184 "Array.prototype.values", 185 "ArrayBuffer.prototype.transfer", 186 "Object.fromEntries", 187 "Object.hasOwn", 188 "Object.is", 189 "Promise.allSettled", 190 "Promise.any", 191 "Promise.prototype.finally", 192 "String.fromCodePoint", 193 "String.prototype.at", 194 "String.prototype.endsWith", 195 "String.prototype.includes", 196 "String.prototype.matchAll", 197 "String.prototype.replaceAll", 198 "String.prototype.trimEnd", 199 "String.prototype.trimStart", 200 "String.prototype.isWellFormed", 201 "String.prototype.toWellFormed", 202 // Other 203 "Atomics", 204 "Atomics.waitAsync", 205 "cleanupSome", 206 "coalesce-expression", 207 "cross-realm", 208 "error-cause", 209 "explicit-resource-management", 210 "for-in-order", 211 "globalThis", 212 "json-modules", 213 "json-parse-with-source", 214 "json-superset", 215 "legacy-regexp", 216 "logical-assignment-operators", 217 "numeric-separator-literal", 218 "optional-catch-binding", 219 "optional-chaining", 220 "resizable-arraybuffer", 221 "ShadowRealm", 222 "string-trimming", 223 "super", 224 "tail-call-optimization", 225 "template", 226 "u180e", 227 "well-formed-json-stringify", 228 "__getter__", 229 "__setter__", 230 "__proto__", 231 // Iterator helpers 232 "iterator-helpers", 233 "set-methods", 234 "change-array-by-copy", 235 "symbols-as-weakmap-keys", 236 "Temporal", 237 "Array.prototype.group", 238 "Math.sumPrecise", 239 "Disposable", 240 "using", 241]; 242 243/// Harness include files that we can handle (we supply our own preamble). 244/// Tests requiring other includes are skipped. 245const SUPPORTED_INCLUDES: &[&str] = &[ 246 "sta.js", 247 "assert.js", 248 "compareArray.js", 249 "propertyHelper.js", 250]; 251 252/// Metadata extracted from a Test262 test file's YAML frontmatter. 253struct TestMeta { 254 /// If true, the test expects a parse/early error. 255 negative_phase_parse: bool, 256 /// If true, the test expects a runtime error. 257 negative_phase_runtime: bool, 258 /// The expected error type for negative tests (e.g. "SyntaxError"). 259 _negative_type: Option<String>, 260 /// If true, this is an async test. 261 is_async: bool, 262 /// If true, this test should be run as a module. 263 is_module: bool, 264 /// If true, skip the harness preamble. 265 is_raw: bool, 266 /// Required features. 267 features: Vec<String>, 268 /// Required harness includes. 269 includes: Vec<String>, 270} 271 272impl TestMeta { 273 fn should_skip(&self) -> Option<&'static str> { 274 if self.is_async { 275 return Some("async"); 276 } 277 if self.is_module { 278 return Some("module"); 279 } 280 // Skip tests requiring unsupported features. 281 for feat in &self.features { 282 if UNSUPPORTED_FEATURES.contains(&feat.as_str()) { 283 return Some("unsupported feature"); 284 } 285 } 286 // Skip tests requiring harness includes we can't provide. 287 for inc in &self.includes { 288 if !SUPPORTED_INCLUDES.contains(&inc.as_str()) { 289 return Some("unsupported include"); 290 } 291 } 292 None 293 } 294} 295 296/// Parse the YAML-ish frontmatter from a Test262 test file. 297/// 298/// The frontmatter is between `/*---` and `---*/`. 299fn parse_frontmatter(source: &str) -> TestMeta { 300 let mut meta = TestMeta { 301 negative_phase_parse: false, 302 negative_phase_runtime: false, 303 _negative_type: None, 304 is_async: false, 305 is_module: false, 306 is_raw: false, 307 features: Vec::new(), 308 includes: Vec::new(), 309 }; 310 311 let start = match source.find("/*---") { 312 Some(i) => i + 5, 313 None => return meta, 314 }; 315 let end = match source[start..].find("---*/") { 316 Some(i) => start + i, 317 None => return meta, 318 }; 319 let yaml = &source[start..end]; 320 321 // Very simple line-by-line YAML extraction. 322 let mut in_negative = false; 323 let mut in_features = false; 324 let mut in_includes = false; 325 let mut in_flags = false; 326 327 for line in yaml.lines() { 328 let trimmed = line.trim(); 329 330 // Detect top-level keys (not indented or with specific indent). 331 if !trimmed.is_empty() && !trimmed.starts_with('-') && !line.starts_with(' ') { 332 in_negative = false; 333 in_features = false; 334 in_includes = false; 335 in_flags = false; 336 } 337 338 if trimmed.starts_with("negative:") { 339 in_negative = true; 340 continue; 341 } 342 if trimmed.starts_with("features:") { 343 in_features = true; 344 // Check for inline list: features: [a, b] 345 if let Some(rest) = trimmed.strip_prefix("features:") { 346 let rest = rest.trim(); 347 if rest.starts_with('[') && rest.ends_with(']') { 348 let inner = &rest[1..rest.len() - 1]; 349 for item in inner.split(',') { 350 let item = item.trim(); 351 if !item.is_empty() { 352 meta.features.push(item.to_string()); 353 } 354 } 355 in_features = false; 356 } 357 } 358 continue; 359 } 360 if trimmed.starts_with("includes:") { 361 in_includes = true; 362 if let Some(rest) = trimmed.strip_prefix("includes:") { 363 let rest = rest.trim(); 364 if rest.starts_with('[') && rest.ends_with(']') { 365 let inner = &rest[1..rest.len() - 1]; 366 for item in inner.split(',') { 367 let item = item.trim(); 368 if !item.is_empty() { 369 meta.includes.push(item.to_string()); 370 } 371 } 372 in_includes = false; 373 } 374 } 375 continue; 376 } 377 if trimmed.starts_with("flags:") { 378 in_flags = true; 379 if let Some(rest) = trimmed.strip_prefix("flags:") { 380 let rest = rest.trim(); 381 if rest.starts_with('[') && rest.ends_with(']') { 382 let inner = &rest[1..rest.len() - 1]; 383 for item in inner.split(',') { 384 let flag = item.trim(); 385 match flag { 386 "async" => meta.is_async = true, 387 "module" => meta.is_module = true, 388 "raw" => meta.is_raw = true, 389 _ => {} 390 } 391 } 392 in_flags = false; 393 } 394 } 395 continue; 396 } 397 398 // Handle list items under current key. 399 if let Some(item) = trimmed.strip_prefix("- ") { 400 if in_features { 401 meta.features.push(item.to_string()); 402 } else if in_includes { 403 meta.includes.push(item.to_string()); 404 } else if in_flags { 405 match item { 406 "async" => meta.is_async = true, 407 "module" => meta.is_module = true, 408 "raw" => meta.is_raw = true, 409 _ => {} 410 } 411 } 412 continue; 413 } 414 415 // Handle sub-keys under negative. 416 if in_negative { 417 if let Some(rest) = trimmed.strip_prefix("phase:") { 418 let phase = rest.trim(); 419 match phase { 420 "parse" | "early" => meta.negative_phase_parse = true, 421 "runtime" | "resolution" => meta.negative_phase_runtime = true, 422 _ => {} 423 } 424 } 425 if let Some(rest) = trimmed.strip_prefix("type:") { 426 meta._negative_type = Some(rest.trim().to_string()); 427 } 428 } 429 } 430 431 meta 432} 433 434/// Recursively collect all `.js` test files under a directory. 435fn collect_test_files(dir: &std::path::Path, files: &mut Vec<std::path::PathBuf>) { 436 let entries = match std::fs::read_dir(dir) { 437 Ok(e) => e, 438 Err(_) => return, 439 }; 440 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); 441 entries.sort_by_key(|e| e.file_name()); 442 443 for entry in entries { 444 let path = entry.path(); 445 if path.is_dir() { 446 collect_test_files(&path, files); 447 } else if path.extension().map_or(false, |e| e == "js") { 448 // Skip _FIXTURE files (test helpers, not tests themselves). 449 let name = path.file_name().unwrap().to_string_lossy(); 450 if !name.contains("_FIXTURE") { 451 files.push(path); 452 } 453 } 454 } 455} 456 457/// Result of running a single test. 458#[allow(dead_code)] 459enum TestResult { 460 Pass, 461 Fail(String), 462 Skip(String), 463} 464 465/// Maximum instructions per test (prevents infinite loops). 466const INSTRUCTION_LIMIT: u64 = 1_000_000; 467 468/// Run a single Test262 test file. Uses `catch_unwind` to handle compiler/VM 469/// panics gracefully so a single broken test doesn't crash the whole suite. 470fn run_test(path: &std::path::Path) -> TestResult { 471 let source = match std::fs::read_to_string(path) { 472 Ok(s) => s, 473 Err(e) => return TestResult::Skip(format!("read error: {e}")), 474 }; 475 476 let meta = parse_frontmatter(&source); 477 478 if let Some(reason) = meta.should_skip() { 479 return TestResult::Skip(reason.to_string()); 480 } 481 482 // Wrap execution in catch_unwind to survive compiler/VM panics. 483 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 484 run_test_inner(&source, &meta) 485 })); 486 487 match result { 488 Ok(r) => r, 489 Err(_) => TestResult::Fail("panic".into()), 490 } 491} 492 493/// Install a silent panic hook to avoid thousands of lines of panic output 494/// when tests trigger compiler/VM bugs. 495fn set_silent_panic_hook() { 496 std::panic::set_hook(Box::new(|_| {})); 497} 498 499/// Restore the default panic hook. 500fn restore_panic_hook() { 501 let _ = std::panic::take_hook(); 502} 503 504fn run_test_inner(source: &str, meta: &TestMeta) -> TestResult { 505 let limit = Some(INSTRUCTION_LIMIT); 506 507 // Negative parse tests: we expect parsing to fail. 508 if meta.negative_phase_parse { 509 match we_js::evaluate(source) { 510 Err(_) => return TestResult::Pass, 511 Ok(_) => return TestResult::Fail("expected parse error but succeeded".into()), 512 } 513 } 514 515 // Negative runtime tests: we expect a runtime error. 516 if meta.negative_phase_runtime { 517 let preamble = if meta.is_raw { "" } else { HARNESS_PREAMBLE }; 518 match we_js::evaluate_with_preamble_limited(preamble, source, limit) { 519 Err(we_js::JsError::RuntimeError(_)) => return TestResult::Pass, 520 Err(we_js::JsError::SyntaxError(_)) => { 521 // Parse error for a runtime-negative test; count as pass since 522 // stricter parsing is acceptable. 523 return TestResult::Pass; 524 } 525 Err(_) => return TestResult::Pass, 526 Ok(()) => return TestResult::Fail("expected runtime error but succeeded".into()), 527 } 528 } 529 530 // Positive tests: should parse and run without errors. 531 if meta.is_raw { 532 match we_js::evaluate(source) { 533 Ok(_) => TestResult::Pass, 534 Err(e) => TestResult::Fail(format!("{e}")), 535 } 536 } else { 537 match we_js::evaluate_with_preamble_limited(HARNESS_PREAMBLE, source, limit) { 538 Ok(()) => TestResult::Pass, 539 Err(e) => TestResult::Fail(format!("{e}")), 540 } 541 } 542} 543 544/// Test category statistics. 545struct CategoryStats { 546 pass: usize, 547 fail: usize, 548 skip: usize, 549} 550 551impl CategoryStats { 552 fn new() -> Self { 553 Self { 554 pass: 0, 555 fail: 0, 556 skip: 0, 557 } 558 } 559 560 fn total(&self) -> usize { 561 self.pass + self.fail + self.skip 562 } 563 564 fn pass_rate(&self) -> f64 { 565 let executed = self.pass + self.fail; 566 if executed == 0 { 567 0.0 568 } else { 569 (self.pass as f64 / executed as f64) * 100.0 570 } 571 } 572} 573 574/// Verify the harness preamble compiles and works correctly. 575#[test] 576fn test262_harness_preamble() { 577 // Preamble should evaluate without errors. 578 we_js::evaluate(HARNESS_PREAMBLE).expect("harness preamble should evaluate cleanly"); 579 580 // assert(true) should pass. 581 we_js::evaluate_with_preamble(HARNESS_PREAMBLE, "assert(true);") 582 .expect("assert(true) should pass"); 583 584 // assert(false) should throw. 585 assert!( 586 we_js::evaluate_with_preamble(HARNESS_PREAMBLE, "assert(false);").is_err(), 587 "assert(false) should throw" 588 ); 589 590 // assert.sameValue with equal values should pass. 591 we_js::evaluate_with_preamble(HARNESS_PREAMBLE, "assert.sameValue(1, 1);") 592 .expect("assert.sameValue(1, 1) should pass"); 593 594 // assert.sameValue with unequal values should throw. 595 assert!( 596 we_js::evaluate_with_preamble(HARNESS_PREAMBLE, "assert.sameValue(1, 2);").is_err(), 597 "assert.sameValue(1, 2) should throw" 598 ); 599 600 // assert.notSameValue with unequal values should pass. 601 we_js::evaluate_with_preamble(HARNESS_PREAMBLE, "assert.notSameValue(1, 2);") 602 .expect("assert.notSameValue(1, 2) should pass"); 603 604 // assert.throws should pass when function throws. 605 we_js::evaluate_with_preamble( 606 HARNESS_PREAMBLE, 607 r#"assert.throws(null, function() { throw "err"; });"#, 608 ) 609 .expect("assert.throws should pass when function throws"); 610 611 // assert.throws should fail when function doesn't throw. 612 assert!( 613 we_js::evaluate_with_preamble(HARNESS_PREAMBLE, r#"assert.throws(null, function() { });"#,) 614 .is_err(), 615 "assert.throws should fail when function doesn't throw" 616 ); 617} 618 619#[test] 620fn test262_language_tests() { 621 // Run in a thread with a large stack to avoid stack overflows in debug mode. 622 // Some test262 tests trigger deep recursion in the parser/compiler/VM. 623 let builder = std::thread::Builder::new() 624 .name("test262".into()) 625 .stack_size(32 * 1024 * 1024); 626 let handle = builder 627 .spawn(test262_language_tests_inner) 628 .expect("failed to spawn test262 thread"); 629 handle.join().expect("test262 thread panicked"); 630} 631 632fn test262_language_tests_inner() { 633 let test_dir = std::path::PathBuf::from(WORKSPACE_ROOT).join("tests/test262/test/language"); 634 635 if !test_dir.exists() { 636 eprintln!( 637 "test262 submodule not checked out at {}", 638 test_dir.display() 639 ); 640 eprintln!("Run: git submodule update --init tests/test262"); 641 return; 642 } 643 644 let mut files = Vec::new(); 645 collect_test_files(&test_dir, &mut files); 646 647 let mut total = CategoryStats::new(); 648 let mut groups: Vec<(String, CategoryStats)> = Vec::new(); 649 let mut current_group = String::new(); 650 651 // Suppress panic output — many tests trigger pre-existing compiler bugs 652 // (register allocation) which panic and are caught by catch_unwind. 653 set_silent_panic_hook(); 654 655 eprintln!("\n=== Test262 Language Tests ===\n"); 656 657 for path in &files { 658 // Determine the top-level group (e.g. "expressions", "literals"). 659 let rel = path.strip_prefix(&test_dir).unwrap_or(path); 660 let group = rel 661 .components() 662 .next() 663 .map(|c| c.as_os_str().to_string_lossy().to_string()) 664 .unwrap_or_default(); 665 666 if group != current_group { 667 current_group = group.clone(); 668 groups.push((group, CategoryStats::new())); 669 } 670 671 let stats = &mut groups.last_mut().unwrap().1; 672 673 match run_test(path) { 674 TestResult::Pass => { 675 stats.pass += 1; 676 total.pass += 1; 677 } 678 TestResult::Fail(_) => { 679 stats.fail += 1; 680 total.fail += 1; 681 } 682 TestResult::Skip(_) => { 683 stats.skip += 1; 684 total.skip += 1; 685 } 686 } 687 } 688 689 // Print results grouped by category. 690 for (name, stats) in &groups { 691 if stats.total() > 0 { 692 eprintln!( 693 " {:<30} {:>4} pass {:>4} fail {:>4} skip ({:.0}% of executed)", 694 name, 695 stats.pass, 696 stats.fail, 697 stats.skip, 698 stats.pass_rate() 699 ); 700 } 701 } 702 703 eprintln!(); 704 eprintln!( 705 " {:<30} {:>4} pass {:>4} fail {:>4} skip ({:.0}% of executed)", 706 "TOTAL", 707 total.pass, 708 total.fail, 709 total.skip, 710 total.pass_rate() 711 ); 712 eprintln!( 713 " {} total tests, {} executed", 714 total.total(), 715 total.pass + total.fail 716 ); 717 eprintln!(); 718 719 // Restore default panic hook. 720 restore_panic_hook(); 721 722 // The test passes as long as the harness itself works. We track pass rate 723 // for progress monitoring but don't assert a minimum threshold yet. 724 // As more built-ins are implemented, the pass rate will increase. 725 assert!( 726 total.pass > 0, 727 "Expected at least some Test262 tests to pass" 728 ); 729}