we (web engine): Experimental web browser project to understand the limits of Claude
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}