From baf8accb5dc190ca66e5246b8291dc0118ec6d33 Mon Sep 17 00:00:00 2001 From: Andy Leiserson Date: Wed, 24 Sep 2025 11:05:11 -0700 Subject: [PATCH] [cts_runner] Print uncaptured errors to stderr This is useful when running test snippets. Update READMEs including to mention how to do that. --- Cargo.lock | 14 ++++++ Cargo.toml | 1 + cts_runner/Cargo.toml | 3 ++ cts_runner/README.md | 17 +++++++ cts_runner/src/bootstrap.js | 19 ++++++++ cts_runner/tests/features.js | 5 -- cts_runner/tests/integration.rs | 84 +++++++++++++++++++++++---------- deno_webgpu/README.md | 16 ++++--- 8 files changed, 123 insertions(+), 36 deletions(-) create mode 100644 cts_runner/README.md delete mode 100644 cts_runner/tests/features.js diff --git a/Cargo.lock b/Cargo.lock index 7ccdeeccbe0..b6149b07443 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -999,6 +999,7 @@ dependencies = [ "deno_webgpu", "deno_webidl", "env_logger", + "tempfile", "termcolor", "tokio", ] @@ -4074,6 +4075,19 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.8", + "windows-sys 0.61.0", +] + [[package]] name = "termcolor" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index bd58ec379ed..8c1f9512d75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -187,6 +187,7 @@ spirv = "0.3" static_assertions = "1.1" strum = { version = "0.27.1", default-features = false, features = ["derive"] } syn = "2.0.98" +tempfile = "3" toml = "0.9.0" trybuild = "1" tracy-client = "0.18" diff --git a/cts_runner/Cargo.toml b/cts_runner/Cargo.toml index 689297daf30..218a61c80df 100644 --- a/cts_runner/Cargo.toml +++ b/cts_runner/Cargo.toml @@ -22,3 +22,6 @@ deno_webidl.workspace = true deno_webgpu.workspace = true tokio = { workspace = true, features = ["full"] } termcolor.workspace = true + +[dev-dependencies] +tempfile.workspace = true diff --git a/cts_runner/README.md b/cts_runner/README.md new file mode 100644 index 00000000000..2144e49ef64 --- /dev/null +++ b/cts_runner/README.md @@ -0,0 +1,17 @@ +# cts_runner + +This crate contains infrastructure for running the WebGPU conformance tests on +Deno's `wgpu`-based implementation of WebGPU. + +Instructions for running the tests via the CTS `xtask` are in the +[top-level README](https://github.com/gfx-rs/wgpu/blob/trunk/README.md#webgpu-conformance-test-suite). +The file [revision.txt](./revision.txt) specifies the version of the CTS that +will be used by default. + +`cts_runner` is somewhat misnamed at this point, in that it is useful for +things other than just running the CTS: + +- The [tests](./tests) directory contains a few directed tests for + Deno's bindings to `wgpu`. +- Standalone JavaScript snippets that use WebGPU can be run + with a command like: `cargo run -p cts_runner -- test.js`. diff --git a/cts_runner/src/bootstrap.js b/cts_runner/src/bootstrap.js index 220a7201384..e4fa7eecaa5 100644 --- a/cts_runner/src/bootstrap.js +++ b/cts_runner/src/bootstrap.js @@ -224,6 +224,25 @@ const windowOrWorkerGlobalScope = { windowOrWorkerGlobalScope.console.enumerable = false; +// Print uncaptured WebGPU errors to stderr. This is useful when running +// standalone JavaScript test snippets. It isn't needed for the CTS, because the +// CTS uses error scopes. (The CTS also installs its own error handler with +// `addEventListener`, so having this here may result in printing duplicate +// errors from the CTS in some cases.) Printing uncaptured errors to stderr +// isn't desired as built-in behavior in Deno, because the console is reserved +// for the application. +// +// Note that catching an error here _does not_ result in a non-zero exit status. +const requestDevice = webgpu.GPUAdapter.prototype.requestDevice; +webgpu.GPUAdapter.prototype.requestDevice = function(desc) { + return requestDevice.call(this, desc).then((device) => { + device.onuncapturederror = (event) => { + core.print("cts_runner caught WebGPU error:" + event.error.message, true); + }; + return device; + }) +}; + const mainRuntimeGlobalProperties = { Window: globalInterfaces.windowConstructorDescriptor, window: util.readOnly(globalThis), diff --git a/cts_runner/tests/features.js b/cts_runner/tests/features.js deleted file mode 100644 index 23b693a3aa5..00000000000 --- a/cts_runner/tests/features.js +++ /dev/null @@ -1,5 +0,0 @@ -const adapter = await navigator.gpu.requestAdapter(); - -if (adapter.features.has("mappable-primary-buffers")) { - throw new TypeError("Adapter should not report support for wgpu native-only features"); -} diff --git a/cts_runner/tests/integration.rs b/cts_runner/tests/integration.rs index bb2118b078c..3554b6bc9df 100644 --- a/cts_runner/tests/integration.rs +++ b/cts_runner/tests/integration.rs @@ -3,12 +3,15 @@ // As of June 2025, these tests are not run in CI. use std::{ - fmt::{self, Debug, Display}, + ffi::OsStr, + io::Write, path::PathBuf, - process::Command, + process::{Command, Output}, str, }; +use tempfile::NamedTempFile; + pub fn target_dir() -> PathBuf { let current_exe = std::env::current_exe().unwrap(); let target_dir = current_exe.parent().unwrap().parent().unwrap(); @@ -24,38 +27,71 @@ pub fn cts_runner_exe_path() -> PathBuf { p } -pub struct JsError; +fn exec_cts_runner(script_file: impl AsRef) -> Output { + Command::new(cts_runner_exe_path()) + .arg(script_file) + .output() + .unwrap() +} -impl Display for JsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "JavaScript test returned an error") - } +fn exec_js_file(script_file: impl AsRef) { + let output = exec_cts_runner(script_file); + println!("{}", str::from_utf8(&output.stdout).unwrap()); + eprintln!("{}", str::from_utf8(&output.stderr).unwrap()); + assert!(output.status.success()); } -impl Debug for JsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self}") - } +fn check_js_stderr(script: &str, expected: &str) { + let mut tempfile = NamedTempFile::new().unwrap(); + tempfile.write_all(script.as_bytes()).unwrap(); + tempfile.flush().unwrap(); + let output = exec_cts_runner(tempfile.path()); + assert!( + output.stdout.is_empty(), + "unexpected output on stdout: {}", + str::from_utf8(&output.stdout).unwrap(), + ); + assert_eq!(str::from_utf8(&output.stderr).unwrap(), expected); + assert!(output.status.success()); } -type JsResult = Result<(), JsError>; +fn exec_js(script: &str) { + check_js_stderr(script, ""); +} -fn exec_js_test(script: &str) -> JsResult { - let output = Command::new(cts_runner_exe_path()) - .arg(script) - .output() - .unwrap(); - println!("{}", str::from_utf8(&output.stdout).unwrap()); - eprintln!("{}", str::from_utf8(&output.stderr).unwrap()); - output.status.success().then_some(()).ok_or(JsError) +#[test] +fn hello_compute_example() { + exec_js_file("examples/hello-compute.js"); } #[test] -fn hello_compute_example() -> JsResult { - exec_js_test("examples/hello-compute.js") +fn features() { + exec_js( + r#" + const adapter = await navigator.gpu.requestAdapter(); + + if (adapter.features.has("mappable-primary-buffers")) { + throw new TypeError("Adapter should not report support for wgpu native-only features"); + } + "#, + ); } #[test] -fn features() -> JsResult { - exec_js_test("tests/features.js") +fn uncaptured_error() { + check_js_stderr( + r#" + const code = `const val: u32 = 1.1;`; + + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + device.createShaderModule({ code }) + "#, + "cts_runner caught WebGPU error: +Shader '' parsing error: the type of `val` is expected to be `u32`, but got `{AbstractFloat}` + ┌─ wgsl:1:7 + │ +1 │ const val: u32 = 1.1; + │ ^^^ definition of `val`\n\n", + ); } diff --git a/deno_webgpu/README.md b/deno_webgpu/README.md index c419bfc60e0..3cedd111c99 100644 --- a/deno_webgpu/README.md +++ b/deno_webgpu/README.md @@ -14,13 +14,15 @@ a [wgpu trace](https://github.com/gfx-rs/wgpu/wiki/Debugging-wgpu-Applications#tracing-infrastructure) to the specified directory. -For testing this op crate will make use of the WebGPU conformance tests suite, -running through our WPT runner. This will be used to validate implementation -conformance. - -GitHub CI doesn't run with GPUs, so testing relies on software like DX WARP & -Vulkan lavapipe. Currently, only using DX WARP works, so tests are only run on -Windows. +This op crate is tested primarily by running the +[WebGPU conformance test suite](https://github.com/gpuweb/cts) using `wgpu`'s +[`cts_runner`](https://github.com/gfx-rs/wgpu/blob/trunk/README.md#webgpu-conformance-test-suite). +`cts_runner` also has a few +[directed tests](https://github.com/gfx-rs/wgpu/tree/trunk/cts_runner/tests) +to fill in missing coverage. + +GPU availability in GitHub CI is limited, so some configurations rely on +software like DX WARP & Vulkan lavapipe. ## Links