//! WPT (Web Platform Tests) test harness. //! //! Discovers `.html` test files under `tests/wpt/`, parses them, executes //! inline scripts with a testharness.js shim, and reports pass/fail/skip //! counts grouped by test directory. //! //! Run with: `cargo test -p we-browser --test wpt -- --nocapture` use std::cell::RefCell; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::rc::Rc; /// Workspace root relative to the crate directory. const WORKSPACE_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../"); /// Maximum instructions per test (prevents infinite loops). const INSTRUCTION_LIMIT: u64 = 2_000_000; /// Minimal testharness.js shim that implements the WPT test API. /// /// This provides `test()`, `async_test()`, `promise_test()`, `assert_*()`, /// and the `Event` constructor needed to run WPT-style HTML tests. const TESTHARNESS_PREAMBLE: &str = r#" // --- WPT testharness.js shim --- // Global results array: each entry is { name, status, message }. // status: 0 = PASS, 1 = FAIL, 2 = TIMEOUT, 3 = NOTRUN var __wpt_results__ = []; var __wpt_test_count__ = 0; function test(func, name) { if (name === undefined) { name = "test " + __wpt_test_count__; } __wpt_test_count__ = __wpt_test_count__ + 1; try { func(); __wpt_results__.push({ name: name, status: 0, message: "" }); } catch (e) { var msg = ""; if (typeof e === "string") { msg = e; } else if (e && e.message) { msg = e.message; } else { msg = String(e); } __wpt_results__.push({ name: name, status: 1, message: msg }); } } // Async test support — simplified for synchronous execution model. function async_test(nameOrFunc, name) { if (typeof nameOrFunc === "function") { // async_test(func, name) — run immediately test(nameOrFunc, name); return { step: function(f) { f(); }, done: function() {} }; } // async_test(name) — returns test handle var testName = nameOrFunc; if (testName === undefined) { testName = "async_test " + __wpt_test_count__; } __wpt_test_count__ = __wpt_test_count__ + 1; var completed = false; var t = { step: function(func) { if (completed) { return; } try { func(); } catch (e) { completed = true; var msg = typeof e === "string" ? e : (e && e.message ? e.message : String(e)); __wpt_results__.push({ name: testName, status: 1, message: msg }); } }, step_func: function(func) { var self = this; return function() { self.step(func); }; }, done: function() { if (!completed) { completed = true; __wpt_results__.push({ name: testName, status: 0, message: "" }); } } }; return t; } // Promise test support — simplified for synchronous execution model. function promise_test(func, name) { if (name === undefined) { name = "promise_test " + __wpt_test_count__; } __wpt_test_count__ = __wpt_test_count__ + 1; try { var result = func(); // If result is a promise, we can't actually await it in sync mode. // Mark as pass if no error thrown. __wpt_results__.push({ name: name, status: 0, message: "" }); } catch (e) { var msg = typeof e === "string" ? e : (e && e.message ? e.message : String(e)); __wpt_results__.push({ name: name, status: 1, message: msg }); } } // --- Assertion functions --- function assert_equals(actual, expected, description) { if (actual === expected) { return; } // Handle NaN if (actual !== actual && expected !== expected) { return; } var msg = "assert_equals: "; if (description) { msg = description + ": "; } msg = msg + "expected " + String(expected) + " but got " + String(actual); throw new Error(msg); } function assert_not_equals(actual, unexpected, description) { if (actual !== unexpected) { return; } var msg = "assert_not_equals: "; if (description) { msg = description + ": "; } msg = msg + "got disallowed value " + String(unexpected); throw new Error(msg); } function assert_true(actual, description) { if (actual === true) { return; } var msg = "assert_true: "; if (description) { msg = description + ": "; } msg = msg + "expected true but got " + String(actual); throw new Error(msg); } function assert_false(actual, description) { if (actual === false) { return; } var msg = "assert_false: "; if (description) { msg = description + ": "; } msg = msg + "expected false but got " + String(actual); throw new Error(msg); } function assert_throws_js(constructor, func, description) { var threw = false; try { func(); } catch (e) { threw = true; } if (!threw) { var msg = "assert_throws_js: "; if (description) { msg = description + ": "; } msg = msg + "function did not throw"; throw new Error(msg); } } function assert_throws_dom(name, func, description) { var threw = false; try { func(); } catch (e) { threw = true; } if (!threw) { var msg = "assert_throws_dom: "; if (description) { msg = description + ": "; } msg = msg + "function did not throw"; throw new Error(msg); } } function assert_array_equals(actual, expected, description) { if (actual.length !== expected.length) { var msg = "assert_array_equals: "; if (description) { msg = description + ": "; } msg = msg + "lengths differ: " + actual.length + " vs " + expected.length; throw new Error(msg); } for (var i = 0; i < actual.length; i = i + 1) { if (actual[i] !== expected[i]) { var msg2 = "assert_array_equals: "; if (description) { msg2 = description + ": "; } msg2 = msg2 + "element " + i + " differs: " + actual[i] + " vs " + expected[i]; throw new Error(msg2); } } } function assert_class_string(object, expected, description) { // Simplified: just check that the object exists if (object === null || object === undefined) { var msg = "assert_class_string: "; if (description) { msg = description + ": "; } msg = msg + "object is " + String(object); throw new Error(msg); } } function assert_readonly(object, name, description) { // Simplified: just verify the property exists if (!(name in object)) { var msg = "assert_readonly: "; if (description) { msg = description + ": "; } msg = msg + "property " + name + " not found"; throw new Error(msg); } } function assert_unreached(description) { var msg = "assert_unreached: "; if (description) { msg = msg + description; } throw new Error(msg); } // Event constructor shim (for tests that create events). if (typeof Event === "undefined") { function Event(type, options) { this.type = type; this.bubbles = false; this.cancelable = false; if (options) { if (options.bubbles) { this.bubbles = options.bubbles; } if (options.cancelable) { this.cancelable = options.cancelable; } } this.defaultPrevented = false; this.target = null; this.currentTarget = null; } } // setup() is a no-op in our harness. function setup(func_or_properties, maybe_properties) {} // done() for manual tests — no-op in our synchronous model. function done() {} // format_value helper used by some tests. function format_value(value) { return String(value); } // After all scripts run, dump results to console for the harness to read. // This is invoked by the test runner after script execution. "#; /// A captured console that stores output. struct CapturedConsole { messages: RefCell>, } impl CapturedConsole { fn new() -> Self { Self { messages: RefCell::new(Vec::new()), } } } impl we_js::vm::ConsoleOutput for CapturedConsole { fn log(&self, message: &str) { self.messages.borrow_mut().push(message.to_string()); } fn error(&self, _message: &str) {} fn warn(&self, _message: &str) {} } /// Wrapper to make Rc implement ConsoleOutput. struct RcConsole(Rc); impl we_js::vm::ConsoleOutput for RcConsole { fn log(&self, message: &str) { self.0.log(message); } fn error(&self, message: &str) { self.0.error(message); } fn warn(&self, message: &str) { self.0.warn(message); } } /// Result of a single subtest within an HTML test file. #[derive(Debug)] struct SubtestResult { name: String, status: SubtestStatus, message: String, } #[derive(Debug, Clone, Copy, PartialEq)] enum SubtestStatus { Pass, Fail, Timeout, NotRun, } /// Result of running an entire HTML test file. #[derive(Debug)] enum TestFileResult { /// Test file executed, subtests collected. Executed(Vec), /// Test file was skipped. Skip(String), /// Test file caused a panic. Panic, } /// Category statistics for reporting. struct CategoryStats { pass: usize, fail: usize, skip: usize, timeout: usize, error: usize, } impl CategoryStats { fn new() -> Self { Self { pass: 0, fail: 0, skip: 0, timeout: 0, error: 0, } } fn total(&self) -> usize { self.pass + self.fail + self.skip + self.timeout + self.error } fn executed(&self) -> usize { self.pass + self.fail } fn pass_rate(&self) -> f64 { let executed = self.executed(); if executed == 0 { 0.0 } else { (self.pass as f64 / executed as f64) * 100.0 } } } /// Recursively collect `.html` test files under a directory. fn collect_test_files(dir: &Path, files: &mut Vec) { let entries = match std::fs::read_dir(dir) { Ok(e) => e, Err(_) => return, }; let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); entries.sort_by_key(|e| e.file_name()); for entry in entries { let path = entry.path(); if path.is_dir() { collect_test_files(&path, files); } else if path .extension() .map_or(false, |e| e == "html" || e == "htm") { files.push(path); } } } /// Check if an HTML file is a testharness.js test (contains a reference /// to testharness.js or uses test()/async_test()/promise_test()). fn is_testharness_test(html: &str) -> bool { html.contains("testharness.js") || html.contains("test(function") || html.contains("test(()") // arrow function variant || html.contains("async_test(") || html.contains("promise_test(") } /// Extract inline script content from all `