From 54034a5dd92cef2a6ab99beccfe295688e7bfac0 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 17 Dec 2024 23:59:25 +0100 Subject: [PATCH] Add `--list`, `--ignored` and `--exact` to `wasm-bindgen-test-runner` (#4356) --- CHANGELOG.md | 6 ++ .../src/bin/wasm-bindgen-test-runner/deno.rs | 2 + .../bin/wasm-bindgen-test-runner/headless.rs | 13 ++-- .../index-headless.html | 4 + .../src/bin/wasm-bindgen-test-runner/main.rs | 77 ++++++++++++++++--- .../src/bin/wasm-bindgen-test-runner/node.rs | 8 +- .../bin/wasm-bindgen-test-runner/server.rs | 6 ++ crates/test-macro/src/lib.rs | 14 +--- crates/test/src/rt/mod.rs | 70 ++++++++++++----- crates/test/src/rt/node.rs | 4 +- 10 files changed, 158 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ece0df74325..72bc905f84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ * Support importing memory and using `wasm_bindgen::module()` in Node.js. [#4349](https://github.com/rustwasm/wasm-bindgen/pull/4349) +* Add `--list`, `--ignored`, `--exact` and `--nocapture` to `wasm-bindgen-test-runner`, analogous to `cargo test`. + [#4356](https://github.com/rustwasm/wasm-bindgen/pull/4356) + ### Changed * Optional parameters are now typed as `T | undefined | null` to reflect the actual JS behavior. @@ -34,6 +37,9 @@ * Remove `WASM_BINDGEN_THREADS_MAX_MEMORY` and `WASM_BINDGEN_THREADS_STACK_SIZE`. The maximum memory size can be set via `-Clink-arg=--max-memory=`. The stack size of a thread can be set when initializing the thread via the `default` function. [#4363](https://github.com/rustwasm/wasm-bindgen/pull/4363) +* `console.*()` calls in tests are now always intercepted by default. To show them use `--nocapture`. When shown they are always printed in-place instead of after test results, analogous to `cargo test`. + [#4356](https://github.com/rustwasm/wasm-bindgen/pull/4356) + ### Fixed - Fixed using [JavaScript keyword](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#keywords) as identifiers not being handled correctly. diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/deno.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/deno.rs index 58c824e0b31..019eb6f9123 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/deno.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/deno.rs @@ -13,6 +13,7 @@ pub fn execute(module: &str, tmpdir: &Path, cli: Cli, tests: &[String]) -> Resul let mut js_to_execute = format!( r#"import * as wasm from "./{module}.js"; + const nocapture = {nocapture}; {console_override} window.__wbg_test_invoke = f => f(); @@ -21,6 +22,7 @@ pub fn execute(module: &str, tmpdir: &Path, cli: Cli, tests: &[String]) -> Resul const tests = []; "#, + nocapture = cli.nocapture.clone(), console_override = SHARED_SETUP, args = cli.into_args(), ); diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/headless.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/headless.rs index 6064475312d..d39229312b2 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/headless.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/headless.rs @@ -210,14 +210,15 @@ pub fn run( println!("output div contained:\n{}", tab(&output)); } } - if !logs.is_empty() { - println!("console.log div contained:\n{}", tab(&logs)); - } - if !errors.is_empty() { - println!("console.log div contained:\n{}", tab(&errors)); - } if !output.contains("test result: ok") { + if !logs.is_empty() { + println!("console.log div contained:\n{}", tab(&logs)); + } + if !errors.is_empty() { + println!("console.log div contained:\n{}", tab(&errors)); + } + bail!("some tests failed") } diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html b/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html index 790245f20db..e5af4a0eb61 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html @@ -18,10 +18,14 @@ } }; + // {NOCAPTURE} const wrap = method => { const og = orig(`console_${method}`); const on_method = `on_console_${method}`; console[method] = function (...args) { + if (nocapture) { + orig("output").apply(this, args); + } if (window[on_method]) { window[on_method](args); } diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs index ec402fadeeb..a16e860fb12 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs @@ -13,6 +13,7 @@ use anyhow::{bail, Context}; use clap::Parser; +use clap::ValueEnum; use std::env; use std::fs; use std::path::Path; @@ -34,14 +35,32 @@ struct Cli { help = "The file to test. `cargo test` passes this argument for you." )] file: PathBuf, - #[arg(long = "include-ignored", help = "Run ignored tests")] + #[arg(long, conflicts_with = "ignored", help = "Run ignored tests")] include_ignored: bool, + #[arg(long, conflicts_with = "include_ignored", help = "Run ignored tests")] + ignored: bool, + #[arg(long, help = "Exactly match filters rather than by substring")] + exact: bool, #[arg( - long = "skip", + long, value_name = "FILTER", help = "Skip tests whose names contain FILTER (this flag can be used multiple times)" )] skip: Vec, + #[arg(long, help = "List all tests and benchmarks")] + list: bool, + #[arg( + long, + help = "don't capture `console.*()` of each task, allow printing directly" + )] + nocapture: bool, + #[arg( + long, + value_enum, + value_name = "terse", + help = "Configure formatting of output" + )] + format: Option, #[arg( index = 2, value_name = "FILTER", @@ -54,6 +73,8 @@ struct Cli { impl Cli { fn into_args(self) -> String { let include_ignored = self.include_ignored; + let ignored = self.ignored; + let exact = self.exact; let skip = self.skip; let filter = if let Some(filter) = self.filter { &format!("\"{filter}\"") @@ -65,6 +86,8 @@ impl Cli { r#" // Forward runtime arguments. cx.include_ignored({include_ignored:?}); + cx.ignored({ignored:?}); + cx.exact({exact:?}); cx.skip({skip:?}); cx.filter({filter}); "# @@ -85,10 +108,6 @@ fn main() -> anyhow::Result<()> { .map(Path::new) .context("file to test is not a valid file, can't extract file name")?; - let tmpdir = tempfile::tempdir()?; - - let module = "wasm-bindgen-test"; - // Collect all tests that the test harness is supposed to run. We assume // that any exported function with the prefix `__wbg_test` is a test we need // to execute. @@ -98,12 +117,45 @@ fn main() -> anyhow::Result<()> { let mut tests = Vec::new(); for export in wasm.exports.iter() { - if !export.name.starts_with("__wbgt_") { - continue; + if export.name.starts_with("__wbgt_") { + tests.push(export.name.to_string()); } - tests.push(export.name.to_string()); } + if cli.list { + 'outer: for test in tests { + if !cli.ignored || test.starts_with("__wbgt_$") { + if let Some(filter) = &cli.filter { + let matches = if cli.exact { + test == *filter + } else { + test.contains(filter) + }; + + if !matches { + continue; + } + } + + for skip in &cli.skip { + if test.contains(skip) { + continue 'outer; + } + } + + println!("{}: test", test.split_once("::").unwrap().1); + } + } + + // Returning cleanly has the strange effect of outputting + // an additional empty line with spaces in it. + std::process::exit(0); + } + + let tmpdir = tempfile::tempdir()?; + + let module = "wasm-bindgen-test"; + // Right now there's a bug where if no tests are present then the // `wasm-bindgen-test` runtime support isn't linked in, so just bail out // early saying everything is ok. @@ -348,3 +400,10 @@ fn coverage_args(file_name: &Path) -> PathBuf { None => PathBuf::from(generated(file_name, &prefix)), } } + +/// Possible values for the `--format` option. +#[derive(Debug, Clone, Copy, ValueEnum)] +enum FormatSetting { + /// Display one character per test + Terse, +} diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs index c9bb0a7d4d4..68249ad7da9 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs @@ -15,13 +15,17 @@ const wrap = method => { const og = console[method]; const on_method = `on_console_${method}`; console[method] = function (...args) { - og.apply(this, args); + if (nocapture) { + og.apply(this, args); + } if (handlers[on_method]) { handlers[on_method](args); } }; }; +// save original `console.log` +global.__wbgtest_og_console_log = console.log; // override `console.log` and `console.error` etc... before we import tests to // ensure they're bound correctly in wasm. This'll allow us to intercept // all these calls and capture the output of tests @@ -53,6 +57,7 @@ pub fn execute( {fs}; {wasm}; + const nocapture = {nocapture}; {console_override} global.__wbg_test_invoke = f => f(); @@ -88,6 +93,7 @@ pub fn execute( r"import fs from 'node:fs/promises'".to_string() }, coverage = coverage.display(), + nocapture = cli.nocapture.clone(), console_override = SHARED_SETUP, args = cli.into_args(), ); diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs index cae510afc43..88999940e52 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs @@ -70,6 +70,7 @@ pub(crate) fn spawn( ) }; + let nocapture = cli.nocapture; let args = cli.into_args(); if test_mode.is_worker() { @@ -102,9 +103,13 @@ pub(crate) fn spawn( worker_script.push_str(&format!( r#" + const nocapture = {nocapture}; const wrap = method => {{ const on_method = `on_console_${{method}}`; self.console[method] = function (...args) {{ + if (nocapture) {{ + self.__wbg_test_output_writeln(args); + }} if (self[on_method]) {{ self[on_method](args); }} @@ -297,6 +302,7 @@ pub(crate) fn spawn( } else { include_str!("index.html") }; + let s = s.replace("// {NOCAPTURE}", &format!("const nocapture = {nocapture};")); let s = if !test_mode.is_worker() && test_mode.no_modules() { s.replace( "", diff --git a/crates/test-macro/src/lib.rs b/crates/test-macro/src/lib.rs index 6ff30a4450f..f123b167b0a 100644 --- a/crates/test-macro/src/lib.rs +++ b/crates/test-macro/src/lib.rs @@ -4,12 +4,8 @@ extern crate proc_macro; use proc_macro2::*; -use quote::format_ident; use quote::quote; use quote::quote_spanned; -use std::sync::atomic::*; - -static CNT: AtomicUsize = AtomicUsize::new(0); #[proc_macro_attribute] pub fn wasm_bindgen_test( @@ -94,18 +90,16 @@ pub fn wasm_bindgen_test( quote! { cx.execute_sync(test_name, #ident, #should_panic_par, #ignore_par); } }; - // We generate a `#[no_mangle]` with a known prefix so the test harness can - // later slurp up all of these functions and pass them as arguments to the - // main test harness. This is the entry point for all tests. - let name = format_ident!("__wbgt_{}_{}", ident, CNT.fetch_add(1, Ordering::SeqCst)); + let ignore_name = if ignore.is_some() { "$" } else { "" }; + let wasm_bindgen_path = attributes.wasm_bindgen_path; tokens.extend( quote! { const _: () = { #wasm_bindgen_path::__rt::wasm_bindgen::__wbindgen_coverage! { - #[no_mangle] + #[export_name = ::core::concat!("__wbgt_", #ignore_name, ::core::module_path!(), "::", ::core::stringify!(#ident))] #[cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))] - pub extern "C" fn #name(cx: &#wasm_bindgen_path::__rt::Context) { + extern "C" fn __wbgt_test(cx: &#wasm_bindgen_path::__rt::Context) { let test_name = ::core::concat!(::core::module_path!(), "::", ::core::stringify!(#ident)); #test_body } diff --git a/crates/test/src/rt/mod.rs b/crates/test/src/rt/mod.rs index 1b0343a641c..f5c4ae3bde7 100644 --- a/crates/test/src/rt/mod.rs +++ b/crates/test/src/rt/mod.rs @@ -136,17 +136,23 @@ struct State { /// Include ignored tests. include_ignored: Cell, + /// Include ignored tests. + ignored: Cell, + + /// Only execute with exactly matching name. + exact: Cell, + /// Tests to skip. skip: RefCell>, /// Counter of the number of tests that have succeeded. - succeeded: Cell, + succeeded_count: Cell, /// Counter of the number of tests that have been filtered - filtered: Cell, + filtered_count: Cell, /// Counter of the number of tests that have been ignored - ignored: Cell, + ignored_count: Cell, /// A list of all tests which have failed. /// @@ -349,13 +355,15 @@ impl Context { state: Rc::new(State { filter: Default::default(), include_ignored: Default::default(), + ignored: Default::default(), + exact: Default::default(), skip: Default::default(), failures: Default::default(), - filtered: Default::default(), - ignored: Default::default(), + filtered_count: Default::default(), + ignored_count: Default::default(), remaining: Default::default(), running: Default::default(), - succeeded: Default::default(), + succeeded_count: Default::default(), formatter, timer, }), @@ -367,6 +375,16 @@ impl Context { self.state.include_ignored.set(include_ignored); } + /// Handle `--ignored` flag. + pub fn ignored(&mut self, ignored: bool) { + self.state.ignored.set(ignored); + } + + /// Handle `--exact` flag. + pub fn exact(&mut self, exact: bool) { + self.state.exact.set(exact); + } + /// Handle `--skip` arguments. pub fn skip(&mut self, skip: Vec) { *self.state.skip.borrow_mut() = skip; @@ -546,28 +564,42 @@ impl Context { // on, nothing to do here. let filter = self.state.filter.borrow(); if let Some(filter) = &*filter { - if !name.contains(filter) { - let filtered = self.state.filtered.get(); - self.state.filtered.set(filtered + 1); + let exact = self.state.exact.get(); + + let matches = if exact { + name == filter + } else { + name.contains(filter) + }; + + if !matches { + let filtered = self.state.filtered_count.get(); + self.state.filtered_count.set(filtered + 1); return; } } for skip in &*self.state.skip.borrow() { if name.contains(skip) { - let filtered = self.state.filtered.get(); - self.state.filtered.set(filtered + 1); + let filtered = self.state.filtered_count.get(); + self.state.filtered_count.set(filtered + 1); return; } } - if !self.state.include_ignored.get() { + if self.state.ignored.get() && ignore.is_none() { + let filtered = self.state.filtered_count.get(); + self.state.filtered_count.set(filtered + 1); + return; + } + + if !self.state.include_ignored.get() && !self.state.ignored.get() { if let Some(ignore) = ignore { self.state .formatter .log_test(name, &TestResult::Ignored(ignore.map(str::to_owned))); - let ignored = self.state.ignored.get(); - self.state.ignored.set(ignored + 1); + let ignored = self.state.ignored_count.get(); + self.state.ignored_count.set(ignored + 1); return; } } @@ -665,7 +697,7 @@ impl State { } self.formatter.log_test(&test.name, &TestResult::Ok); - self.succeeded.set(self.succeeded.get() + 1); + self.succeeded_count.set(self.succeeded_count.get() + 1); } else { self.formatter .log_test(&test.name, &TestResult::Err(JsValue::NULL)); @@ -677,7 +709,7 @@ impl State { self.formatter.log_test(&test.name, &result); match result { - TestResult::Ok => self.succeeded.set(self.succeeded.get() + 1), + TestResult::Ok => self.succeeded_count.set(self.succeeded_count.get() + 1), TestResult::Err(e) => self.failures.borrow_mut().push((test, Failure::Error(e))), _ => (), } @@ -710,10 +742,10 @@ impl State { {} filtered out\ {}\n", if failures.len() == 0 { "ok" } else { "FAILED" }, - self.succeeded.get(), + self.succeeded_count.get(), failures.len(), - self.ignored.get(), - self.filtered.get(), + self.ignored_count.get(), + self.filtered_count.get(), finished_in, )); } diff --git a/crates/test/src/rt/node.rs b/crates/test/src/rt/node.rs index a505564f1e8..40e5276b3dd 100644 --- a/crates/test/src/rt/node.rs +++ b/crates/test/src/rt/node.rs @@ -19,6 +19,8 @@ extern "C" { type NodeError; #[wasm_bindgen(method, getter, js_class = "Error", structural)] fn stack(this: &NodeError) -> String; + #[wasm_bindgen(js_name = __wbgtest_og_console_log)] + fn og_console_log(s: &str); } impl Node { @@ -30,7 +32,7 @@ impl Node { impl super::Formatter for Node { fn writeln(&self, line: &str) { - super::js_console_log(line); + og_console_log(line); } fn log_test(&self, name: &str, result: &TestResult) {