diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eed5b1fd78..02e23a00a4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,9 @@ * Added importing strings as `JsString` through `#[wasm_bindgen(thread_local, static_string)] static STRING: JsString = "a string literal";`. [#4055](https://github.com/rustwasm/wasm-bindgen/pull/4055) +* Added experimental test coverage support for `wasm-bindgen-test-runner`, see the guide for more information. + [#4060](https://github.com/rustwasm/wasm-bindgen/pull/4060) + ### Changed * Stabilize Web Share API. diff --git a/Cargo.toml b/Cargo.toml index 663ae407b7d..ca425da4c2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,11 @@ serde_derive = "1.0" wasm-bindgen-test-crate-a = { path = 'tests/crates/a' } wasm-bindgen-test-crate-b = { path = 'tests/crates/b' } +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(wasm_bindgen_unstable_test_coverage)', +] } + [workspace] members = [ "benchmarks", diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index 4add9859cd6..d11894518cb 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -220,6 +220,7 @@ impl ToTokens for ast::Struct { (quote! { #[automatically_derived] impl #wasm_bindgen::describe::WasmDescribe for #name { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { use #wasm_bindgen::__wbindgen_if_not_std; use #wasm_bindgen::describe::*; @@ -293,6 +294,7 @@ impl ToTokens for ast::Struct { #[doc(hidden)] // `allow_delayed` is whether it's ok to not actually free the `ptr` immediately // if it's still borrowed. + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] pub unsafe extern "C" fn #free_fn(ptr: u32, allow_delayed: u32) { use #wasm_bindgen::__rt::alloc::rc::Rc; @@ -401,6 +403,7 @@ impl ToTokens for ast::Struct { } impl #wasm_bindgen::describe::WasmDescribeVector for #name { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe_vector() { use #wasm_bindgen::describe::*; inform(VECTOR); @@ -484,6 +487,7 @@ impl ToTokens for ast::StructField { const _: () = { #[cfg_attr(all(target_arch = "wasm32", target_os = "unknown"), no_mangle)] #[doc(hidden)] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] pub unsafe extern "C" fn #getter(js: u32) -> #wasm_bindgen::convert::WasmRet<<#ty as #wasm_bindgen::convert::IntoWasmAbi>::Abi> { @@ -525,6 +529,7 @@ impl ToTokens for ast::StructField { const _: () = { #[no_mangle] #[doc(hidden)] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] pub unsafe extern "C" fn #setter( js: u32, #(#args,)* @@ -781,6 +786,7 @@ impl TryToTokens for ast::Export { all(target_arch = "wasm32", target_os = "unknown"), export_name = #export_name, )] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] pub unsafe extern "C" fn #generated_name(#(#args),*) -> #wasm_bindgen::convert::WasmRet<#projection::Abi> { #start_check @@ -932,6 +938,7 @@ impl ToTokens for ast::ImportType { use #wasm_bindgen::__rt::core; impl WasmDescribe for #rust_name { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { #description } @@ -1222,6 +1229,7 @@ impl ToTokens for ast::StringEnum { #[automatically_derived] impl #wasm_bindgen::describe::WasmDescribe for #enum_name { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { use #wasm_bindgen::describe::*; inform(STRING_ENUM); @@ -1563,6 +1571,7 @@ impl ToTokens for ast::Enum { #[automatically_derived] impl #wasm_bindgen::describe::WasmDescribe for #enum_name { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { use #wasm_bindgen::describe::*; inform(ENUM); @@ -1599,6 +1608,7 @@ impl ToTokens for ast::Enum { } impl #wasm_bindgen::describe::WasmDescribeVector for #enum_name { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe_vector() { use #wasm_bindgen::describe::*; inform(VECTOR); @@ -1795,6 +1805,7 @@ impl<'a, T: ToTokens> ToTokens for Descriptor<'a, T> { #(#attrs)* #[no_mangle] #[doc(hidden)] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] pub extern "C" fn #name() { use #wasm_bindgen::describe::*; // See definition of `link_mem_intrinsics` for what this is doing 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 585f8aa34ca..26959e9e57f 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs @@ -15,6 +15,7 @@ use anyhow::{anyhow, bail, Context}; use log::error; use std::env; use std::fs; +use std::path::Path; use std::path::PathBuf; use std::thread; use wasm_bindgen_cli_support::Bindgen; @@ -222,6 +223,8 @@ fn main() -> anyhow::Result<()> { b.split_linked_modules(true); } + let coverage = coverage_args(&tmpdir); + b.debug(debug) .input_module(module, wasm) .keep_debug(false) @@ -256,6 +259,7 @@ fn main() -> anyhow::Result<()> { &tests, test_mode, std::env::var("WASM_BINDGEN_TEST_NO_ORIGIN_ISOLATION").is_err(), + coverage, ) .context("failed to spawn server")?; let addr = srv.server_addr(); @@ -282,3 +286,28 @@ fn main() -> anyhow::Result<()> { } Ok(()) } + +fn coverage_args(tmpdir: &Path) -> PathBuf { + fn generated(tmpdir: &Path, prefix: &str) -> String { + let res = format!( + "{prefix}{}.profraw", + tmpdir.file_name().and_then(|s| s.to_str()).unwrap() + ); + res + } + + let prefix = env::var_os("WASM_BINDGEN_UNSTABLE_TEST_PROFRAW_PREFIX") + .map(|s| s.to_str().unwrap().to_string()) + .unwrap_or_default(); + + match env::var_os("WASM_BINDGEN_UNSTABLE_TEST_PROFRAW_OUT") { + Some(s) => { + let mut buf = PathBuf::from(s); + if buf.is_dir() { + buf.push(generated(tmpdir, &prefix)); + } + buf + } + None => PathBuf::from(generated(tmpdir, &prefix)), + } +} 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 9c434a00010..ad3678b1cae 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs @@ -1,8 +1,9 @@ use std::borrow::Cow; use std::ffi::OsString; use std::fs; +use std::io::{Read, Write}; use std::net::SocketAddr; -use std::path::Path; +use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context, Error}; use rouille::{Request, Response, Server}; @@ -18,9 +19,24 @@ pub(crate) fn spawn( tests: &[String], test_mode: TestMode, isolate_origin: bool, + coverage: PathBuf, ) -> Result Response + Send + Sync>, Error> { let mut js_to_execute = String::new(); + let cov_import = if test_mode.no_modules() { + "let __wbgtest_cov_dump = wasm_bindgen.__wbgtest_cov_dump;" + } else { + "__wbgtest_cov_dump," + }; + let cov_dump = r#" + // Dump the coverage data collected during the tests + const coverage = __wbgtest_cov_dump(); + await fetch("/__wasm_bindgen/coverage", { + method: "POST", + body: coverage + }); + "#; + let wbg_import_script = if test_mode.no_modules() { String::from( r#" @@ -30,6 +46,7 @@ pub(crate) fn spawn( let __wbgtest_console_info = wasm_bindgen.__wbgtest_console_info; let __wbgtest_console_warn = wasm_bindgen.__wbgtest_console_warn; let __wbgtest_console_error = wasm_bindgen.__wbgtest_console_error; + {cov_import} let init = wasm_bindgen; "#, ) @@ -43,6 +60,7 @@ pub(crate) fn spawn( __wbgtest_console_info, __wbgtest_console_warn, __wbgtest_console_error, + {cov_import} default as init, }} from './{}'; "#, @@ -116,6 +134,7 @@ pub(crate) fn spawn( cx.args({1:?}); await cx.run(tests.map(s => wasm[s])); + {cov_dump} }} port.onmessage = function(e) {{ @@ -250,6 +269,7 @@ pub(crate) fn spawn( cx.args({1:?}); await cx.run(test.map(s => wasm[s])); + {cov_dump} }} const tests = []; @@ -300,6 +320,16 @@ pub(crate) fn spawn( } return response; + } else if request.url() == "/__wasm_bindgen/coverage" { + return if let Err(e) = handle_coverage_dump(&coverage, request) { + let s: &str = &format!("Failed to dump coverage: {e}"); + log::error!("{s}"); + let mut ret = Response::text(s); + ret.status_code = 500; + ret + } else { + Response::empty_204() + }; } // Otherwise we need to find the asset here. It may either be in our @@ -351,6 +381,21 @@ pub(crate) fn spawn( } } +fn handle_coverage_dump(profraw_path: &Path, request: &Request) -> anyhow::Result<()> { + // This is run after all tests are done and dumps the data received in the request + // into a single profraw file + let mut profraw = std::fs::File::create(profraw_path)?; + let mut data = Vec::new(); + if let Some(mut r_data) = request.data() { + r_data.read_to_end(&mut data)?; + } + // Warnings about empty data should have already been handled by + // the client + + profraw.write_all(&data)?; + Ok(()) +} + /* * Set the Cross-Origin-Opener-Policy and Cross-Origin_Embedder-Policy headers * on the Server response to enable worker context sharing, as described in: diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml index e448a0d2285..9a738c7a533 100644 --- a/crates/macro/Cargo.toml +++ b/crates/macro/Cargo.toml @@ -31,3 +31,8 @@ trybuild = "1.0" wasm-bindgen = { path = "../.." } wasm-bindgen-futures = { path = "../futures" } web-sys = { path = "../web-sys", features = ["Worker"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(wasm_bindgen_unstable_test_coverage)', +] } diff --git a/crates/macro/src/lib.rs b/crates/macro/src/lib.rs index aa4ab3f066b..2c1fa471ce2 100644 --- a/crates/macro/src/lib.rs +++ b/crates/macro/src/lib.rs @@ -1,4 +1,9 @@ #![doc(html_root_url = "https://docs.rs/wasm-bindgen-macro/0.2")] +#![cfg_attr( + wasm_bindgen_unstable_test_coverage, + feature(allow_internal_unstable), + allow(internal_features) +)] extern crate proc_macro; @@ -6,6 +11,10 @@ use proc_macro::TokenStream; use quote::quote; #[proc_macro_attribute] +#[cfg_attr( + wasm_bindgen_unstable_test_coverage, + allow_internal_unstable(coverage_attribute) +)] pub fn wasm_bindgen(attr: TokenStream, input: TokenStream) -> TokenStream { match wasm_bindgen_macro_support::expand(attr.into(), input.into()) { Ok(tokens) => { @@ -32,6 +41,10 @@ pub fn wasm_bindgen(attr: TokenStream, input: TokenStream) -> TokenStream { /// let worker = Worker::new(&wasm_bindgen::link_to!(module = "/src/worker.js")); /// ``` #[proc_macro] +#[cfg_attr( + wasm_bindgen_unstable_test_coverage, + allow_internal_unstable(coverage_attribute) +)] pub fn link_to(input: TokenStream) -> TokenStream { match wasm_bindgen_macro_support::expand_link_to(input.into()) { Ok(tokens) => { @@ -48,6 +61,10 @@ pub fn link_to(input: TokenStream) -> TokenStream { } #[proc_macro_attribute] +#[cfg_attr( + wasm_bindgen_unstable_test_coverage, + allow_internal_unstable(coverage_attribute) +)] pub fn __wasm_bindgen_class_marker(attr: TokenStream, input: TokenStream) -> TokenStream { match wasm_bindgen_macro_support::expand_class_marker(attr.into(), input.into()) { Ok(tokens) => { diff --git a/crates/test-macro/Cargo.toml b/crates/test-macro/Cargo.toml index 61b7c19a0bd..89b6bf48223 100644 --- a/crates/test-macro/Cargo.toml +++ b/crates/test-macro/Cargo.toml @@ -20,3 +20,8 @@ syn = { version = "2.0", default-features = false, features = [ "parsing", "proc [dev-dependencies] wasm-bindgen-test = { path = "../test" } trybuild = "1.0" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(wasm_bindgen_unstable_test_coverage)', +] } diff --git a/crates/test-macro/src/lib.rs b/crates/test-macro/src/lib.rs index c3829df823c..d9bd781481a 100644 --- a/crates/test-macro/src/lib.rs +++ b/crates/test-macro/src/lib.rs @@ -1,6 +1,12 @@ //! See the README for `wasm-bindgen-test` for a bit more info about what's //! going on here. +#![cfg_attr( + wasm_bindgen_unstable_test_coverage, + feature(allow_internal_unstable), + allow(internal_features) +)] + extern crate proc_macro; use proc_macro2::*; @@ -12,6 +18,10 @@ use std::sync::atomic::*; static CNT: AtomicUsize = AtomicUsize::new(0); #[proc_macro_attribute] +#[cfg_attr( + wasm_bindgen_unstable_test_coverage, + allow_internal_unstable(coverage_attribute) +)] pub fn wasm_bindgen_test( attr: proc_macro::TokenStream, body: proc_macro::TokenStream, @@ -102,6 +112,7 @@ pub fn wasm_bindgen_test( tokens.extend( quote! { #[no_mangle] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] pub extern "C" fn #name(cx: &#wasm_bindgen_path::__rt::Context) { let test_name = ::core::concat!(::core::module_path!(), "::", ::core::stringify!(#ident)); #test_body diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 4a8dfdf21c3..e8135d9a6fb 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -18,5 +18,13 @@ wasm-bindgen-futures = { path = '../futures', version = '0.4.42' } wasm-bindgen-test-macro = { path = '../test-macro', version = '=0.3.42' } gg-alloc = { version = "1.0", optional = true } +[target.'cfg(all(target_arch = "wasm32", wasm_bindgen_unstable_test_coverage))'.dependencies] +minicov = "0.3" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(wasm_bindgen_unstable_test_coverage)', +] } + [lib] test = false diff --git a/crates/test/src/coverage.rs b/crates/test/src/coverage.rs new file mode 100644 index 00000000000..a28bc734780 --- /dev/null +++ b/crates/test/src/coverage.rs @@ -0,0 +1,24 @@ +use wasm_bindgen::prelude::wasm_bindgen; + +#[cfg(wasm_bindgen_unstable_test_coverage)] +#[wasm_bindgen] +pub fn __wbgtest_cov_dump() -> Vec { + let mut coverage = Vec::new(); + // SAFETY: this function is not thread-safe, but our whole test runner is running single-threaded. + unsafe { + minicov::capture_coverage(&mut coverage).unwrap(); + } + if coverage.is_empty() { + console_error!( + "Empty coverage data received. Make sure you compile the tests with + RUSTFLAGS=\"-Cinstrument-coverage -Zno-profile-runtime --emit=llvm-ir\"", + ); + } + coverage +} + +#[cfg(not(wasm_bindgen_unstable_test_coverage))] +#[wasm_bindgen] +pub fn __wbgtest_cov_dump() -> Vec { + Vec::new() +} diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index d05d46f5785..2827dbcbe4d 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -12,12 +12,21 @@ pub use wasm_bindgen_test_macro::wasm_bindgen_test; #[global_allocator] static A: gg_alloc::GgAlloc = gg_alloc::GgAlloc::new(std::alloc::System); +/// Helper macro which acts like `println!` only routes to `console.error` +/// instead. +#[macro_export] +macro_rules! console_error { + ($($arg:tt)*) => ( + $crate::__rt::console_error(&format_args!($($arg)*)) + ) +} + /// Helper macro which acts like `println!` only routes to `console.log` /// instead. #[macro_export] macro_rules! console_log { ($($arg:tt)*) => ( - $crate::__rt::log(&format_args!($($arg)*)) + $crate::__rt::console_log(&format_args!($($arg)*)) ) } @@ -87,3 +96,9 @@ macro_rules! wasm_bindgen_test_configure { #[path = "rt/mod.rs"] pub mod __rt; + +// Make this only available to wasm32 so that we don't +// import minicov on other archs. +// That way you can use normal cargo test without minicov +#[cfg(target_arch = "wasm32")] +mod coverage; diff --git a/crates/test/src/rt/mod.rs b/crates/test/src/rt/mod.rs index b89646026c1..838156155b3 100644 --- a/crates/test/src/rt/mod.rs +++ b/crates/test/src/rt/mod.rs @@ -239,16 +239,25 @@ extern "C" { #[doc(hidden)] pub fn js_console_log(s: &str); + #[wasm_bindgen(js_namespace = console, js_name = error)] + #[doc(hidden)] + pub fn js_console_error(s: &str); + // General-purpose conversion into a `String`. #[wasm_bindgen(js_name = String)] fn stringify(val: &JsValue) -> String; } /// Internal implementation detail of the `console_log!` macro. -pub fn log(args: &fmt::Arguments) { +pub fn console_log(args: &fmt::Arguments) { js_console_log(&args.to_string()); } +/// Internal implementation detail of the `console_error!` macro. +pub fn console_error(args: &fmt::Arguments) { + js_console_error(&args.to_string()); +} + #[wasm_bindgen(js_class = WasmBindgenTestContext)] impl Context { /// Creates a new context ready to run tests. diff --git a/crates/wasm-interpreter/src/lib.rs b/crates/wasm-interpreter/src/lib.rs index c13a23f64b8..61357c1b12c 100644 --- a/crates/wasm-interpreter/src/lib.rs +++ b/crates/wasm-interpreter/src/lib.rs @@ -18,6 +18,7 @@ #![deny(missing_docs)] +use anyhow::{bail, ensure}; use std::collections::{BTreeMap, BTreeSet, HashMap}; use walrus::ir::Instr; use walrus::{ElementId, FunctionId, LocalId, Module, TableId}; @@ -239,7 +240,14 @@ impl Interpreter { } for (instr, _) in block.instrs.iter() { - frame.eval(instr); + if let Err(err) = frame.eval(instr) { + if let Some(name) = &module.funcs.get(id).name { + panic!("{name}: {err}") + } else { + panic!("{err}") + } + } + if frame.done { break; } @@ -256,7 +264,7 @@ struct Frame<'a> { } impl Frame<'_> { - fn eval(&mut self, instr: &Instr) { + fn eval(&mut self, instr: &Instr) -> anyhow::Result<()> { use walrus::ir::*; let stack = &mut self.interp.scratch; @@ -264,7 +272,7 @@ impl Frame<'_> { match instr { Instr::Const(c) => match c.value { Value::I32(n) => stack.push(n), - _ => panic!("non-i32 constant"), + _ => bail!("non-i32 constant"), }, Instr::LocalGet(e) => stack.push(self.locals.get(&e.local).cloned().unwrap_or(0)), Instr::LocalSet(e) => { @@ -291,7 +299,7 @@ impl Frame<'_> { stack.push(match e.op { BinaryOp::I32Sub => lhs - rhs, BinaryOp::I32Add => lhs + rhs, - op => panic!("invalid binary op {:?}", op), + op => bail!("invalid binary op {:?}", op), }); } @@ -300,23 +308,23 @@ impl Frame<'_> { // theory there doesn't need to be. Instr::Load(e) => { let address = stack.pop().unwrap(); - assert!( + ensure!( address > 0, "Read a negative address value from the stack. Did we run out of memory?" ); let address = address as u32 + e.arg.offset; - assert!(address % 4 == 0); + ensure!(address % 4 == 0); stack.push(self.interp.mem[address as usize / 4]) } Instr::Store(e) => { let value = stack.pop().unwrap(); let address = stack.pop().unwrap(); - assert!( + ensure!( address > 0, "Read a negative address value from the stack. Did we run out of memory?" ); let address = address as u32 + e.arg.offset; - assert!(address % 4 == 0); + ensure!(address % 4 == 0); self.interp.mem[address as usize / 4] = value; } @@ -356,10 +364,27 @@ impl Frame<'_> { // ... otherwise this is a normal call so we recurse. } else { + // Skip profiling related functions which we don't want to interpret. + if self + .module + .funcs + .get(e.func) + .name + .as_ref() + .is_some_and(|name| { + name.starts_with("__llvm_profile_init") + || name.starts_with("__llvm_profile_register_function") + || name.starts_with("__llvm_profile_register_function") + }) + { + return Ok(()); + } + let ty = self.module.types.get(self.module.funcs.get(e.func).ty()); let args = (0..ty.params().len()) .map(|_| stack.pop().unwrap()) .collect::>(); + self.interp.call(e.func, self.module, &args); } } @@ -373,7 +398,9 @@ impl Frame<'_> { // Note that LLVM may change over time to generate new // instructions in debug mode, and we'll have to react to those // sorts of changes as they arise. - s => panic!("unknown instruction {:?}", s), + s => bail!("unknown instruction {:?}", s), } + + Ok(()) } } diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 7ba50cd5042..d11123ee764 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -111,6 +111,7 @@ - [Writing Asynchronous Tests](./wasm-bindgen-test/asynchronous-tests.md) - [Testing in Headless Browsers](./wasm-bindgen-test/browsers.md) - [Continuous Integration](./wasm-bindgen-test/continuous-integration.md) + - [Coverage (Experimental)](./wasm-bindgen-test/coverage.md) - [Contributing to `wasm-bindgen`](./contributing/index.md) - [Testing](./contributing/testing.md) diff --git a/guide/src/wasm-bindgen-test/coverage.md b/guide/src/wasm-bindgen-test/coverage.md new file mode 100644 index 00000000000..6172877917b --- /dev/null +++ b/guide/src/wasm-bindgen-test/coverage.md @@ -0,0 +1,83 @@ +# Generating Coverage Data + +You can ask the runner to generate coverage data from functions marked as `#[wasm_bindgen_test]` in the `.profraw` format. + +
+ Coverage is still in an experimental state, requires Rust Nightly, may be + unreliable and could experience breaking changes at any time. +
+ +## Enabling the feature + +To enable this feature, you need to set `cfg(wasm_bindgen_unstable_test_coverage)` for `wasm-bindgen-test` and its dependencies. + +Currently it is particularly difficult to [deliver compile-line arguments to proc-macros when cross-compiling with Cargo][1]. To circumvent this [host-config] can be used. + +[1]: https://github.com/rust-lang/cargo/issues/4423 +[host-config]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#host-config + +## Generating the data + +### `RUSTFLAGS` that need to be present + +Make sure you are using `RUSTFLAGS=-Cinstrument-coverage -Zno-profiler-runtime`. + +Due to the current limitation of `llvm-cov`, we can't collect profiling symbols from the generated `.wasm` files. Instead, we can grab them from the LLVM IR with `--emit=llvm-ir` by using Clang. Additionally, the emitted LLVM IR files by Rust contain invalid code that can't be parsed by Clang, so they need to be adjusted. Clang must use the same LLVM version that Rustc is using, which can be checkd by calling `rustc +nightly -vV`. + +### Arguments to the test runner + +The following environment variables can be used to control the coverage output when [executing the test runner][2]: + +- `WASM_BINDGEN_UNSTABLE_TEST_PROFRAW_OUT` to control the file name of the profraw or the directory in which it is placed +- `WASM_BINDGEN_UNSTABLE_TEST_PROFRAW_PREFIX` to add a custom prefix to the profraw files. This can be useful if you're running the tests automatically in succession. + +[2]: usage.html#appendix-using-wasm-bindgen-test-without-wasm-pack + +### Target features + +This feature relies on the [minicov] crate, which provides a profiling runtime for WebAssembly. It in turn uses [cc] to compile the runtime to Wasm, which [currently doesn't support accounting for target feature][3]. Use e.g. `CFLAGS_wasm32_unknown_unknown="-matomics -mbulk-memory"` to account for that. + +[3]: https://github.com/rust-lang/cc-rs/issues/268 +[cc]: https://crates.io/crates/cc +[minicov]: https://crates.io/crates/minicov + +### Example + +```sh +# Run the tests: +# - `CARGO_HOST_RUSTFLAGS` to pass the configuration to `wasm-bindgen-macro`. +# - `-Ztarget-applies-to-host -Zhost-config` to enable `CARGO_HOST_RUSTFLAGS`. +# - `--tests` to not run documentation tests, which is currently not supported. +CARGO_HOST_RUSTFLAGS=--cfg=wasm_bindgen_unstable_test_coverage \ +RUSTFLAGS="-Cinstrument-coverage -Zno-profiler-runtime --emit=llvm-ir --cfg=wasm_bindgen_unstable_test_coverage" \ +CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ +cargo +nightly test -Ztarget-applies-to-host -Zhost-config --tests +# Adjust the LLVM IR and compile to object files: +# - Replaces every function body with `unreachable`. +# - Removes Rust-specific `range` annotations from function signatures. +name=name_of_the_tested_crate_in_snake_case; \ +for file in `ls target/wasm32-unknown-unknown/debug/deps/$name-*.ll`; \ +do \ + perl -i -p0e 's/(^define.*?$).*?^}/$1\nstart:\n unreachable\n}/gms' $file && \ + perl -i -p0e 's/(?<=noundef) range\(.*?\)//g' $file && \ + clang $file -Wno-override-module -c; \ +done +# Merge all generated raw profiling data. +# This uses `cargo-binutils` which uses LLVM tools shipped by Rust to make sure there is no LLVM version discrepancy. +# But `llvm-profdata` can be used directly as well. +# See . +rust-profdata merge -sparse ./*.profraw -o coverage.profdata +# Produce test coverage data in the HTML format. +rust-cov show --instr-profile=coverage.profdata --object ./*.o --format=html --Xdemangler=rust-demangler --sources src --output-dir coverage +``` + +The [rustc book] has a lot more exapmles and information on test coverage as well. + +[rustc book]: https://doc.rust-lang.org/nightly/rustc/instrument-coverage.html + +## Attribution + +These methods have originally been pioneered by [Hacken OÜ], see [their guide][4] as well. + +[4]: https://hknio.github.io/wasmcov +[Hacken OÜ]: https://hacken.io diff --git a/src/closure.rs b/src/closure.rs index b038b1775ba..8cda5f896a7 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -331,12 +331,14 @@ where // See crates/cli-support/src/js/closures.rs for a more information // about what's going on here. + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] extern "C" fn describe() { inform(CLOSURE); T::describe() } #[inline(never)] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] unsafe fn breaks_if_inlined(a: usize, b: usize) -> u32 { super::__wbindgen_describe_closure(a as u32, b as u32, describe:: as u32) } @@ -462,6 +464,7 @@ impl WasmDescribe for Closure where T: WasmClosure + ?Sized, { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(EXTERNREF); } @@ -562,8 +565,10 @@ macro_rules! doit { where $($var: FromWasmAbi + 'static,)* R: ReturnWasmAbi + 'static, { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { #[allow(non_snake_case)] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] unsafe extern "C" fn invoke<$($var: FromWasmAbi,)* R: ReturnWasmAbi>( a: usize, b: usize, @@ -619,8 +624,10 @@ macro_rules! doit { where $($var: FromWasmAbi + 'static,)* R: ReturnWasmAbi + 'static, { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { #[allow(non_snake_case)] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] unsafe extern "C" fn invoke<$($var: FromWasmAbi,)* R: ReturnWasmAbi>( a: usize, b: usize, @@ -760,8 +767,10 @@ where A: RefFromWasmAbi, R: ReturnWasmAbi + 'static, { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { #[allow(non_snake_case)] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] unsafe extern "C" fn invoke( a: usize, b: usize, @@ -786,6 +795,7 @@ where inform(invoke:: as u32); + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] unsafe extern "C" fn destroy(a: usize, b: usize) { // See `Fn()` above for why we simply return if a == 0 { @@ -806,8 +816,10 @@ where A: RefFromWasmAbi, R: ReturnWasmAbi + 'static, { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { #[allow(non_snake_case)] + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] unsafe extern "C" fn invoke( a: usize, b: usize, @@ -833,6 +845,7 @@ where inform(invoke:: as u32); + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] unsafe extern "C" fn destroy(a: usize, b: usize) { // See `Fn()` above for why we simply return if a == 0 { diff --git a/src/convert/closures.rs b/src/convert/closures.rs index 8227a6b622f..dbe1318cb72 100644 --- a/src/convert/closures.rs +++ b/src/convert/closures.rs @@ -54,6 +54,7 @@ macro_rules! stack_closures { where $($var: FromWasmAbi,)* R: ReturnWasmAbi { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(FUNCTION); inform($invoke::<$($var,)* R> as u32); @@ -108,6 +109,7 @@ macro_rules! stack_closures { where $($var: FromWasmAbi,)* R: ReturnWasmAbi { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(FUNCTION); inform($invoke_mut::<$($var,)* R> as u32); @@ -151,6 +153,7 @@ where } #[allow(non_snake_case)] +#[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] unsafe extern "C" fn invoke1_ref( a: usize, b: usize, @@ -177,6 +180,7 @@ where A: RefFromWasmAbi, R: ReturnWasmAbi, { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(FUNCTION); inform(invoke1_ref:: as u32); @@ -206,6 +210,7 @@ where } #[allow(non_snake_case)] +#[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] unsafe extern "C" fn invoke1_mut_ref( a: usize, b: usize, @@ -232,6 +237,7 @@ where A: RefFromWasmAbi, R: ReturnWasmAbi, { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(FUNCTION); inform(invoke1_mut_ref:: as u32); diff --git a/src/convert/slices.rs b/src/convert/slices.rs index ee922b901b2..99adc9b3745 100644 --- a/src/convert/slices.rs +++ b/src/convert/slices.rs @@ -123,6 +123,7 @@ impl DerefMut for MutSlice { macro_rules! vectors { ($($t:ident)*) => ($( impl WasmDescribeVector for $t { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe_vector() { inform(VECTOR); $t::describe(); @@ -225,6 +226,7 @@ vectors! { } impl WasmDescribeVector for String { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe_vector() { inform(VECTOR); inform(NAMED_EXTERNREF); diff --git a/src/describe.rs b/src/describe.rs index 58b02e17a22..f7e9e83d2cf 100644 --- a/src/describe.rs +++ b/src/describe.rs @@ -56,6 +56,7 @@ tys! { } #[inline(always)] // see the wasm-interpreter crate +#[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] pub fn inform(a: u32) { unsafe { super::__wbindgen_describe(a) } } @@ -73,6 +74,7 @@ pub trait WasmDescribeVector { macro_rules! simple { ($($t:ident => $d:ident)*) => ($( impl WasmDescribe for $t { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform($d) } } )*) @@ -110,24 +112,28 @@ cfg_if! { } impl WasmDescribe for *const T { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(U32) } } impl WasmDescribe for *mut T { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(U32) } } impl WasmDescribe for NonNull { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(NONNULL) } } impl WasmDescribe for [T] { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(SLICE); T::describe(); @@ -135,6 +141,7 @@ impl WasmDescribe for [T] { } impl<'a, T: WasmDescribe + ?Sized> WasmDescribe for &'a T { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(REF); T::describe(); @@ -142,6 +149,7 @@ impl<'a, T: WasmDescribe + ?Sized> WasmDescribe for &'a T { } impl<'a, T: WasmDescribe + ?Sized> WasmDescribe for &'a mut T { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(REFMUT); T::describe(); @@ -162,6 +170,7 @@ cfg_if! { } impl WasmDescribeVector for JsValue { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe_vector() { inform(VECTOR); JsValue::describe(); @@ -169,6 +178,7 @@ impl WasmDescribeVector for JsValue { } impl WasmDescribeVector for T { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe_vector() { inform(VECTOR); T::describe(); @@ -176,6 +186,7 @@ impl WasmDescribeVector for T { } impl WasmDescribe for Box<[T]> { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { T::describe_vector(); } @@ -185,12 +196,14 @@ impl WasmDescribe for Vec where Box<[T]>: WasmDescribe, { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { >::describe(); } } impl WasmDescribe for Option { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(OPTIONAL); T::describe(); @@ -198,12 +211,14 @@ impl WasmDescribe for Option { } impl WasmDescribe for () { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(UNIT) } } impl> WasmDescribe for Result { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(RESULT); T::describe(); @@ -211,6 +226,7 @@ impl> WasmDescribe for Result { } impl WasmDescribe for Clamped { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { inform(CLAMPED); T::describe(); @@ -218,6 +234,7 @@ impl WasmDescribe for Clamped { } impl WasmDescribe for JsError { + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] fn describe() { JsValue::describe(); } diff --git a/src/lib.rs b/src/lib.rs index e8e30b9aa95..23cc9db2f0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ //! interface. #![no_std] +#![cfg_attr(wasm_bindgen_unstable_test_coverage, feature(coverage_attribute))] #![allow(coherence_leak_check)] #![doc(html_root_url = "https://docs.rs/wasm-bindgen/0.2")] @@ -1877,6 +1878,7 @@ pub mod __rt { /// in the object file and link the intrinsics. /// /// Ideas for how to improve this are most welcome! + #[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] pub fn link_mem_intrinsics() { crate::link::link_intrinsics(); } diff --git a/src/link.rs b/src/link.rs index aa75707700e..4d9858189ab 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1,3 +1,4 @@ // see comment in module above this in `link_mem_intrinsics` #[inline(never)] +#[cfg_attr(wasm_bindgen_unstable_test_coverage, coverage(off))] pub fn link_intrinsics() {}