diff --git a/Cargo.lock b/Cargo.lock index 402ed419b179..1b3fa70926e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4586,6 +4586,41 @@ dependencies = [ name = "ruffle_wstr" version = "0.1.0" +[[package]] +name = "rust-embed" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.48", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -5731,8 +5766,10 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vfs" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e4fe92cfc1bad19c19925d5eee4b30584dbbdee4ff10183b261acccbef74e2d" +source = "git+https://github.com/manuel-woelker/rust-vfs?rev=722ecc60b76c90fd7ce4c6dac6114bc13271cb65#722ecc60b76c90fd7ce4c6dac6114bc13271cb65" +dependencies = [ + "rust-embed", +] [[package]] name = "vswhom" @@ -5990,6 +6027,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_test_runner" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "getrandom", + "image", + "js-sys", + "ruffle_render_wgpu", + "ruffle_test_framework", + "rust-embed", + "tracing", + "tracing-log", + "tracing-subscriber", + "tracing-wasm", + "vfs", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "webbrowser" version = "0.8.12" diff --git a/Cargo.toml b/Cargo.toml index d3f3537a8686..cefdcc973ab7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "flv", "web", "web/packages/extension/safari", + "web/packages/swf-tests/runner", "wstr", "scanner", "exporter", diff --git a/deny.toml b/deny.toml index daccccbf2e5f..c88832061614 100644 --- a/deny.toml +++ b/deny.toml @@ -71,4 +71,5 @@ unknown-git = "deny" # github.com organizations to allow git sources for github = [ "ruffle-rs", + "manuel-woelker", # https://github.com/manuel-woelker/rust-vfs ] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000000..c4ba92c89ad2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "ruffle", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/render/wgpu/src/backend.rs b/render/wgpu/src/backend.rs index c2c4b8490152..7c064e112e4a 100644 --- a/render/wgpu/src/backend.rs +++ b/render/wgpu/src/backend.rs @@ -85,6 +85,28 @@ impl WgpuRenderBackend { Self::new(Arc::new(descriptors), target) } + #[cfg(target_family = "wasm")] + pub async fn descriptors_for_offscreen_canvas( + backends: wgpu::Backends, + canvas: web_sys::OffscreenCanvas, + ) -> Result, Error> { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + ..Default::default() + }); + let surface = instance.create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas))?; + let (adapter, device, queue) = request_adapter_and_device( + backends, + &instance, + Some(&surface), + wgpu::PowerPreference::HighPerformance, + None, + ) + .await?; + let descriptors = Descriptors::new(instance, adapter, device, queue); + Ok(Arc::new(descriptors)) + } + /// # Safety /// See [`wgpu::SurfaceTargetUnsafe`] variants for safety requirements. #[cfg(not(target_family = "wasm"))] @@ -134,8 +156,8 @@ impl WgpuRenderBackend { } } -#[cfg(not(target_family = "wasm"))] impl WgpuRenderBackend { + #[cfg(not(target_family = "wasm"))] pub fn for_offscreen( size: (u32, u32), backend: wgpu::Backends, diff --git a/render/wgpu/src/utils.rs b/render/wgpu/src/utils.rs index 74ae0ad458e6..078996befcf4 100644 --- a/render/wgpu/src/utils.rs +++ b/render/wgpu/src/utils.rs @@ -154,7 +154,6 @@ pub fn capture_image R>( result } -#[cfg(not(target_family = "wasm"))] pub fn buffer_to_image( device: &wgpu::Device, buffer: &wgpu::Buffer, diff --git a/tests/framework/Cargo.toml b/tests/framework/Cargo.toml index e03878d92887..a40790e50b74 100644 --- a/tests/framework/Cargo.toml +++ b/tests/framework/Cargo.toml @@ -27,7 +27,7 @@ serde = "1.0.196" toml = "0.8.9" anyhow = "1.0.79" async-channel = "2.1.1" -vfs = "0.10.0" +vfs = { git = "https://github.com/manuel-woelker/rust-vfs", rev = "722ecc60b76c90fd7ce4c6dac6114bc13271cb65" } percent-encoding = "2.3.1" [features] diff --git a/tests/framework/src/options.rs b/tests/framework/src/options.rs index 36ba4be5ce7e..1755710a0585 100644 --- a/tests/framework/src/options.rs +++ b/tests/framework/src/options.rs @@ -146,7 +146,7 @@ impl RequiredFeatures { pub struct PlayerOptions { max_execution_duration: Option, viewport_dimensions: Option, - with_renderer: Option, + pub with_renderer: Option, with_audio: bool, with_video: bool, runtime: PlayerRuntime, @@ -371,7 +371,7 @@ impl ImageComparison { #[derive(Clone, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct RenderOptions { - optional: bool, + pub optional: bool, pub sample_count: u32, pub exclude_warp: bool, } diff --git a/tests/framework/src/results.rs b/tests/framework/src/results.rs new file mode 100644 index 000000000000..1d9a281bf7e2 --- /dev/null +++ b/tests/framework/src/results.rs @@ -0,0 +1,91 @@ +use crate::options::{Approximations, ImageComparison}; +use image::RgbaImage; + +#[derive(Default)] +pub struct TestResults { + pub results: Vec, +} + +impl TestResults { + pub fn success(&self) -> bool { + self.results.iter().all(|img| img.success()) + } + + pub fn add(&mut self, result: impl Into) { + self.results.push(result.into()); + } +} + +pub enum TestResult { + Trace(TraceComparisonResult), + Approximation(NumberApproximationResult), + Image(ImageComparisonResult), + InvalidTest(String), +} + +impl TestResult { + pub fn success(&self) -> bool { + match self { + TestResult::Trace(result) => result.success(), + TestResult::Approximation(result) => result.success(), + TestResult::Image(result) => result.success(), + TestResult::InvalidTest(_) => false, + } + } +} + +impl From for TestResult { + fn from(value: String) -> Self { + TestResult::InvalidTest(value) + } +} + +pub struct TraceComparisonResult { + pub expected: String, + pub actual: String, +} + +impl TraceComparisonResult { + pub fn success(&self) -> bool { + self.expected == self.actual + } +} + +pub struct NumberApproximationResult { + pub line: usize, + pub expected: f64, + pub actual: f64, + pub options: Approximations, + pub group: Option, + pub surrounding_text: Option, +} + +impl NumberApproximationResult { + pub fn success(&self) -> bool { + if let Some(surrounding_text) = &self.surrounding_text { + if !surrounding_text.success() { + return false; + } + } + // NaNs should be able to pass in an approx test. + if self.actual.is_nan() && self.expected.is_nan() { + return true; + } + self.options.compare(self.actual, self.expected) + } +} + +pub struct ImageComparisonResult { + pub name: String, + pub expected: RgbaImage, + pub actual: RgbaImage, + pub difference_color: Option, + pub difference_alpha: Option, + pub options: ImageComparison, +} + +impl ImageComparisonResult { + pub fn success(&self) -> bool { + self.expected == self.actual + } +} diff --git a/web/package-lock.json b/web/package-lock.json index 9aff5a1c4d60..777178b9fc0b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -486,6 +486,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -535,6 +546,20 @@ "node": ">=6.9.0" } }, + "node_modules/@cocalc/ansi-to-react": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@cocalc/ansi-to-react/-/ansi-to-react-7.0.0.tgz", + "integrity": "sha512-FOuHtOnuBtqTZSPR78Zg5w86/n+WJ/AOd0Y0PTh7Sx2TttyN3KjXRD8gSD8zEp1Ewf3Qv30tP3m8kNoPQa3lTw==", + "hasInstallScript": true, + "dependencies": { + "anser": "^2.1.1", + "escape-carriage": "^1.3.0" + }, + "peerDependencies": { + "react": "^16.3.2 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.3.2 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1089,6 +1114,54 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.24.8.tgz", + "integrity": "sha512-AuYeDoaR8jtUlUXtZ1IJ/6jtBkGnSpJXbGNzokBL87VDJ8opMq1Bgrc0szhK482ReQY6KZsMoZCVSb4xwalkBA==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.1", + "aria-hidden": "^1.2.3", + "tabbable": "^6.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@fluent/bundle": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/@fluent/bundle/-/bundle-0.18.0.tgz", @@ -1377,6 +1450,43 @@ "node": ">= 0.4" } }, + "node_modules/@mantine/core": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.5.3.tgz", + "integrity": "sha512-Wvv6DJXI+GX9mmKG5HITTh/24sCZ0RoYQHdTHh0tOfGnEy+RleyhA82UjnMsp0n2NjfCISBwbiKgfya6b2iaFw==", + "dependencies": { + "@floating-ui/react": "^0.24.8", + "clsx": "2.0.0", + "react-number-format": "^5.3.1", + "react-remove-scroll": "^2.5.7", + "react-textarea-autosize": "8.5.3", + "type-fest": "^3.13.1" + }, + "peerDependencies": { + "@mantine/hooks": "7.5.3", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@mantine/core/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mantine/hooks": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.5.3.tgz", + "integrity": "sha512-mFI448mAs12v8FrgSVhytqlhTVrEjIfd/PqPEfwJu5YcZIq4YZdqpzJIUbANnRrFSvmoQpDb1PssdKx7Ds35hw==", + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1482,6 +1592,23 @@ "node": ">=12" } }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.5.1.tgz", @@ -1668,6 +1795,216 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@swc/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.1.tgz", + "integrity": "sha512-3y+Y8js+e7BbM16iND+6Rcs3jdiL28q3iVtYsCviYSSpP2uUVKkp5sJnCY4pg8AaVvyN7CGQHO7gLEZQ5ByozQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.2", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.4.1", + "@swc/core-darwin-x64": "1.4.1", + "@swc/core-linux-arm-gnueabihf": "1.4.1", + "@swc/core-linux-arm64-gnu": "1.4.1", + "@swc/core-linux-arm64-musl": "1.4.1", + "@swc/core-linux-x64-gnu": "1.4.1", + "@swc/core-linux-x64-musl": "1.4.1", + "@swc/core-win32-arm64-msvc": "1.4.1", + "@swc/core-win32-ia32-msvc": "1.4.1", + "@swc/core-win32-x64-msvc": "1.4.1" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.1.tgz", + "integrity": "sha512-ePyfx0348UbR4DOAW24TedeJbafnzha8liXFGuQ4bdXtEVXhLfPngprrxKrAddCuv42F9aTxydlF6+adD3FBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.1.tgz", + "integrity": "sha512-eLf4JSe6VkCMdDowjM8XNC5rO+BrgfbluEzAVtKR8L2HacNYukieumN7EzpYCi0uF1BYwu1ku6tLyG2r0VcGxA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.1.tgz", + "integrity": "sha512-K8VtTLWMw+rkN/jDC9o/Q9SMmzdiHwYo2CfgkwVT29NsGccwmNhCQx6XoYiPKyKGIFKt4tdQnJHKUFzxUqQVtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.1.tgz", + "integrity": "sha512-0e8p4g0Bfkt8lkiWgcdiENH3RzkcqKtpRXIVNGOmVc0OBkvc2tpm2WTx/eoCnes2HpTT4CTtR3Zljj4knQ4Fvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.1.tgz", + "integrity": "sha512-b/vWGQo2n7lZVUnSQ7NBq3Qrj85GrAPPiRbpqaIGwOytiFSk8VULFihbEUwDe0rXgY4LDm8z8wkgADZcLnmdUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.1.tgz", + "integrity": "sha512-AFMQlvkKEdNi1Vk2GFTxxJzbICttBsOQaXa98kFTeWTnFFIyiIj2w7Sk8XRTEJ/AjF8ia8JPKb1zddBWr9+bEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.1.tgz", + "integrity": "sha512-QX2MxIECX1gfvUVZY+jk528/oFkS9MAl76e3ZRvG2KC/aKlCQL0KSzcTSm13mOxkDKS30EaGRDRQWNukGpMeRg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.1.tgz", + "integrity": "sha512-OklkJYXXI/tntD2zaY8i3iZldpyDw5q+NAP3k9OlQ7wXXf37djRsHLV0NW4+ZNHBjE9xp2RsXJ0jlOJhfgGoFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.1.tgz", + "integrity": "sha512-MBuc3/QfKX9FnLOU7iGN+6yHRTQaPQ9WskiC8s8JFiKQ+7I2p25tay2RplR9dIEEGgVAu6L7auv96LbNTh+FaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.1.tgz", + "integrity": "sha512-lu4h4wFBb/bOK6N2MuZwg7TrEpwYXgpQf5R7ObNSXL65BwZ9BG8XRzD+dLJmALu8l5N08rP/TrpoKRoGT4WSxw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", + "dev": true + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -1680,6 +2017,31 @@ "node": ">=14.16" } }, + "node_modules/@tabler/icons": { + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-2.47.0.tgz", + "integrity": "sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-2.47.0.tgz", + "integrity": "sha512-iqly2FvCF/qUbgmvS8E40rVeYY7laltc5GUjRxQj59DuX0x/6CpKHTXt86YlI2whg4czvd/c8Ce8YR08uEku0g==", + "dependencies": { + "@tabler/icons": "2.47.0", + "prop-types": "^15.7.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@testim/chrome-version": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz", @@ -1832,6 +2194,12 @@ "@types/node": "*" } }, + "node_modules/@types/css-modules": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/css-modules/-/css-modules-1.0.5.tgz", + "integrity": "sha512-oeKafs/df9lwOvtfiXVliZsocFVOexK9PZtLQWuPeuVCFR7jwiqlg60lu80JTe5NFNtH3tnV6Fs/ySR8BUPHAw==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.44.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", @@ -2006,7 +2374,7 @@ "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "devOptional": true }, "node_modules/@types/qs": { "version": "6.9.7", @@ -2024,7 +2392,7 @@ "version": "18.2.52", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.52.tgz", "integrity": "sha512-E/YjWh3tH+qsLKaUzgpZb5AY0ChVa+ZJzF7ogehVILrFpdQk6nC/WXOv0bfFEABbXbgNxLBGU7IIZByPKb6eBw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2085,7 +2453,7 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "devOptional": true }, "node_modules/@types/semver": { "version": "7.5.6", @@ -3009,6 +3377,11 @@ "ajv": "^6.9.1" } }, + "node_modules/anser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/anser/-/anser-2.1.1.tgz", + "integrity": "sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ==" + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -3199,6 +3572,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -3702,6 +4086,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001519", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz", @@ -4030,6 +4423,14 @@ "node": ">=0.10.0" } }, + "node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4528,7 +4929,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "devOptional": true }, "node_modules/dashdash": { "version": "1.14.1", @@ -4828,6 +5229,11 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/devtools-protocol": { "version": "0.0.1249869", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1249869.tgz", @@ -5204,6 +5610,11 @@ "node": ">=6" } }, + "node_modules/escape-carriage": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz", + "integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==" + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -6407,6 +6818,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-port": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz", @@ -7214,6 +7633,14 @@ "node": ">=10.13.0" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ip": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", @@ -9183,7 +9610,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9768,6 +10194,79 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-mixins": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-9.0.4.tgz", + "integrity": "sha512-XVq5jwQJDRu5M1XGkdpgASqLk37OqkH4JCFDXl/Dn7janOJjCTEKL+36cnRVy7bMtoBzALfO7bV7nTIsFnUWLA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "postcss-js": "^4.0.0", + "postcss-simple-vars": "^7.0.0", + "sugarss": "^4.0.1" + }, + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-preset-mantine": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.13.0.tgz", + "integrity": "sha512-1bv/mQz2K+/FixIMxYd83BYH7PusDZaI7LpUtKbb1l/5N5w6t1p/V9ONHfRJeeAZyfa6Xc+AtR+95VKdFXRH1g==", + "dev": true, + "dependencies": { + "postcss-mixins": "^9.0.4", + "postcss-nested": "^6.0.1" + }, + "peerDependencies": { + "postcss": ">=8.0.0" + } + }, "node_modules/postcss-resolve-nested-selector": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", @@ -9813,6 +10312,22 @@ "node": ">=4" } }, + "node_modules/postcss-simple-vars": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz", + "integrity": "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==", + "dev": true, + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.1" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -9916,6 +10431,21 @@ "node": ">=0.4.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -10221,6 +10751,18 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/react-number-format": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.3.1.tgz", + "integrity": "sha512-qpYcQLauIeEhCZUZY9jXZnnroOtdy3jYaS1zQ3M1Sr6r/KMOBEIGNIb7eKT19g2N1wbYgFgvDzs19hw5TrB8XQ==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -10230,6 +10772,101 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", + "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", + "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-virtuoso": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.6.3.tgz", + "integrity": "sha512-NcoSsf4B0OCx7U8i2s+VWe8b9e+FWzcN/5ly4hKjErynBzGONbWORZ1C5amUlWrPi6+HbUQ2PjnT4OpyQIpP9A==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16 || >=17 || >= 18", + "react-dom": ">=16 || >=17 || >= 18" + } + }, "node_modules/read-pkg": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", @@ -10451,6 +11088,11 @@ "node": ">=6.0.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/replace-in-file": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-7.1.0.tgz", @@ -10860,6 +11502,10 @@ "resolved": "packages/selfhosted", "link": true }, + "node_modules/ruffle-swf-tests": { + "resolved": "packages/swf-tests", + "link": true + }, "node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -11932,6 +12578,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sugarss": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-4.0.1.tgz", + "integrity": "sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11991,6 +12653,11 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -12438,8 +13105,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -12690,6 +13356,84 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", + "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/userhome": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.0.tgz", @@ -12823,6 +13567,42 @@ } } }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.1.tgz", + "integrity": "sha512-hogbZ6yT7+AqBaV6lK9JRNvJDn4/IJvHLu6ET06arNfo0t2IsyCaon7el9Xa8OumH+ESuq//SDf8xscZFE0rWw==", + "dev": true, + "dependencies": { + "@rollup/plugin-virtual": "^3.0.2", + "@swc/core": "^1.3.100", + "uuid": "^9.0.1" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, + "node_modules/vite-plugin-top-level-await/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.3.0.tgz", + "integrity": "sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==", + "dev": true, + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5" + } + }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -13835,6 +14615,38 @@ "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" } + }, + "packages/swf-tests": { + "name": "ruffle-swf-tests", + "version": "0.1.0", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@cocalc/ansi-to-react": "^7.0.0", + "@mantine/core": "^7.4.1", + "@mantine/hooks": "^7.4.1", + "@tabler/icons-react": "^2.46.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-virtuoso": "^4.6.3" + }, + "devDependencies": { + "@types/css-modules": "^1.0.5", + "@types/react": "^18.2.46", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^6.16.0", + "@typescript-eslint/parser": "^6.16.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.33", + "postcss-preset-mantine": "^1.12.3", + "postcss-simple-vars": "^7.0.1", + "typescript": "^5.3.3", + "vite": "^5.0.10", + "vite-plugin-top-level-await": "^1.4.1", + "vite-plugin-wasm": "^3.3.0" + } } } } diff --git a/web/package.json b/web/package.json index f55354489247..c3926c0d14b2 100644 --- a/web/package.json +++ b/web/package.json @@ -37,7 +37,7 @@ "chromedriver": "^121.0.0" }, "scripts": { - "build": "npm run build --workspace=ruffle-core && npm run build --workspace=ruffle-demo --workspace=ruffle-extension --workspace=ruffle-selfhosted", + "build": "npm run build --workspace=ruffle-core && npm run build --workspace=ruffle-demo --workspace=ruffle-extension --workspace=ruffle-selfhosted --workspace=ruffle-swf-tests", "build:debug": "cross-env NODE_ENV=development CARGO_FEATURES=avm_debug npm run build", "build:dual-wasm": "cross-env ENABLE_WASM_EXTENSIONS=true npm run build", "build:repro": "cross-env ENABLE_WASM_EXTENSIONS=true ENABLE_CARGO_CLEAN=true ENABLE_VERSION_SEAL=true npm run build", diff --git a/web/packages/swf-tests/.eslintrc.cjs b/web/packages/swf-tests/.eslintrc.cjs new file mode 100644 index 000000000000..ed7fe5c05b6f --- /dev/null +++ b/web/packages/swf-tests/.eslintrc.cjs @@ -0,0 +1,19 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "plugin:prettier/recommended", + ], + ignorePatterns: ["dist", ".eslintrc.cjs", "build"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh"], + rules: { + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, +}; diff --git a/web/packages/swf-tests/.gitignore b/web/packages/swf-tests/.gitignore new file mode 100644 index 000000000000..d16386367f7c --- /dev/null +++ b/web/packages/swf-tests/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/web/packages/swf-tests/LICENSE_APACHE b/web/packages/swf-tests/LICENSE_APACHE new file mode 100644 index 000000000000..1b5ec8b78e23 --- /dev/null +++ b/web/packages/swf-tests/LICENSE_APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/web/packages/swf-tests/LICENSE_MIT b/web/packages/swf-tests/LICENSE_MIT new file mode 100644 index 000000000000..941fe9938e26 --- /dev/null +++ b/web/packages/swf-tests/LICENSE_MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018 Ruffle LLC and Ruffle contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/web/packages/swf-tests/README.md b/web/packages/swf-tests/README.md new file mode 100644 index 000000000000..052d0614f27f --- /dev/null +++ b/web/packages/swf-tests/README.md @@ -0,0 +1,7 @@ +# ruffle-swf-tests + +ruffle-swf-tests is the web runner (and visualizer) for Ruffle's swf test framework. + +## Building, testing or contributing + +Please see [the ruffle-web README](https://github.com/ruffle-rs/ruffle/blob/master/web/README.md). diff --git a/web/packages/swf-tests/index.html b/web/packages/swf-tests/index.html new file mode 100644 index 000000000000..0acd01c5b7f3 --- /dev/null +++ b/web/packages/swf-tests/index.html @@ -0,0 +1,14 @@ + + + + + + + + Ruffle SWF Tests + + +
+ + + diff --git a/web/packages/swf-tests/package.json b/web/packages/swf-tests/package.json new file mode 100644 index 000000000000..88a0086ca708 --- /dev/null +++ b/web/packages/swf-tests/package.json @@ -0,0 +1,42 @@ +{ + "name": "ruffle-swf-tests", + "version": "0.1.0", + "description": "Ruffle Flash emulator tests", + "license": "(MIT OR Apache-2.0)", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "prebuild": "node tools/build_wasm.js", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@cocalc/ansi-to-react": "^7.0.0", + "@mantine/core": "^7.4.1", + "@mantine/hooks": "^7.4.1", + "@tabler/icons-react": "^2.46.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-virtuoso": "^4.6.3" + }, + "devDependencies": { + "@types/css-modules": "^1.0.5", + "@types/react": "^18.2.46", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^6.16.0", + "@typescript-eslint/parser": "^6.16.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.33", + "postcss-preset-mantine": "^1.12.3", + "postcss-simple-vars": "^7.0.1", + "typescript": "^5.3.3", + "vite": "^5.0.10", + "vite-plugin-top-level-await": "^1.4.1", + "vite-plugin-wasm": "^3.3.0" + } +} diff --git a/web/packages/swf-tests/postcss.config.cjs b/web/packages/swf-tests/postcss.config.cjs new file mode 100644 index 000000000000..b09b7f921f7f --- /dev/null +++ b/web/packages/swf-tests/postcss.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + plugins: { + "postcss-preset-mantine": {}, + "postcss-simple-vars": { + variables: { + "mantine-breakpoint-xs": "36em", + "mantine-breakpoint-sm": "48em", + "mantine-breakpoint-md": "62em", + "mantine-breakpoint-lg": "75em", + "mantine-breakpoint-xl": "88em", + }, + }, + }, +}; diff --git a/web/packages/swf-tests/runner/Cargo.toml b/web/packages/swf-tests/runner/Cargo.toml new file mode 100644 index 000000000000..09f29f8221e3 --- /dev/null +++ b/web/packages/swf-tests/runner/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "web_test_runner" +authors = ["Nathan Adams "] +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "=0.2.91" +js-sys = "0.3.67" +ruffle_test_framework = { path = "../../../../tests/framework" } +ruffle_render_wgpu = { path = "../../../../render/wgpu", features = ["webgl"] } +vfs = { git = "https://github.com/manuel-woelker/rust-vfs", rev = "722ecc60b76c90fd7ce4c6dac6114bc13271cb65", features = ["embedded-fs"] } +rust-embed = { version = "8.0.0", features = ["include-exclude"] } +getrandom = { version = "0.2", features = ["js"] } +anyhow = "1.0.79" +chrono = { version = "0.4", default-features = false, features = ["wasmbind", "clock"] } +tracing-subscriber = { workspace = true } +tracing = { workspace = true } +tracing-log = "0.2.0" +tracing-wasm = "0.2.1" +wasm-bindgen-futures = "0.4.40" +image = { version = "0.24.8", default-features = false, features = ["png"] } + +[dependencies.web-sys] +version = "0.3.67" +features = ["Window"] \ No newline at end of file diff --git a/web/packages/swf-tests/runner/src/lib.rs b/web/packages/swf-tests/runner/src/lib.rs new file mode 100644 index 000000000000..a39bfe5e964a --- /dev/null +++ b/web/packages/swf-tests/runner/src/lib.rs @@ -0,0 +1,285 @@ +use image::RgbaImage; +use ruffle_render_wgpu::backend::WgpuRenderBackend; +use ruffle_render_wgpu::descriptors::Descriptors; +use ruffle_render_wgpu::target::TextureTarget; +use ruffle_render_wgpu::wgpu; +use ruffle_test_framework::environment::{Environment, RenderBackend, RenderInterface}; +use ruffle_test_framework::options::{RenderOptions, TestOptions}; +use ruffle_test_framework::runner::{TestRunner, TestStatus}; +use ruffle_test_framework::test::Test; +use rust_embed::RustEmbed; +use std::sync::Arc; +use vfs::{AltrootFS, EmbeddedFS, VfsPath, VfsResult}; +use wasm_bindgen::prelude::*; + +#[derive(RustEmbed, Debug)] +#[folder = "../../../../tests/tests/swfs"] +#[exclude = "*.fla"] +struct TestAssets; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = onPanic)] + fn handle_panic(error: JsError); +} + +#[wasm_bindgen(start)] +fn start() { + std::panic::set_hook(Box::new(|panic| { + let error = JsError::new(&panic.to_string()); + handle_panic(error); + })); +} + +fn find_tests(output: &mut Vec, folder: VfsPath) -> VfsResult<()> { + for path in folder.read_dir()? { + if path.is_dir()? { + find_tests(output, path.clone())?; + if let Some(test) = TestInfo::from_path(path) { + output.push(test); + } + } + } + Ok(()) +} + +#[wasm_bindgen] +pub struct TestInfo(Test); + +impl TestInfo { + pub fn from_path(path: VfsPath) -> Option { + if let Ok(options_file) = path.join("test.toml") { + if options_file.is_file().ok()? { + if let Ok(options) = TestOptions::read(&options_file) { + let name = path.as_str(); + if let Ok(test) = Test::from_options( + options, + VfsPath::new(AltrootFS::new(path.clone())), + name.to_string(), + ) { + return Some(TestInfo(test)); + } + } + } + } + None + } +} + +#[wasm_bindgen] +impl TestInfo { + #[wasm_bindgen(getter)] + pub fn name(&self) -> String { + self.0.name.to_string() + } + + #[wasm_bindgen(getter)] + pub fn ignored(&self) -> bool { + self.0.options.ignore + } + + #[wasm_bindgen(getter)] + pub fn known_failure(&self) -> bool { + self.0.options.known_failure + } + + #[wasm_bindgen(getter)] + pub fn should_run(&self) -> bool { + true //self.0.should_run(true, &WebEnvironment) + } + + #[wasm_bindgen(getter)] + pub fn wants_renderer(&self) -> String { + match &self.0.options.player_options.with_renderer { + None => "no", + Some(r) if r.optional => "optional", + Some(_) => "required", + } + .to_string() + } + + pub async fn start(&self) -> Result { + let environment = WgpuEnvironment::new(wgpu::Backends::GL).await; + if !self.0.should_run(false, &environment) { + return Ok(ActiveTest { + runner: None, + sleep: 0, + error: None, + finished: true, + skipped: true, + }); + } + + match self.0.create_test_runner(&environment) { + Ok(runner) => Ok(ActiveTest { + runner: Some(runner), + sleep: 0, + error: None, + finished: false, + skipped: false, + }), + Err(e) => Err(e.to_string()), + } + } +} + +#[wasm_bindgen] +pub struct ActiveTest { + runner: Option, + sleep: u32, + error: Option, + finished: bool, + skipped: bool, +} + +#[wasm_bindgen] +impl ActiveTest { + pub fn tick(&mut self) { + if self.finished { + return; + } + if let Some(runner) = &mut self.runner { + runner.tick(); + } + } + + pub fn run(&mut self) { + if self.finished { + return; + } + match self.runner.as_mut().map(|runner| runner.test()) { + Some(Ok(TestStatus::Continue)) => { + self.sleep = 0; + } + Some(Ok(TestStatus::Sleep(duration))) => { + self.sleep = duration.as_millis() as u32; + } + Some(Ok(TestStatus::Finished)) => { + self.error = None; + self.finished = true; + } + Some(Err(e)) => { + self.error = Some(e.to_string()); + self.finished = true; + } + None => { + // test is none, but JS is trying to run it anyway... silly js developer + self.finished = true; + } + } + } + + #[wasm_bindgen(getter)] + pub fn known_failure(&self) -> bool { + self.runner + .as_ref() + .map(|runner| runner.options().known_failure) + .unwrap_or_default() + } + + #[wasm_bindgen(getter)] + pub fn error(&self) -> Option { + self.error.clone() + } + + #[wasm_bindgen(getter)] + pub fn finished(&self) -> bool { + self.finished + } + + #[wasm_bindgen(getter)] + pub fn skipped(&self) -> bool { + self.skipped + } + + #[wasm_bindgen(getter)] + pub fn sleep(&self) -> u32 { + self.sleep + } +} + +#[wasm_bindgen] +pub fn list_tests() -> Vec { + let root = VfsPath::new(EmbeddedFS::::new()); + let mut result = vec![]; + let _ = find_tests(&mut result, root); + + result +} + +#[wasm_bindgen] +pub fn get_test(name: String) -> Option { + let root = VfsPath::new(EmbeddedFS::::new()); + TestInfo::from_path(root.join(name).ok()?) +} + +struct WgpuEnvironment { + descriptors: Option>, +} + +impl WgpuEnvironment { + #[cfg(target_family = "wasm")] + pub async fn new(backends: wgpu::Backends) -> Self { + if let Ok(canvas) = web_sys::OffscreenCanvas::new(10, 10) { + if let Ok(backend) = + WgpuRenderBackend::descriptors_for_offscreen_canvas(backends, canvas).await + { + return Self { + descriptors: Some(backend), + }; + } + } + Self { descriptors: None } + } + + #[cfg(not(target_family = "wasm"))] + pub async fn new(_backends: wgpu::Backends) -> Self { + Self { descriptors: None } + } +} + +impl Environment for WgpuEnvironment { + fn is_render_supported(&self, _requirements: &RenderOptions) -> bool { + self.descriptors.is_some() + } + + fn create_renderer( + &self, + width: u32, + height: u32, + ) -> Option<(Box, Box)> { + if let Some(descriptors) = self.descriptors.clone() { + let target = TextureTarget::new(&descriptors.device, (width, height)).expect( + "WGPU Texture Target creation must not fail, everything was checked ahead of time", + ); + + Some( (Box::new(WgpuRenderInterface(descriptors.clone())), Box::new( + WgpuRenderBackend::new(descriptors, target) + .expect("WGPU Render backend creation must not fail, everything was checked ahead of time"), + ))) + } else { + None + } + } +} + +struct WgpuRenderInterface(Arc); + +impl RenderInterface for WgpuRenderInterface { + fn name(&self) -> String { + let adapter_info = self.0.adapter.get_info(); + if adapter_info.backend == ruffle_render_wgpu::wgpu::Backend::Gl { + "wgpu-webgl".to_string() + } else { + format!("{}-{:?}", std::env::consts::OS, adapter_info.backend) + } + } + + fn capture(&self, backend: &mut Box) -> RgbaImage { + let renderer = backend + .downcast_mut::>() + .unwrap(); + + renderer.capture_frame().expect("Failed to capture image") + } +} diff --git a/web/packages/swf-tests/src/App.tsx b/web/packages/swf-tests/src/App.tsx new file mode 100644 index 000000000000..a7db843164ad --- /dev/null +++ b/web/packages/swf-tests/src/App.tsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from "react"; +import "@mantine/core/styles.css"; +import { + ColorSchemeScript, + Container, + MantineProvider, + Stack, +} from "@mantine/core"; +import { + AppToOrchestratorMessage, + OrchestratorToAppMessage, + TestInfo, + TestResult, +} from "./worker_api.tsx"; +import { TestAndResult, TestTable } from "./table.tsx"; +import { MenuBar } from "./menu.tsx"; +import { ProgressBar } from "./progress.tsx"; +import { TestVisibilityFilter } from "./filters.tsx"; + +interface Orchestrator extends Omit { + postMessage(command: AppToOrchestratorMessage): void; +} + +export function App() { + const [knownTests, setKnownTests] = useState([] as TestInfo[]); + const [testResults, setTestResults] = useState( + {} as Record, + ); + const [orchestrator, setOrchestrator] = useState(null); + const [numPassedTests, setNumPassedTests] = useState(0); + const [numFailedTests, setNumFailedTests] = useState(0); + const [numSkippedTests, setNumSkippedTests] = useState(0); + const [numQueuedTests, setNumQueuedTests] = useState(0); + const [visibilityFilter, setVisibilityFilter] = + useState((): TestVisibilityFilter => { + return { + state: { + failed: true, + running: true, + skipped: false, + success: false, + pending: true, + }, + renderer: { + no: true, + optional: true, + required: true, + }, + search: "", + }; + }); + + useEffect(() => { + const parsedHash = new URLSearchParams( + window.location.hash.substring(1), + ); + const worker = new Worker( + new URL("./workers/orchestrator.tsx", import.meta.url), + { + type: "module", + }, + ); + worker.onmessage = (e) => { + const message = e.data as OrchestratorToAppMessage; + switch (message.type) { + case "test_list": + setKnownTests(message.tests); + break; + case "update_test_result": + setTestResults((old) => { + const results = { ...old }; + results[message.name] = message.result; + return results; + }); + break; + case "progress_update": + setNumQueuedTests(message.total); + setNumPassedTests(message.passed); + setNumFailedTests(message.failed); + setNumSkippedTests(message.skipped); + break; + } + }; + worker.postMessage({ + type: "configure_workers", + amount: parseInt(parsedHash.get("runners") || "", 10) || 5, + }); + setOrchestrator(worker); + return () => { + worker.terminate(); + }; + }, []); + + const startTest = (name: string) => { + orchestrator!.postMessage({ type: "start_test", names: [name] }); + }; + + const items: TestAndResult[] = []; + const searchLower = visibilityFilter.search.toLowerCase() || ""; + for (const testInfo of knownTests) { + const result = testResults[testInfo.name]; + const state = result == null ? "pending" : result.state; + if ( + visibilityFilter.state[state] && + visibilityFilter.renderer[testInfo.wants_renderer] && + testInfo.name.toLowerCase().indexOf(searchLower) != -1 + ) { + items.push({ + test: testInfo, + results: result, + }); + } + } + + const startAllTests = () => { + orchestrator!.postMessage({ + type: "start_test", + names: items.map((test) => test.test.name), + }); + }; + + return ( + <> + + + + + + setVisibilityFilter((old) => ({ + ...old, + search, + })) + } + startAllTests={startAllTests} + /> + + + + + + + ); +} diff --git a/web/packages/swf-tests/src/filter.module.css b/web/packages/swf-tests/src/filter.module.css new file mode 100644 index 000000000000..ee6e4b81fb2e --- /dev/null +++ b/web/packages/swf-tests/src/filter.module.css @@ -0,0 +1,24 @@ +.button { + border: rem(1px) solid + light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + padding: var(--mantine-spacing-xs) var(--mantine-spacing-xs); + margin-bottom: var(--mantine-spacing-xs); + border-radius: var(--mantine-radius-md); + font-weight: 500; + transition: + color 100ms ease, + background-color 100ms ease, + border-color 100ms ease; + cursor: pointer; + + &[data-checked] { + background-color: var(--mantine-color-blue-filled); + border-color: var(--mantine-color-blue-filled); + color: var(--mantine-color-white); + } + + & * { + pointer-events: none; + user-select: none; + } +} diff --git a/web/packages/swf-tests/src/filters.tsx b/web/packages/swf-tests/src/filters.tsx new file mode 100644 index 000000000000..034a5dc842e9 --- /dev/null +++ b/web/packages/swf-tests/src/filters.tsx @@ -0,0 +1,129 @@ +import { RendererRequirement, TestState } from "./worker_api.tsx"; +import React from "react"; +import { Checkbox, Popover, UnstyledButton } from "@mantine/core"; +import classes from "./filter.module.css"; +import { IconFilter } from "@tabler/icons-react"; + +export type TestStateFilter = { + [state in TestState | "pending"]: boolean; +}; + +export type RendererRequirementFilter = { + [state in RendererRequirement]: boolean; +}; + +export interface TestVisibilityFilter { + state: TestStateFilter; + renderer: RendererRequirementFilter; + search: string; +} + +export interface FilterCheckboxProps { + filter: TestVisibilityFilter; + setFilter: (filter: TestVisibilityFilter) => void; +} + +function FilterCheckbox({ + checked, + setChecked, + name, +}: { + checked: boolean; + setChecked: (checked: boolean) => void; + name: string; +}) { + return ( + setChecked(event.currentTarget.checked)} + wrapperProps={{ + onClick: () => setChecked(!checked), + }} + /> + ); +} + +export function StateFilterCheckboxes({ + filter, + setFilter, +}: FilterCheckboxProps) { + const names: { [state in TestState | "pending"]: string } = { + skipped: "Skipped", + running: "Running", + failed: "Failed", + success: "Success", + pending: "Not Yet Ran", + }; + return ( + <> + {Object.entries(names).map(([key, label]) => ( + { + const newFilter: TestVisibilityFilter = { + ...filter, + }; + newFilter.state[key as TestState] = visible; + setFilter(newFilter); + }} + name={label} + /> + ))} + + ); +} + +export function RenderFilterCheckboxes({ + filter, + setFilter, +}: FilterCheckboxProps) { + const names: { [requirement in RendererRequirement]: string } = { + no: "Renderer Not Used", + optional: "Rendering Optional", + required: "Rendering Required", + }; + return ( + <> + {Object.entries(names).map(([key, label]) => ( + { + const newFilter: TestVisibilityFilter = { + ...filter, + }; + newFilter.renderer[key as RendererRequirement] = + visible; + setFilter(newFilter); + }} + name={label} + /> + ))} + + ); +} + +export function FilterButton({ + filter, + setFilter, + Checkboxes, +}: { + filter: TestVisibilityFilter; + setFilter: (filter: TestVisibilityFilter) => void; + Checkboxes: (props: FilterCheckboxProps) => React.JSX.Element; +}) { + return ( + + + + + + + + + + + ); +} diff --git a/web/packages/swf-tests/src/global.css b/web/packages/swf-tests/src/global.css new file mode 100644 index 000000000000..b598de27c97b --- /dev/null +++ b/web/packages/swf-tests/src/global.css @@ -0,0 +1,5 @@ +html, +body, +#root { + height: 100%; +} diff --git a/web/packages/swf-tests/src/main.tsx b/web/packages/swf-tests/src/main.tsx new file mode 100644 index 000000000000..673dffa9ddfb --- /dev/null +++ b/web/packages/swf-tests/src/main.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "./App.tsx"; +import "./global.css"; + +async function load() { + ReactDOM.createRoot(document.getElementById("root")!).render( + + + , + ); +} +load().catch(console.error); diff --git a/web/packages/swf-tests/src/menu.module.css b/web/packages/swf-tests/src/menu.module.css new file mode 100644 index 000000000000..6ecf9b94383a --- /dev/null +++ b/web/packages/swf-tests/src/menu.module.css @@ -0,0 +1,3 @@ +.search { + flex: 1; +} diff --git a/web/packages/swf-tests/src/menu.tsx b/web/packages/swf-tests/src/menu.tsx new file mode 100644 index 000000000000..a6de2ee916a0 --- /dev/null +++ b/web/packages/swf-tests/src/menu.tsx @@ -0,0 +1,58 @@ +import { useRef } from "react"; +import { ActionIcon, Button, Group, rem, TextInput } from "@mantine/core"; +import classNames from "./menu.module.css"; +import { IconArrowRight, IconSearch } from "@tabler/icons-react"; + +export function MenuBar({ + setSearch, + startAllTests, +}: { + setSearch: (search: string) => void; + startAllTests: () => void; +}) { + const searchInput = useRef(null); + + const applySearch = () => { + setSearch(searchInput.current?.value || ""); + }; + + return ( + + + } + rightSection={ + + + + } + /> + + + ); +} diff --git a/web/packages/swf-tests/src/progress.tsx b/web/packages/swf-tests/src/progress.tsx new file mode 100644 index 000000000000..9ca2dbf89d3c --- /dev/null +++ b/web/packages/swf-tests/src/progress.tsx @@ -0,0 +1,39 @@ +import { Progress } from "@mantine/core"; + +export function ProgressBar({ + numPassedTests, + numQueuedTests, + numFailedTests, + numSkippedTests, +}: { + numPassedTests: number; + numQueuedTests: number; + numFailedTests: number; + numSkippedTests: number; +}) { + return ( + + + {numPassedTests} + + + {numFailedTests} + + + {numSkippedTests} + + + ); +} diff --git a/web/packages/swf-tests/src/table.module.css b/web/packages/swf-tests/src/table.module.css new file mode 100644 index 000000000000..ce79e002b987 --- /dev/null +++ b/web/packages/swf-tests/src/table.module.css @@ -0,0 +1,3 @@ +.header { + background-color: var(--mantine-color-default); +} diff --git a/web/packages/swf-tests/src/table.tsx b/web/packages/swf-tests/src/table.tsx new file mode 100644 index 000000000000..fdd6cfe75394 --- /dev/null +++ b/web/packages/swf-tests/src/table.tsx @@ -0,0 +1,166 @@ +import { RendererRequirement, TestInfo, TestResult } from "./worker_api.tsx"; +import { + TableComponents as VirtuosoTableComponents, + TableVirtuoso, +} from "react-virtuoso"; +import { + Button, + Code, + Loader, + Table, + TableScrollContainer, + TableTbody, + TableTh, + TableThead, + TableTr, + Text, + Tooltip, +} from "@mantine/core"; +import React from "react"; +import Ansi from "@cocalc/ansi-to-react"; +import classes from "./table.module.css"; +import { + IconCircleCheck, + IconCircleCheckFilled, + IconX, +} from "@tabler/icons-react"; +import { + FilterButton, + RenderFilterCheckboxes, + StateFilterCheckboxes, + TestVisibilityFilter, +} from "./filters.tsx"; + +export interface TestAndResult { + test: TestInfo; + results?: TestResult; +} + +const TableComponents: VirtuosoTableComponents = { + Scroller: React.forwardRef((props, ref) => ( + + )), + Table: (props) => , + TableHead: React.forwardRef((props, ref) => ( + + )), + TableRow: (props) => , + TableBody: React.forwardRef((props, ref) => ( + + )), +}; + +function TestResults({ result }: { result: TestResult | null }) { + if (result?.state == "failed") { + return ( + + {result?.result} + {result?.stack} + + ); + } + if (result?.state == "skipped") { + return Could not run; + } + if (result?.state == "success") { + return Success!; + } + + return ; +} + +function RendererRequirementIcon({ + requirement, +}: { + requirement: RendererRequirement; +}) { + let Icon; + let tooltip; + switch (requirement) { + case "optional": + Icon = IconCircleCheck; + tooltip = "Optional"; + break; + case "required": + Icon = IconCircleCheckFilled; + tooltip = "Required"; + break; + case "no": + default: + Icon = IconX; + tooltip = "Not Used"; + break; + } + return ( + + + + ); +} + +export function TestTable({ + tests, + runTest, + filter, + setFilter, +}: { + tests: TestAndResult[]; + runTest: (name: string) => void; + filter: TestVisibilityFilter; + setFilter: (filter: TestVisibilityFilter) => void; +}) { + return ( + <> + ( + + Name + + Renderer + + + + Result + + + Actions + + )} + itemContent={(_index, test: TestAndResult) => ( + <> + {test.test.name} + + + + + {test.results && ( + + )} + + + + + + )} + /> + + ); +} diff --git a/web/packages/swf-tests/src/worker_api.tsx b/web/packages/swf-tests/src/worker_api.tsx new file mode 100644 index 000000000000..6426f18db4b3 --- /dev/null +++ b/web/packages/swf-tests/src/worker_api.tsx @@ -0,0 +1,62 @@ +export type RendererRequirement = "no" | "optional" | "required"; + +export interface TestInfo { + ignored: boolean; + known_failure: boolean; + name: string; + should_run: boolean; + wants_renderer: RendererRequirement; +} + +export interface ConfigureWorkers { + type: "configure_workers"; + amount: number; +} + +export type TestState = "skipped" | "running" | "failed" | "success"; + +export interface TestResult { + result?: string; + stack?: string; + state: TestState; +} + +export interface TestList { + type: "test_list"; + tests: TestInfo[]; +} + +export interface UpdateTestResult { + type: "update_test_result"; + name: string; + result: TestResult; +} + +export interface ProgressUpdate { + type: "progress_update"; + total: number; + done: number; + passed: number; + failed: number; + skipped: number; +} + +export type OrchestratorToAppMessage = + | TestList + | UpdateTestResult + | ProgressUpdate; + +export interface StartTest { + type: "start_test"; + names: string[]; +} + +export type AppToOrchestratorMessage = StartTest | ConfigureWorkers; + +export interface TestFinished { + type: "test_finished"; + result: TestResult; + needsRestart: boolean; +} + +export type RunnerToOrchestratorMessage = TestFinished; diff --git a/web/packages/swf-tests/src/workers/orchestrator.tsx b/web/packages/swf-tests/src/workers/orchestrator.tsx new file mode 100644 index 000000000000..bd3a8da5c051 --- /dev/null +++ b/web/packages/swf-tests/src/workers/orchestrator.tsx @@ -0,0 +1,178 @@ +import init, { list_tests, TestInfo } from "../../build/web_test_runner"; +import { + AppToOrchestratorMessage, + OrchestratorToAppMessage, + RendererRequirement, + RunnerToOrchestratorMessage, +} from "../worker_api.tsx"; + +function sendMessage(message: OrchestratorToAppMessage) { + postMessage(message); +} + +interface Runner extends Omit { + postMessage(command: string): void; +} + +let allTests: TestInfo[] = []; +const testsToRun: string[] = []; +const runnerPool: Runner[] = []; + +let numRunners = 0; + +async function start() { + await init(); + allTests = list_tests(); + sendMessage({ + type: "test_list", + tests: allTests.map((test) => { + return { + name: test.name, + ignored: test.ignored, + known_failure: test.known_failure, + should_run: test.should_run, + wants_renderer: test.wants_renderer as RendererRequirement, + }; + }), + }); +} +start().catch(console.error); + +let running = false; +let totalQueued = 0; +let totalDone = 0; +let totalPass = 0; +let totalFail = 0; +let totalSkip = 0; + +function createRunner() { + const runner = new Worker(new URL("./runner.tsx", import.meta.url), { + type: "module", + }) as Runner; + runnerPool.push(runner); +} + +function setRunnerCount(newCount: number) { + const delta = newCount - numRunners; + console.log(`Setting runner count to ${newCount} from ${numRunners}`); + if (delta > 0) { + for (let i = 0; i < delta; i++) { + createRunner(); + } + } else if (delta < 0) { + for (let i = 0; i < -delta; i++) { + const runner = runnerPool.pop(); + if (runner) { + runner.terminate(); + } + } + // TODO: If there's tests in progress, they might dangle some runners. + } + numRunners = newCount; +} + +function tick() { + running = false; + while (runnerPool.length > 0 && testsToRun.length > 0) { + const nextTest = testsToRun.shift()!; + const runner = runnerPool.pop()!; + console.log("Starting test", nextTest); + sendMessage({ + type: "update_test_result", + name: nextTest, + result: { + state: "running", + }, + }); + let finished = false; + runner.onmessage = (e) => { + if (finished) return; + finished = true; + const message = e.data as RunnerToOrchestratorMessage; + totalDone++; + switch (message.result.state) { + case "skipped": + totalSkip++; + break; + case "failed": + totalFail++; + break; + case "success": + totalPass++; + break; + } + sendMessage({ + type: "update_test_result", + name: nextTest, + result: message.result, + }); + sendMessage({ + type: "progress_update", + total: totalQueued, + done: totalDone, + failed: totalFail, + passed: totalPass, + skipped: totalSkip, + }); + if (message.needsRestart) { + console.error("Runner needs restart"); + runner.terminate(); + createRunner(); + } else { + runnerPool.push(runner); + } + queueTick(); + }; + runner.onerror = (e) => { + if (finished) return; + finished = true; + console.error("Runner failed", e.message); + runner.terminate(); + createRunner(); + testsToRun.unshift(nextTest); // Push it at the front + queueTick(); + }; + runner.postMessage(nextTest); + } +} + +function queueTick() { + if (!running) { + setTimeout(tick, 5); + running = true; + } +} + +function queueTests(tests: string[]) { + testsToRun.push(...tests); + queueTick(); +} + +addEventListener("message", (event) => { + const message = event.data as AppToOrchestratorMessage; + switch (message.type) { + case "start_test": + if (testsToRun.length == 0) { + // Reset these numbers when a *new* test starts and we finished last queue + totalDone = 0; + totalQueued = 0; + totalFail = 0; + totalPass = 0; + totalSkip = 0; + } + totalQueued += message.names.length; + sendMessage({ + type: "progress_update", + total: totalQueued, + done: totalDone, + failed: totalFail, + passed: totalPass, + skipped: totalSkip, + }); + queueTests(message.names); + break; + case "configure_workers": + setRunnerCount(message.amount); + break; + } +}); diff --git a/web/packages/swf-tests/src/workers/runner.tsx b/web/packages/swf-tests/src/workers/runner.tsx new file mode 100644 index 000000000000..7cb13f2ef0a2 --- /dev/null +++ b/web/packages/swf-tests/src/workers/runner.tsx @@ -0,0 +1,172 @@ +import init, { ActiveTest, get_test } from "../../build/web_test_runner"; +import { RunnerToOrchestratorMessage } from "../worker_api.tsx"; + +function sendMessage(message: RunnerToOrchestratorMessage) { + postMessage(message); +} + +function sleep(duration: number) { + return new Promise((resolve) => { + setTimeout(resolve, duration); + }); +} + +async function runTest(name: string): Promise { + await init(); + const test = catchPanic(() => get_test(name)); + if (test === undefined) { + return { + needsRestart: false, + result: { state: "skipped", result: "Unknown test" }, + type: "test_finished", + }; + } + + // [NA] Rule of thumb: panics need a restarted worker. If in catch block, `needsRestart: true` + + // Pull this out just in case we panic and get into a weird post-panic-state + const knownFailure = test.known_failure; + + let activeTest: ActiveTest; + try { + activeTest = await catchPanic(() => test.start()); + } catch (e) { + const error = e as Error; // Guaranteed by `catchPanic` + return { + needsRestart: true, + type: "test_finished", + result: { + result: error.message, + stack: error.stack, + state: "failed", + }, + }; + } + + while (!activeTest.finished) { + try { + catchPanic(() => activeTest.tick()); + await sleep(activeTest.sleep); + catchPanic(() => activeTest.run()); + } catch (e) { + freeIfPossible(activeTest); + if (knownFailure) { + return { + needsRestart: true, + type: "test_finished", + result: { state: "success" }, + }; + } else { + const error = e as Error; // Guaranteed by `catchPanic` + return { + needsRestart: true, + type: "test_finished", + result: { + result: error.message, + stack: error.stack, + state: "failed", + }, + }; + } + } + } + + let response: RunnerToOrchestratorMessage; + + if (activeTest.skipped) { + response = { + needsRestart: false, + type: "test_finished", + result: { state: "skipped" }, + }; + } else if (activeTest.error) { + if (knownFailure) { + response = { + needsRestart: false, + type: "test_finished", + result: { state: "success" }, + }; + } else { + response = { + needsRestart: false, + type: "test_finished", + result: { + state: "failed", + result: activeTest.error, + }, + }; + } + } else { + if (knownFailure) { + response = { + needsRestart: false, + type: "test_finished", + result: { + state: "failed", + result: `${name} was known to be failing, but now passes successfully. Please update it and remove \`known_failure = true\`!`, + }, + }; + } else { + response = { + needsRestart: false, + type: "test_finished", + result: { state: "success" }, + }; + } + } + + freeIfPossible(activeTest); + return response; +} + +addEventListener("message", (event) => { + const name = event.data as string; + runTest(name) + .then((result) => sendMessage(result)) + .catch((e) => { + setTimeout(function () { + throw catchPanic(() => e); + }); + }); +}); + +declare global { + interface WorkerGlobalScope { + onPanic?: (error: Error) => void; + } +} + +// When rust panics, it'll trigger our `onPanic` method below and *then* throw in the calling JS. +// Store the panic here, and use it in the catch block to be able to see what it was. +let panic: Error | null = null; + +self.onPanic = (error: Error) => { + panic = error; +}; + +/** + * Calls the given function, translating any panics to the real panic message rather than just "Unreachable executed" + * @param f Function to call + * @throws {Error} Either the true panic cause, or an {Error} wrapping what we know about the error + */ +function catchPanic(f: () => T): T { + try { + return f(); + } catch (e) { + if (panic) { + throw panic; + } else if (e instanceof Error) { + throw e; + } else { + throw new Error(e as string); + } + } +} + +function freeIfPossible(object: { free: () => void }) { + try { + object.free(); + } catch (error) { + console.error("Error trying to free rust object", error); + } +} diff --git a/web/packages/swf-tests/src/workers/tsconfig.json b/web/packages/swf-tests/src/workers/tsconfig.json new file mode 100644 index 000000000000..6d836bf3825c --- /dev/null +++ b/web/packages/swf-tests/src/workers/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "WebWorker"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + }, +} diff --git a/web/packages/swf-tests/tools/.eslintrc.yaml b/web/packages/swf-tests/tools/.eslintrc.yaml new file mode 100644 index 000000000000..485e5859fe33 --- /dev/null +++ b/web/packages/swf-tests/tools/.eslintrc.yaml @@ -0,0 +1,2 @@ +env: + node: true diff --git a/web/packages/swf-tests/tools/build_wasm.js b/web/packages/swf-tests/tools/build_wasm.js new file mode 100644 index 000000000000..bc1e1cb47bce --- /dev/null +++ b/web/packages/swf-tests/tools/build_wasm.js @@ -0,0 +1,125 @@ +import { execFileSync } from "child_process"; + +import process from "process"; + +function runWasmOpt({ path, flags }) { + let args = ["-o", path, "-O", "-g", path]; + if (flags) { + args = args.concat(flags); + } + execFileSync("wasm-opt", args, { + stdio: "inherit", + }); +} +function runWasmBindgen({ path, outName, flags, dir }) { + let args = [ + path, + "--target", + "web", + "--out-dir", + dir, + "--out-name", + outName, + ]; + if (flags) { + args = args.concat(flags); + } + execFileSync("wasm-bindgen", args, { + stdio: "inherit", + }); +} +function cargoBuild({ profile, features, rustFlags }) { + let args = [ + "build", + "--locked", + "--target", + "wasm32-unknown-unknown", + "-p", + "web_test_runner", + ]; + if (profile) { + args.push("--profile", profile); + } + if (process.env["CARGO_FEATURES"]) { + features = (features || []).concat( + process.env["CARGO_FEATURES"].split(","), + ); + } + if (features) { + args.push("--features", features.join(",")); + } + let totalRustFlags = process.env["RUSTFLAGS"] || ""; + if (rustFlags) { + if (totalRustFlags) { + totalRustFlags += ` ${rustFlags.join(" ")}`; + } else { + totalRustFlags = rustFlags.join(" "); + } + } + if (process.env["CARGO_FLAGS"]) { + args = args.concat(process.env["CARGO_FLAGS"].split(" ")); + } + execFileSync("cargo", args, { + env: Object.assign(Object.assign({}, process.env), { + RUSTFLAGS: totalRustFlags, + }), + stdio: "inherit", + }); +} +function cargoClean() { + execFileSync("cargo", ["clean"], { + stdio: "inherit", + }); +} +function buildWasm(profile, filename, optimise, extensions) { + const rustFlags = ["--cfg=web_sys_unstable_apis", "-Aunknown_lints"]; + const wasmBindgenFlags = []; + const wasmOptFlags = []; + const flavor = extensions ? "extensions" : "vanilla"; + if (extensions) { + rustFlags.push( + "-C", + "target-feature=+bulk-memory,+simd128,+nontrapping-fptoint,+sign-ext,+reference-types", + ); + wasmBindgenFlags.push("--reference-types"); + wasmOptFlags.push("--enable-reference-types"); + } + console.log(`Building ${flavor} with cargo...`); + cargoBuild({ + profile, + rustFlags, + }); + console.log(`Running wasm-bindgen on ${flavor}...`); + runWasmBindgen({ + path: `../../../target/wasm32-unknown-unknown/${profile}/web_test_runner.wasm`, + outName: filename, + dir: "build", + flags: wasmBindgenFlags, + }); + if (process.env["ENABLE_CARGO_CLEAN"]) { + console.log(`Running cargo clean...`); + cargoClean(); + } + if (optimise) { + console.log(`Running wasm-opt on ${flavor}...`); + runWasmOpt({ + path: `build/${filename}_bg.wasm`, + flags: wasmOptFlags, + }); + } +} +function detectWasmOpt() { + try { + execFileSync("wasm-opt", ["--version"]); + return true; + } catch (_a) { + return false; + } +} +const hasWasmOpt = detectWasmOpt(); +if (!hasWasmOpt) { + console.log( + "NOTE: Since wasm-opt could not be found (or it failed), the resulting module might not perform that well, but it should still work.", + ); +} +buildWasm("web-vanilla-wasm", "web_test_runner", hasWasmOpt, false); diff --git a/web/packages/swf-tests/tsconfig.json b/web/packages/swf-tests/tsconfig.json new file mode 100644 index 000000000000..24ddaa92f32d --- /dev/null +++ b/web/packages/swf-tests/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + }, + "include": ["src"], + "exclude": ["src/workers"], + "references": [{ "path": "./tsconfig.node.json" }], +} diff --git a/web/packages/swf-tests/tsconfig.node.json b/web/packages/swf-tests/tsconfig.node.json new file mode 100644 index 000000000000..26063d8571ef --- /dev/null +++ b/web/packages/swf-tests/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/packages/swf-tests/vite.config.ts b/web/packages/swf-tests/vite.config.ts new file mode 100644 index 000000000000..a178298cc953 --- /dev/null +++ b/web/packages/swf-tests/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import wasm from "vite-plugin-wasm"; +import topLevelAwait from "vite-plugin-top-level-await"; + +export default defineConfig({ + plugins: [react(), wasm(), topLevelAwait()], + base: "/ruffle-swf-tests-on-web/", +});