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