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.
5//!
6//! Run with: `cargo test -p we-js --test test262`
7
8/// Workspace root relative to the crate directory.
9const WORKSPACE_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../");
10
11/// Metadata extracted from a Test262 test file's YAML frontmatter.
12struct TestMeta {
13 /// If true, the test expects a parse/early error.
14 negative_phase_parse: bool,
15 /// If true, the test expects a runtime error.
16 negative_phase_runtime: bool,
17 /// The expected error type for negative tests (e.g. "SyntaxError").
18 negative_type: Option<String>,
19 /// If true, this is an async test.
20 is_async: bool,
21 /// If true, this test should be run as a module.
22 is_module: bool,
23 /// If true, skip the harness preamble.
24 is_raw: bool,
25 /// Required features.
26 features: Vec<String>,
27 /// Required harness includes.
28 includes: Vec<String>,
29}
30
31impl TestMeta {
32 fn should_skip(&self) -> bool {
33 // Skip async tests and module tests for now.
34 self.is_async || self.is_module
35 }
36}
37
38/// Parse the YAML-ish frontmatter from a Test262 test file.
39///
40/// The frontmatter is between `/*---` and `---*/`.
41fn parse_frontmatter(source: &str) -> TestMeta {
42 let mut meta = TestMeta {
43 negative_phase_parse: false,
44 negative_phase_runtime: false,
45 negative_type: None,
46 is_async: false,
47 is_module: false,
48 is_raw: false,
49 features: Vec::new(),
50 includes: Vec::new(),
51 };
52
53 let start = match source.find("/*---") {
54 Some(i) => i + 5,
55 None => return meta,
56 };
57 let end = match source[start..].find("---*/") {
58 Some(i) => start + i,
59 None => return meta,
60 };
61 let yaml = &source[start..end];
62
63 // Very simple line-by-line YAML extraction.
64 let mut in_negative = false;
65 let mut in_features = false;
66 let mut in_includes = false;
67 let mut in_flags = false;
68
69 for line in yaml.lines() {
70 let trimmed = line.trim();
71
72 // Detect top-level keys (not indented or with specific indent).
73 if !trimmed.is_empty() && !trimmed.starts_with('-') && !line.starts_with(' ') {
74 in_negative = false;
75 in_features = false;
76 in_includes = false;
77 in_flags = false;
78 }
79
80 if trimmed.starts_with("negative:") {
81 in_negative = true;
82 continue;
83 }
84 if trimmed.starts_with("features:") {
85 in_features = true;
86 // Check for inline list: features: [a, b]
87 if let Some(rest) = trimmed.strip_prefix("features:") {
88 let rest = rest.trim();
89 if rest.starts_with('[') && rest.ends_with(']') {
90 let inner = &rest[1..rest.len() - 1];
91 for item in inner.split(',') {
92 let item = item.trim();
93 if !item.is_empty() {
94 meta.features.push(item.to_string());
95 }
96 }
97 in_features = false;
98 }
99 }
100 continue;
101 }
102 if trimmed.starts_with("includes:") {
103 in_includes = true;
104 if let Some(rest) = trimmed.strip_prefix("includes:") {
105 let rest = rest.trim();
106 if rest.starts_with('[') && rest.ends_with(']') {
107 let inner = &rest[1..rest.len() - 1];
108 for item in inner.split(',') {
109 let item = item.trim();
110 if !item.is_empty() {
111 meta.includes.push(item.to_string());
112 }
113 }
114 in_includes = false;
115 }
116 }
117 continue;
118 }
119 if trimmed.starts_with("flags:") {
120 in_flags = true;
121 if let Some(rest) = trimmed.strip_prefix("flags:") {
122 let rest = rest.trim();
123 if rest.starts_with('[') && rest.ends_with(']') {
124 let inner = &rest[1..rest.len() - 1];
125 for item in inner.split(',') {
126 let flag = item.trim();
127 match flag {
128 "async" => meta.is_async = true,
129 "module" => meta.is_module = true,
130 "raw" => meta.is_raw = true,
131 _ => {}
132 }
133 }
134 in_flags = false;
135 }
136 }
137 continue;
138 }
139
140 // Handle list items under current key.
141 if let Some(item) = trimmed.strip_prefix("- ") {
142 if in_features {
143 meta.features.push(item.to_string());
144 } else if in_includes {
145 meta.includes.push(item.to_string());
146 } else if in_flags {
147 match item {
148 "async" => meta.is_async = true,
149 "module" => meta.is_module = true,
150 "raw" => meta.is_raw = true,
151 _ => {}
152 }
153 }
154 continue;
155 }
156
157 // Handle sub-keys under negative.
158 if in_negative {
159 if let Some(rest) = trimmed.strip_prefix("phase:") {
160 let phase = rest.trim();
161 match phase {
162 "parse" | "early" => meta.negative_phase_parse = true,
163 "runtime" | "resolution" => meta.negative_phase_runtime = true,
164 _ => {}
165 }
166 }
167 if let Some(rest) = trimmed.strip_prefix("type:") {
168 meta.negative_type = Some(rest.trim().to_string());
169 }
170 }
171 }
172
173 meta
174}
175
176/// Recursively collect all `.js` test files under a directory.
177fn collect_test_files(dir: &std::path::Path, files: &mut Vec<std::path::PathBuf>) {
178 let entries = match std::fs::read_dir(dir) {
179 Ok(e) => e,
180 Err(_) => return,
181 };
182 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
183 entries.sort_by_key(|e| e.file_name());
184
185 for entry in entries {
186 let path = entry.path();
187 if path.is_dir() {
188 collect_test_files(&path, files);
189 } else if path.extension().map_or(false, |e| e == "js") {
190 // Skip _FIXTURE files (test helpers, not tests themselves).
191 let name = path.file_name().unwrap().to_string_lossy();
192 if !name.contains("_FIXTURE") {
193 files.push(path);
194 }
195 }
196 }
197}
198
199/// Run a single Test262 test file. Returns (pass, fail, skip).
200fn run_test(path: &std::path::Path) -> (usize, usize, usize) {
201 let source = match std::fs::read_to_string(path) {
202 Ok(s) => s,
203 Err(_) => return (0, 0, 1),
204 };
205
206 let meta = parse_frontmatter(&source);
207
208 if meta.should_skip() {
209 return (0, 0, 1);
210 }
211
212 // For negative parse tests, if our evaluate returns an error, that's a pass.
213 // For positive tests, evaluate should succeed (return Ok).
214 let result = we_js::evaluate(&source);
215
216 if meta.negative_phase_parse {
217 // We expect a parse error. If our engine returns any error, count as pass.
218 match result {
219 Err(_) => (1, 0, 0),
220 Ok(()) => (0, 1, 0),
221 }
222 } else {
223 // We expect success.
224 match result {
225 Ok(()) => (1, 0, 0),
226 Err(_) => (0, 1, 0),
227 }
228 }
229}
230
231#[test]
232fn test262_language_tests() {
233 let test_dir = std::path::PathBuf::from(WORKSPACE_ROOT).join("tests/test262/test/language");
234
235 if !test_dir.exists() {
236 eprintln!(
237 "test262 submodule not checked out at {}",
238 test_dir.display()
239 );
240 eprintln!("Run: git submodule update --init tests/test262");
241 return;
242 }
243
244 let mut files = Vec::new();
245 collect_test_files(&test_dir, &mut files);
246
247 let mut total_pass = 0;
248 let mut total_fail = 0;
249 let mut total_skip = 0;
250
251 // Group results by top-level subdirectory for reporting.
252 let mut current_group = String::new();
253 let mut group_pass = 0;
254 let mut group_fail = 0;
255 let mut group_skip = 0;
256
257 for path in &files {
258 // Determine the top-level group (e.g. "expressions", "literals").
259 let rel = path.strip_prefix(&test_dir).unwrap_or(path);
260 let group = rel
261 .components()
262 .next()
263 .map(|c| c.as_os_str().to_string_lossy().to_string())
264 .unwrap_or_default();
265
266 if group != current_group {
267 if !current_group.is_empty() {
268 eprintln!(
269 " {}: {} pass, {} fail, {} skip",
270 current_group, group_pass, group_fail, group_skip
271 );
272 }
273 current_group = group;
274 group_pass = 0;
275 group_fail = 0;
276 group_skip = 0;
277 }
278
279 let (p, f, s) = run_test(path);
280 group_pass += p;
281 group_fail += f;
282 group_skip += s;
283 total_pass += p;
284 total_fail += f;
285 total_skip += s;
286 }
287
288 // Print last group.
289 if !current_group.is_empty() {
290 eprintln!(
291 " {}: {} pass, {} fail, {} skip",
292 current_group, group_pass, group_fail, group_skip
293 );
294 }
295
296 eprintln!();
297 eprintln!(
298 "Test262 language totals: {} pass, {} fail, {} skip ({} total)",
299 total_pass,
300 total_fail,
301 total_skip,
302 total_pass + total_fail + total_skip
303 );
304}