diff --git a/Cargo.lock b/Cargo.lock index d91d84fa0a8..a0b25b52671 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,7 +201,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 5.3.1", + "event-listener 5.4.0", "event-listener-strategy", "pin-project-lite", ] @@ -230,7 +230,7 @@ dependencies = [ "async-task", "blocking", "cfg-if", - "event-listener 5.3.1", + "event-listener 5.4.0", "futures-lite 2.5.0", "rustix", "tracing", @@ -606,6 +606,17 @@ dependencies = [ "wasm-bindgen-test", ] +[[package]] +name = "boa_wpt" +version = "0.1.0" +dependencies = [ + "boa_engine", + "boa_gc", + "boa_interop", + "boa_runtime", + "rstest", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -759,9 +770,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.23" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", "clap_derive", @@ -769,9 +780,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ "anstream", "anstyle", @@ -781,9 +792,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck", "proc-macro2", @@ -1245,9 +1256,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -1260,7 +1271,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ - "event-listener 5.3.1", + "event-listener 5.4.0", "pin-project-lite", ] @@ -1364,6 +1375,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-buffered" version = "0.2.9" @@ -1376,6 +1402,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-concurrency" version = "7.6.2" @@ -1397,6 +1433,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -1431,6 +1478,53 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gen-icu4x-data" version = "0.20.0" @@ -2349,9 +2443,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.20" +version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" dependencies = [ "cc", "libc", @@ -2361,9 +2455,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" @@ -2804,6 +2898,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.4" @@ -3085,6 +3185,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "ring" version = "0.17.8" @@ -3100,6 +3206,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -3118,11 +3254,20 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ "bitflags 2.6.0", "errno", @@ -3617,18 +3762,18 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb" dependencies = [ "proc-macro2", "quote", @@ -3719,9 +3864,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "pin-project-lite", @@ -3730,9 +3875,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 459aa6623de..ce8b07173dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,6 @@ members = [ # OTHERS "examples", "cli", - # TOOLS - "tools/*", ] exclude = [ @@ -21,6 +19,16 @@ exclude = [ "tests/src", # Just a hack to have fuzz inside tests ] +default-members = [ + "core/*", + "ffi/*", + "tests/macros", + "tests/tester", + "tools/*", + "examples", + "cli", +] + [workspace.package] edition = "2021" version = "0.20.0" @@ -117,6 +125,7 @@ criterion = "0.5.1" float-cmp = "0.10.0" futures-lite = "2.5.0" test-case = "3.3.1" +rstest = "0.23.0" winapi = { version = "0.3.9", default-features = false } url = "2.5.4" tokio = { version = "1.42.0", default-features = false } diff --git a/core/runtime/src/console/mod.rs b/core/runtime/src/console/mod.rs index 4789f29730b..9db37ddab10 100644 --- a/core/runtime/src/console/mod.rs +++ b/core/runtime/src/console/mod.rs @@ -93,6 +93,32 @@ impl Logger for DefaultLogger { } } +/// A logger that drops all logging. Useful for testing. +#[derive(Debug, Trace, Finalize)] +pub struct NullLogger; + +impl Logger for NullLogger { + #[inline] + fn log(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> { + Ok(()) + } + + #[inline] + fn info(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> { + Ok(()) + } + + #[inline] + fn warn(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> { + Ok(()) + } + + #[inline] + fn error(&self, _: String, _: &ConsoleState, _: &mut Context) -> JsResult<()> { + Ok(()) + } +} + /// This represents the `console` formatter. fn formatter(data: &[JsValue], context: &mut Context) -> JsResult { fn to_string(value: &JsValue, context: &mut Context) -> JsResult { diff --git a/core/runtime/src/console/tests.rs b/core/runtime/src/console/tests.rs index 61417c8db31..918d83c3132 100644 --- a/core/runtime/src/console/tests.rs +++ b/core/runtime/src/console/tests.rs @@ -1,6 +1,6 @@ use super::{formatter, Console, ConsoleState}; use crate::test::{run_test_actions, run_test_actions_with, TestAction}; -use crate::Logger; +use crate::{Logger, NullLogger}; use boa_engine::{js_string, property::Attribute, Context, JsError, JsResult, JsValue}; use boa_gc::{Gc, GcRefCell}; use indoc::indoc; @@ -97,7 +97,7 @@ fn formatter_float_format_works() { #[test] fn console_log_cyclic() { let mut context = Context::default(); - let console = Console::init(&mut context); + let console = Console::init_with_logger(&mut context, NullLogger); context .register_global_property(Console::NAME, console, Attribute::all()) .unwrap(); diff --git a/core/runtime/src/lib.rs b/core/runtime/src/lib.rs index 96a35626b7f..a7c628cfe6b 100644 --- a/core/runtime/src/lib.rs +++ b/core/runtime/src/lib.rs @@ -45,6 +45,8 @@ )] #![cfg_attr(test, allow(clippy::needless_raw_string_hashes))] // Makes strings a bit more copy-pastable #![cfg_attr(not(test), forbid(clippy::unwrap_used))] +// Currently throws a false positive regarding dependencies that are only used in tests. +#![allow(unused_crate_dependencies)] #![allow( clippy::module_name_repetitions, clippy::redundant_pub_crate, @@ -54,7 +56,7 @@ mod console; #[doc(inline)] -pub use console::{Console, ConsoleState, Logger}; +pub use console::{Console, ConsoleState, DefaultLogger, Logger, NullLogger}; mod text; @@ -69,15 +71,15 @@ pub struct RegisterOptions { console_logger: L, } -impl Default for RegisterOptions { +impl Default for RegisterOptions { fn default() -> Self { Self { - console_logger: console::DefaultLogger, + console_logger: DefaultLogger, } } } -impl RegisterOptions { +impl RegisterOptions { /// Create a new `RegisterOptions` with the default options. #[must_use] pub fn new() -> Self { diff --git a/core/runtime/src/url.rs b/core/runtime/src/url.rs index 2e07b8c19dc..1de22b47d28 100644 --- a/core/runtime/src/url.rs +++ b/core/runtime/src/url.rs @@ -36,6 +36,14 @@ impl Url { Ok(()) } + /// Create a new `URL` object from a `url::Url`. + /// + /// # Errors + /// Any errors that might occur during URL parsing. + pub fn new>(url: T) -> Result { + url.try_into().map(Self) + } + /// Create a new `URL` object. Meant to be called from the JavaScript constructor. /// /// # Errors diff --git a/tests/wpt/Cargo.toml b/tests/wpt/Cargo.toml new file mode 100644 index 00000000000..42f8405b1e4 --- /dev/null +++ b/tests/wpt/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "boa_wpt" +publish = false +version = "0.1.0" +edition = "2021" + +[dependencies] +boa_engine.workspace = true +boa_gc.workspace = true +boa_interop.workspace = true +boa_runtime.workspace = true +rstest.workspace = true diff --git a/tests/wpt/src/lib.rs b/tests/wpt/src/lib.rs new file mode 100644 index 00000000000..4658e3303fa --- /dev/null +++ b/tests/wpt/src/lib.rs @@ -0,0 +1,393 @@ +//! Integration tests running the Web Platform Tests (WPT) for the `boa_runtime` crate. +#![allow(unused_crate_dependencies)] + +use boa_engine::class::Class; +use boa_engine::parser::source::UTF16Input; +use boa_engine::property::Attribute; +use boa_engine::value::TryFromJs; +use boa_engine::{ + js_error, js_str, js_string, Context, Finalize, JsData, JsResult, JsString, JsValue, Source, + Trace, +}; +use boa_gc::Gc; +use boa_interop::{ContextData, IntoJsFunctionCopied}; +use boa_runtime::url::Url; +use boa_runtime::RegisterOptions; +use logger::RecordingLogEvent; +use std::cell::OnceCell; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; + +mod logger; + +/// The test status JavaScript type from WPT. This is defined in the test harness. +#[derive(Debug, Clone, PartialEq, Eq)] +enum TestStatus { + Pass = 0, + Fail = 1, + Timeout = 2, + NotRun = 3, + PreconditionFailed = 4, +} + +impl std::fmt::Display for TestStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pass => write!(f, "PASS"), + Self::Fail => write!(f, "FAIL"), + Self::Timeout => write!(f, "TIMEOUT"), + Self::NotRun => write!(f, "NOTRUN"), + Self::PreconditionFailed => write!(f, "PRECONDITION FAILED"), + } + } +} + +impl TryFromJs for TestStatus { + fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult { + match value.to_u32(context) { + Ok(0) => Ok(Self::Pass), + Ok(1) => Ok(Self::Fail), + Ok(2) => Ok(Self::Timeout), + Ok(3) => Ok(Self::NotRun), + Ok(4) => Ok(Self::PreconditionFailed), + _ => Err(js_error!("Invalid test status")), + } + } +} + +/// A single test serialization. +#[derive(TryFromJs)] +struct Test { + name: JsString, + status: TestStatus, + message: Option, + properties: BTreeMap, +} + +/// A Test suite source code. +struct TestSuiteSource { + path: PathBuf, + bytes: OnceCell>, +} + +const REWRITE_RULES: &[(&str, &str)] = &[( + "/resources/WebIDLParser.js", + "/resources/webidl2/webidl2.js", +)]; + +impl TestSuiteSource { + /// Create a new test suite source. + fn new(source: impl AsRef) -> Self { + Self { + path: source.as_ref().to_path_buf(), + bytes: OnceCell::new(), + } + } + + fn read_to_string(&self) -> Result> { + fn read_string(slice: &[u8], size: usize) -> Option { + assert!(2 * size <= slice.len()); + let iter = (0..size).map(|i| u16::from_be_bytes([slice[2 * i], slice[2 * i + 1]])); + + std::char::decode_utf16(iter) + .collect::>() + .ok() + } + let buffer = std::fs::read(&self.path)?; + // Check if buffer contains UTF8 or UTF16. + let maybe_utf8 = String::from_utf8(buffer.clone()); + if let Ok(utf8) = maybe_utf8 { + Ok(utf8) + } else if let Some(utf16) = read_string(&buffer, buffer.len() / 2) { + Ok(utf16) + } else { + Err("Could not determine encoding".into()) + } + } + + fn source(&self) -> Source<'_, UTF16Input<'_>> { + let b = self.bytes.get_or_init(|| { + self.read_to_string() + .unwrap() + .encode_utf16() + .collect::>() + }); + Source::from_utf16(b).with_path(&self.path) + } + + fn scripts(&self) -> Result, Box> { + let mut scripts: Vec = Vec::new(); + let dir = self.path.parent().expect("Could not get parent directory"); + + 'outer: for script in self.meta()?.get("script").unwrap_or(&Vec::new()) { + let script = script + .split_once('?') + .map_or(script.to_string(), |(s, _)| s.to_string()); + + // Resolve the source path relative to the script path, but under the wpt_path. + let script_path = Path::new(&script); + let path = if script_path.is_relative() { + dir.join(script_path) + } else { + script_path.to_path_buf() + }; + + for (from, to) in REWRITE_RULES { + if path.to_string_lossy().as_ref() == *from { + scripts.push((*to).to_string()); + continue 'outer; + } + } + scripts.push(path.to_string_lossy().to_string()); + } + Ok(scripts) + } + + fn meta(&self) -> Result>, Box> { + let mut meta: BTreeMap> = BTreeMap::new(); + + // Read the whole file and extract the metadata. + let content = self.read_to_string()?; + for line in content.lines() { + if let Some(kv) = line.strip_prefix("// META:") { + let kv = kv.trim(); + if let Some((key, value)) = kv.split_once('=') { + meta.entry(key.to_string()) + .or_default() + .push(value.to_string()); + } + } else if !line.starts_with("//") && !line.is_empty() { + break; + } + } + + Ok(meta) + } +} + +/// Create the BOA context and add the necessary global objects for WPT. +fn create_context(wpt_path: &Path) -> (Context, logger::RecordingLogger) { + let mut context = Context::default(); + let logger = logger::RecordingLogger::new(); + boa_runtime::register( + &mut context, + RegisterOptions::new().with_console_logger(logger.clone()), + ) + .expect("Failed to register boa_runtime"); + + // Define self as the globalThis. + let global_this = context.global_object(); + context + .register_global_property(js_str!("self"), global_this, Attribute::all()) + .unwrap(); + + // Define location to be an empty URL. + let location = Url::new("about:blank").expect("Could not parse the location URL"); + let location = + Url::from_data(location, &mut context).expect("Could not create the location URL"); + context + .register_global_property(js_str!("location"), location, Attribute::all()) + .unwrap(); + + let harness_path = wpt_path.join("resources/testharness.js"); + let harness = Source::from_filepath(&harness_path).expect("Could not create source."); + + context + .eval(harness) + .expect("Failed to eval testharness.js"); + + (context, logger) +} + +/// The result callback for the WPT test. +fn result_callback__( + ContextData(logger): ContextData, + test: Test, + context: &mut Context, +) -> JsResult<()> { + // Check the logs if the test succeeded. + assert_eq!( + test.status, + TestStatus::Pass, + "Test {:?} failed with message:\n {:?}", + test.name.to_std_string_lossy(), + test.message.unwrap_or_default() + ); + + // Check the logs. + let logs = logger.all_logs(); + if let Some(log_regex) = test.properties.get(&js_string!("logs")) { + if let Ok(logs_re) = log_regex.try_js_into::>(context) { + for re in logs_re { + let passes = if let Some(re) = re.as_regexp() { + logs.iter().any(|log: &RecordingLogEvent| -> bool { + let s = JsString::from(log.msg.clone()); + re.test(s, context).unwrap_or(false) + }) + } else { + let re_str = re.to_string(context)?.to_std_string_escaped(); + logs.iter() + .any(|log: &RecordingLogEvent| -> bool { log.msg.contains(&re_str) }) + }; + assert!( + passes, + "Test {:?} failed to find log: {}", + test.name.to_std_string_lossy(), + re.display() + ); + } + } + } + + Ok(()) +} + +fn complete_callback__(ContextData(test_done): ContextData) { + test_done.done(); +} + +#[derive(Debug, Clone, Trace, Finalize, JsData)] +struct TestCompletion(Gc); + +impl TestCompletion { + fn new() -> Self { + Self(Gc::new(AtomicBool::new(false))) + } + + fn done(&self) { + self.0.store(true, std::sync::atomic::Ordering::SeqCst); + } + + fn is_done(&self) -> bool { + self.0.load(std::sync::atomic::Ordering::SeqCst) + } +} + +/// Load and execute the test file. +// This can be marked as allow unused because it would give false positives +// in clippy. +#[allow(unused)] +fn execute_test_file(path: &Path) { + let dir = path.parent().unwrap(); + let wpt_path = PathBuf::from( + std::env::var("WPT_ROOT").expect("Could not find WPT_ROOT environment variable"), + ); + let (mut context, logger) = create_context(&wpt_path); + let test_done = TestCompletion::new(); + + // Insert the logger to be able to access the logs after the test is done. + context.insert_data(logger.clone()); + context.insert_data(test_done.clone()); + + let function = result_callback__ + .into_js_function_copied(&mut context) + .to_js_function(context.realm()); + context + .register_global_property(js_str!("result_callback__"), function, Attribute::all()) + .expect("Could not register result_callback__"); + context + .eval(Source::from_bytes( + b"add_result_callback(result_callback__);", + )) + .expect("Could not eval add_result_callback"); + + let function = complete_callback__ + .into_js_function_copied(&mut context) + .to_js_function(context.realm()); + context + .register_global_property(js_str!("complete_callback__"), function, Attribute::all()) + .expect("Could not register complete_callback__"); + context + .eval(Source::from_bytes( + b"add_completion_callback(complete_callback__);", + )) + .expect("Could not eval add_completion_callback"); + + // Load the test. + let source = TestSuiteSource::new(path); + for script in source.scripts().expect("Could not get scripts") { + // Resolve the source path relative to the script path, but under the wpt_path. + let script_path = Path::new(&script); + let path = if script_path.is_relative() { + dir.join(script_path) + } else { + wpt_path.join(script_path.to_string_lossy().trim_start_matches('/')) + }; + + if path.exists() { + let source = Source::from_filepath(&path).expect("Could not parse source."); + if let Err(err) = context.eval(source) { + panic!("Could not eval script, path = {path:?}, err = {err:?}"); + } + } else { + panic!("Script does not exist, path = {path:?}"); + } + } + context + .eval(source.source()) + .expect("Could not evaluate source"); + context.run_jobs(); + + // Done() + context + .eval(Source::from_bytes(b"done()")) + .expect("Done unexpectedly threw an error."); + + let start = std::time::Instant::now(); + while !test_done.is_done() { + context.run_jobs(); + + assert!( + start.elapsed().as_secs() < 10, + "Test did not complete in 10 seconds." + ); + } +} + +/// Test the console with the WPT test suite. +#[cfg(not(clippy))] +#[rstest::rstest] +fn console( + #[base_dir = "${WPT_ROOT}"] + #[files("console/*.any.js")] + // TODO: The console-log-large-array.any.js test is too slow. + #[exclude("console-log-large-array.any.js")] + #[exclude("idlharness")] + path: PathBuf, +) { + execute_test_file(&path); +} + +/// Test the text encoder/decoder with the WPT test suite. +#[cfg(not(clippy))] +#[ignore] // TODO: support all encodings. +#[rstest::rstest] +fn encoding( + #[base_dir = "${WPT_ROOT}"] + #[files("encoding/textdecoder-*.any.js")] + #[exclude("idlharness")] + path: PathBuf, +) { + execute_test_file(&path); +} + +/// Test the URL class with the WPT test suite. +// A bunch of these tests are failing due to lack of support in the URL class, +// or missing APIs such as fetch. +#[cfg(not(clippy))] +#[rstest::rstest] +fn url( + #[base_dir = "${WPT_ROOT}"] + #[files("url/url-*.any.js")] + #[exclude("idlharness")] + // "Base URL about:blank cannot be a base" + #[exclude("url-searchparams.any.js")] + // "fetch is not defined" + #[exclude("url-origin.any.js")] + #[exclude("url-setters.any.js")] + #[exclude("url-constructor.any.js")] + path: PathBuf, +) { + execute_test_file(&path); +} diff --git a/tests/wpt/src/logger/mod.rs b/tests/wpt/src/logger/mod.rs new file mode 100644 index 00000000000..29a6fbf7b22 --- /dev/null +++ b/tests/wpt/src/logger/mod.rs @@ -0,0 +1,94 @@ +use boa_engine::{Context, Finalize, JsData, JsResult, Trace}; +use boa_gc::{Gc, GcRefCell}; +use boa_runtime::{ConsoleState, Logger}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// A unique index of all logs. +static UNIQUE: AtomicUsize = AtomicUsize::new(0); + +#[derive(Clone, Debug, Trace, Finalize, JsData)] +pub(crate) struct RecordingLogEvent { + pub index: usize, + pub indent: usize, + pub msg: String, +} + +impl RecordingLogEvent { + pub(crate) fn new(msg: String, state: &ConsoleState) -> Self { + Self { + index: UNIQUE.fetch_add(1, Ordering::SeqCst), + indent: state.indent(), + msg, + } + } +} + +#[derive(Trace, Finalize, JsData)] +struct RecordingLoggerInner { + pub log: Vec, + pub error: Vec, +} + +#[derive(Clone, Trace, Finalize, JsData)] +pub(crate) struct RecordingLogger { + inner: Gc>, +} + +impl Logger for RecordingLogger { + fn log(&self, msg: String, state: &ConsoleState, _: &mut Context) -> JsResult<()> { + self.inner + .borrow_mut() + .log + .push(RecordingLogEvent::new(msg, state)); + Ok(()) + } + + fn info(&self, msg: String, state: &ConsoleState, _: &mut Context) -> JsResult<()> { + self.inner + .borrow_mut() + .log + .push(RecordingLogEvent::new(msg, state)); + Ok(()) + } + + fn warn(&self, msg: String, state: &ConsoleState, _: &mut Context) -> JsResult<()> { + self.inner + .borrow_mut() + .log + .push(RecordingLogEvent::new(msg, state)); + Ok(()) + } + + fn error(&self, msg: String, state: &ConsoleState, _: &mut Context) -> JsResult<()> { + self.inner + .borrow_mut() + .error + .push(RecordingLogEvent::new(msg, state)); + Ok(()) + } +} + +impl RecordingLogger { + pub(crate) fn new() -> Self { + Self { + inner: Gc::new(GcRefCell::new(RecordingLoggerInner { + log: Vec::new(), + error: Vec::new(), + })), + } + } + + pub(crate) fn all_logs(&self) -> Vec { + let mut all: Vec = self.log().into_iter().chain(self.error()).collect(); + all.sort_by_key(|x| x.index); + all + } + + pub(crate) fn log(&self) -> Vec { + self.inner.borrow().log.clone() + } + + pub(crate) fn error(&self) -> Vec { + self.inner.borrow().error.clone() + } +}