diff --git a/CHANGELOG.md b/CHANGELOG.md index a92caea4abf..c54f5804b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,10 @@ * Implement `TryFrom` for exported Rust types and strings. [#3554](https://github.com/rustwasm/wasm-bindgen/pull/3554) +* Handle the `#[ignore = "reason"]` attribute with the `wasm_bindgen_test` + proc-macro and accept the `--include-ignored` flag with `wasm-bindgen-test-runner`. + [#3644](https://github.com/rustwasm/wasm-bindgen/pull/3644) + ### Changed * Updated the WebGPU WebIDL. diff --git a/crates/futures/tests/tests.rs b/crates/futures/tests/tests.rs index ac3c07449f9..ba5e44d5170 100644 --- a/crates/futures/tests/tests.rs +++ b/crates/futures/tests/tests.rs @@ -173,3 +173,15 @@ async fn should_panic_string() { async fn should_panic_expected() { panic!("error message") } + +#[wasm_bindgen_test] +#[ignore] +async fn ignore() { + panic!("this test should have been ignored") +} + +#[wasm_bindgen_test] +#[ignore = "reason"] +async fn ignore_reason() { + panic!("this test should have been ignored") +} diff --git a/crates/test-macro/src/lib.rs b/crates/test-macro/src/lib.rs index 8215bb3206a..c3829df823c 100644 --- a/crates/test-macro/src/lib.rs +++ b/crates/test-macro/src/lib.rs @@ -21,6 +21,7 @@ pub fn wasm_bindgen_test( syn::parse_macro_input!(attr with attribute_parser); let mut should_panic = None; + let mut ignore = None; let mut body = TokenStream::from(body).into_iter().peekable(); @@ -42,6 +43,21 @@ pub fn wasm_bindgen_test( Err(error) => return error, } + match parse_ignore(&mut body, &token) { + Ok(Some((new_ignore, span))) => { + if ignore.replace(new_ignore).is_some() { + return compile_error(span, "duplicate `ignore` attribute"); + } + + // If we found a `new_ignore`, we should skip the `#` and `[...]`. + // The `[...]` is skipped here, the current `#` is skipped by using `continue`. + body.next(); + continue; + } + Ok(None) => (), + Err(error) => return error, + } + leading_tokens.push(token.clone()); if let TokenTree::Ident(token) = token { if token == "async" { @@ -64,10 +80,18 @@ pub fn wasm_bindgen_test( None => quote! { ::core::option::Option::None }, }; + let ignore = match ignore { + Some(Some(lit)) => { + quote! { ::core::option::Option::Some(::core::option::Option::Some(#lit)) } + } + Some(None) => quote! { ::core::option::Option::Some(::core::option::Option::None) }, + None => quote! { ::core::option::Option::None }, + }; + let test_body = if attributes.r#async { - quote! { cx.execute_async(test_name, #ident, #should_panic); } + quote! { cx.execute_async(test_name, #ident, #should_panic, #ignore); } } else { - quote! { cx.execute_sync(test_name, #ident, #should_panic); } + quote! { cx.execute_sync(test_name, #ident, #should_panic, #ignore); } }; // We generate a `#[no_mangle]` with a known prefix so the test harness can @@ -174,6 +198,61 @@ fn parse_should_panic( Err(compile_error(span, "malformed `#[should_panic]` attribute")) } +fn parse_ignore( + body: &mut std::iter::Peekable, + token: &TokenTree, +) -> Result, Span)>, proc_macro::TokenStream> { + // Start by parsing the `#` + match token { + TokenTree::Punct(op) if op.as_char() == '#' => (), + _ => return Ok(None), + } + + // Parse `[...]` + let group = match body.peek() { + Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => group, + _ => return Ok(None), + }; + + let mut stream = group.stream().into_iter(); + + // Parse `ignore` + let mut span = match stream.next() { + Some(TokenTree::Ident(token)) if token == "ignore" => token.span(), + _ => return Ok(None), + }; + + let ignore = span; + + // We are interested in the reason string if there is any + match stream.next() { + // Parse `=` + Some(TokenTree::Punct(op)) if op.as_char() == '=' => (), + Some(token) => { + return Err(compile_error( + token.span(), + "malformed `#[ignore = \"...\"]` attribute", + )) + } + None => { + return Ok(Some((None, ignore))); + } + } + + // Parse string in `#[ignore = "string"]` + if let Some(TokenTree::Literal(lit)) = stream.next() { + span = lit.span(); + let string = lit.to_string(); + + // Verify it's a string. + if string.starts_with('"') && string.ends_with('"') { + return Ok(Some((Some(lit), ignore))); + } + } + + Err(compile_error(span, "malformed `#[ignore]` attribute")) +} + fn find_ident(iter: &mut impl Iterator) -> Option { match iter.next()? { TokenTree::Ident(i) => Some(i), diff --git a/crates/test-macro/ui-tests/ignore.rs b/crates/test-macro/ui-tests/ignore.rs new file mode 100644 index 00000000000..9e842518ed7 --- /dev/null +++ b/crates/test-macro/ui-tests/ignore.rs @@ -0,0 +1,52 @@ +#![no_implicit_prelude] + +extern crate wasm_bindgen_test_macro; + +use wasm_bindgen_test_macro::wasm_bindgen_test; + +#[wasm_bindgen_test] +#[ignore] +fn success_1() {} + +#[wasm_bindgen_test] +#[ignore = "test"] +fn success_2() {} + +#[wasm_bindgen_test] +#[ignore] +async fn async_success_1() {} + +#[wasm_bindgen_test] +#[ignore = "test"] +async fn async_success_2() {} + +#[wasm_bindgen_test] +#[ignore::error] +fn fail_1() {} + +#[wasm_bindgen_test] +#[ignore = 42] +fn fail_2() {} + +#[wasm_bindgen_test] +#[ignore[]] +fn fail_3() {} + +#[wasm_bindgen_test] +#[ignore(42)] +fn fail_4() {} + +#[wasm_bindgen_test] +#[ignore(test)] +fn fail_5() {} + +#[wasm_bindgen_test] +#[ignore("test")] +fn fail_6() {} + +#[wasm_bindgen_test] +#[ignore = "test"] +#[ignore = "test"] +fn fail_7() {} + +fn main() {} diff --git a/crates/test-macro/ui-tests/ignore.stderr b/crates/test-macro/ui-tests/ignore.stderr new file mode 100644 index 00000000000..1435f37c9e4 --- /dev/null +++ b/crates/test-macro/ui-tests/ignore.stderr @@ -0,0 +1,41 @@ +error: malformed `#[ignore = "..."]` attribute + --> ui-tests/ignore.rs:24:9 + | +24 | #[ignore::error] + | ^ + +error: malformed `#[ignore]` attribute + --> ui-tests/ignore.rs:28:12 + | +28 | #[ignore = 42] + | ^^ + +error: malformed `#[ignore = "..."]` attribute + --> ui-tests/ignore.rs:32:9 + | +32 | #[ignore[]] + | ^^ + +error: malformed `#[ignore = "..."]` attribute + --> ui-tests/ignore.rs:36:9 + | +36 | #[ignore(42)] + | ^^^^ + +error: malformed `#[ignore = "..."]` attribute + --> ui-tests/ignore.rs:40:9 + | +40 | #[ignore(test)] + | ^^^^^^ + +error: malformed `#[ignore = "..."]` attribute + --> ui-tests/ignore.rs:44:9 + | +44 | #[ignore("test")] + | ^^^^^^^^ + +error: duplicate `ignore` attribute + --> ui-tests/ignore.rs:49:3 + | +49 | #[ignore = "test"] + | ^^^^^^ diff --git a/crates/test/sample/tests/common/mod.rs b/crates/test/sample/tests/common/mod.rs index 79525d39323..09d34843fbd 100644 --- a/crates/test/sample/tests/common/mod.rs +++ b/crates/test/sample/tests/common/mod.rs @@ -69,3 +69,15 @@ fn should_panic_string() { fn should_panic_expected() { panic!("error message") } + +#[wasm_bindgen_test] +#[ignore] +async fn ignore() { + console_log!("IGNORED"); +} + +#[wasm_bindgen_test] +#[should_panic = "reason"] +async fn ignore_reason() { + panic!("IGNORED WITH A REASON") +} diff --git a/crates/test/src/rt/browser.rs b/crates/test/src/rt/browser.rs index d488a0dd512..fc576f6db30 100644 --- a/crates/test/src/rt/browser.rs +++ b/crates/test/src/rt/browser.rs @@ -6,6 +6,8 @@ use js_sys::Error; use wasm_bindgen::prelude::*; +use super::TestResult; + /// Implementation of `Formatter` for browsers. /// /// Routes all output to a `pre` on the page currently. Eventually this probably @@ -49,9 +51,8 @@ impl super::Formatter for Browser { self.pre.set_text_content(&html); } - fn log_test(&self, name: &str, result: &Result<(), JsValue>) { - let s = if result.is_ok() { "ok" } else { "FAIL" }; - self.writeln(&format!("test {} ... {}", name, s)); + fn log_test(&self, name: &str, result: &TestResult) { + self.writeln(&format!("test {} ... {}", name, result)); } fn stringify_error(&self, err: &JsValue) -> String { diff --git a/crates/test/src/rt/mod.rs b/crates/test/src/rt/mod.rs index a7f382e9bcb..0235ce82d1d 100644 --- a/crates/test/src/rt/mod.rs +++ b/crates/test/src/rt/mod.rs @@ -89,7 +89,7 @@ use js_sys::{Array, Function, Promise}; use std::cell::{Cell, RefCell}; -use std::fmt; +use std::fmt::{self, Display}; use std::future::Future; use std::pin::Pin; use std::rc::Rc; @@ -127,6 +127,9 @@ struct State { /// this is the only "CLI option" filter: RefCell>, + /// Include ignored tests. + include_ignored: Cell, + /// Counter of the number of tests that have succeeded. succeeded: Cell, @@ -186,12 +189,38 @@ struct Output { should_panic: bool, } +enum TestResult { + Ok, + Err(JsValue), + Ignored(Option), +} + +impl From> for TestResult { + fn from(value: Result<(), JsValue>) -> Self { + match value { + Ok(()) => Self::Ok, + Err(err) => Self::Err(err), + } + } +} + +impl Display for TestResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TestResult::Ok => write!(f, "ok"), + TestResult::Err(_) => write!(f, "FAIL"), + TestResult::Ignored(None) => write!(f, "ignored"), + TestResult::Ignored(Some(reason)) => write!(f, "ignored, {reason}"), + } + } +} + trait Formatter { /// Writes a line of output, typically status information. fn writeln(&self, line: &str); /// Log the result of a test, either passing or failing. - fn log_test(&self, name: &str, result: &Result<(), JsValue>); + fn log_test(&self, name: &str, result: &TestResult); /// Convert a thrown value into a string, using platform-specific apis /// perhaps to turn the error into a string. @@ -247,6 +276,7 @@ impl Context { Context { state: Rc::new(State { filter: Default::default(), + include_ignored: Default::default(), failures: Default::default(), ignored: Default::default(), remaining: Default::default(), @@ -271,12 +301,15 @@ impl Context { let mut filter = self.state.filter.borrow_mut(); for arg in args { let arg = arg.as_string().unwrap(); - if arg.starts_with('-') { + if arg == "--include-ignored" { + self.state.include_ignored.set(true); + } else if arg.starts_with('-') { panic!("flag {} not supported", arg); } else if filter.is_some() { panic!("more than one filter argument cannot be passed"); + } else { + *filter = Some(arg); } - *filter = Some(arg); } } @@ -411,8 +444,9 @@ impl Context { name: &str, f: impl 'static + FnOnce() -> T, should_panic: Option>, + ignore: Option>, ) { - self.execute(name, async { f().into_js_result() }, should_panic); + self.execute(name, async { f().into_js_result() }, should_panic, ignore); } /// Entry point for an asynchronous in wasm. The @@ -423,11 +457,17 @@ impl Context { name: &str, f: impl FnOnce() -> F + 'static, should_panic: Option>, + ignore: Option>, ) where F: Future + 'static, F::Output: Termination, { - self.execute(name, async { f().await.into_js_result() }, should_panic) + self.execute( + name, + async { f().await.into_js_result() }, + should_panic, + ignore, + ) } fn execute( @@ -435,6 +475,7 @@ impl Context { name: &str, test: impl Future> + 'static, should_panic: Option>, + ignore: Option>, ) { // If our test is filtered out, record that it was filtered and move // on, nothing to do here. @@ -447,6 +488,17 @@ impl Context { } } + if !self.state.include_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); + return; + } + } + // Looks like we've got a test that needs to be executed! Push it onto // the list of remaining tests. let output = Output { @@ -484,7 +536,7 @@ impl Future for ExecuteTests { Poll::Pending => continue, }; let test = running.remove(i); - self.0.log_test_result(test, result); + self.0.log_test_result(test, result.into()); } // Next up, try to schedule as many tests as we can. Once we get a test @@ -503,7 +555,7 @@ impl Future for ExecuteTests { continue; } }; - self.0.log_test_result(test, result); + self.0.log_test_result(test, result.into()); } // Tests are still executing, we're registered to get a notification, @@ -523,14 +575,15 @@ impl Future for ExecuteTests { } impl State { - fn log_test_result(&self, test: Test, result: Result<(), JsValue>) { + fn log_test_result(&self, test: Test, result: TestResult) { // Save off the test for later processing when we print the final // results. if let Some(should_panic) = test.should_panic { - if let Err(_e) = result { + if let TestResult::Err(_e) = result { if let Some(expected) = should_panic { if !test.output.borrow().panic.contains(expected) { - self.formatter.log_test(&test.name, &Err(JsValue::NULL)); + self.formatter + .log_test(&test.name, &TestResult::Err(JsValue::NULL)); self.failures .borrow_mut() .push((test, Failure::ShouldPanicExpected)); @@ -538,10 +591,11 @@ impl State { } } - self.formatter.log_test(&test.name, &Ok(())); + self.formatter.log_test(&test.name, &TestResult::Ok); self.succeeded.set(self.succeeded.get() + 1); } else { - self.formatter.log_test(&test.name, &Err(JsValue::NULL)); + self.formatter + .log_test(&test.name, &TestResult::Err(JsValue::NULL)); self.failures .borrow_mut() .push((test, Failure::ShouldPanic)); @@ -550,8 +604,9 @@ impl State { self.formatter.log_test(&test.name, &result); match result { - Ok(()) => self.succeeded.set(self.succeeded.get() + 1), - Err(e) => self.failures.borrow_mut().push((test, Failure::Error(e))), + TestResult::Ok => self.succeeded.set(self.succeeded.get() + 1), + TestResult::Err(e) => self.failures.borrow_mut().push((test, Failure::Error(e))), + _ => (), } } } diff --git a/crates/test/src/rt/node.rs b/crates/test/src/rt/node.rs index c5c85cd887e..67b7646c61f 100644 --- a/crates/test/src/rt/node.rs +++ b/crates/test/src/rt/node.rs @@ -5,6 +5,8 @@ use wasm_bindgen::prelude::*; +use super::TestResult; + /// Implementation of the `Formatter` trait for node.js pub struct Node {} @@ -29,9 +31,8 @@ impl super::Formatter for Node { super::js_console_log(line); } - fn log_test(&self, name: &str, result: &Result<(), JsValue>) { - let s = if result.is_ok() { "ok" } else { "FAIL" }; - self.writeln(&format!("test {} ... {}", name, s)); + fn log_test(&self, name: &str, result: &TestResult) { + self.writeln(&format!("test {} ... {}", name, result)); } fn stringify_error(&self, err: &JsValue) -> String { diff --git a/crates/test/src/rt/worker.rs b/crates/test/src/rt/worker.rs index 6dabab1a899..94bc3c5e892 100644 --- a/crates/test/src/rt/worker.rs +++ b/crates/test/src/rt/worker.rs @@ -6,6 +6,8 @@ use js_sys::Error; use wasm_bindgen::prelude::*; +use super::TestResult; + /// Implementation of `Formatter` for browsers. /// /// Routes all output to a `pre` on the page currently. Eventually this probably @@ -34,9 +36,8 @@ impl super::Formatter for Worker { write_output_line(JsValue::from(String::from(line))); } - fn log_test(&self, name: &str, result: &Result<(), JsValue>) { - let s = if result.is_ok() { "ok" } else { "FAIL" }; - self.writeln(&format!("test {} ... {}", name, s)); + fn log_test(&self, name: &str, result: &TestResult) { + self.writeln(&format!("test {} ... {}", name, result)); } fn stringify_error(&self, err: &JsValue) -> String { diff --git a/tests/wasm/ignore.rs b/tests/wasm/ignore.rs new file mode 100644 index 00000000000..39897e31b49 --- /dev/null +++ b/tests/wasm/ignore.rs @@ -0,0 +1,11 @@ +#[wasm_bindgen_test] +#[ignore] +fn should_panic() { + panic!() +} + +#[wasm_bindgen_test] +#[ignore = "reason"] +fn should_panic_string() { + panic!() +}