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