From 8cb328a1ebb2f083ebf5f00b0d52ffcbfd33d83a Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Wed, 17 Jan 2024 18:38:50 +0100 Subject: [PATCH 1/5] Implement wasm-pack subcommand --- src/clean.rs | 2 +- src/cli.rs | 23 ++++++++-- src/main.rs | 125 ++++++++++++++++++++++++++++++++++++++++++++------ tests/test.rs | 3 +- 4 files changed, 134 insertions(+), 19 deletions(-) diff --git a/src/clean.rs b/src/clean.rs index aa5d73b1..88622f5b 100644 --- a/src/clean.rs +++ b/src/clean.rs @@ -17,7 +17,7 @@ use crate::{ fs, metadata::PackageId, regex_vec::{RegexVec, RegexVecBuilder}, - term, + term, wasm_target_dir, }; pub(crate) fn run(args: &mut Args) -> Result<()> { diff --git a/src/cli.rs b/src/cli.rs index 1f25c678..0cc6e51d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -541,7 +541,10 @@ impl Args { } } match subcommand { - Subcommand::None | Subcommand::Nextest { .. } | Subcommand::NextestArchive => {} + Subcommand::None + | Subcommand::Nextest { .. } + | Subcommand::NextestArchive + | Subcommand::WasmPack => {} Subcommand::Test => { if no_run { unexpected("--no-run", subcommand)?; @@ -591,7 +594,8 @@ impl Args { | Subcommand::Test | Subcommand::Run | Subcommand::Nextest { .. } - | Subcommand::NextestArchive => {} + | Subcommand::NextestArchive + | Subcommand::WasmPack => {} _ => { if !bin.is_empty() { unexpected("--bin", subcommand)?; @@ -619,6 +623,7 @@ impl Args { | Subcommand::Run | Subcommand::Nextest { .. } | Subcommand::NextestArchive + | Subcommand::WasmPack | Subcommand::ShowEnv => {} _ => { if no_cfg_coverage { @@ -634,7 +639,8 @@ impl Args { | Subcommand::Test | Subcommand::Nextest { .. } | Subcommand::NextestArchive - | Subcommand::Clean => {} + | Subcommand::Clean + | Subcommand::WasmPack => {} _ => { if workspace { unexpected("--workspace", subcommand)?; @@ -930,6 +936,9 @@ pub(crate) enum Subcommand { /// Build and archive tests with cargo nextest NextestArchive, + /// Run tests with wasm-pack + WasmPack, + // internal (unstable) Demangle, } @@ -946,7 +955,10 @@ static CARGO_LLVM_COV_NEXTEST_ARCHIVE_USAGE: &str = impl Subcommand { fn can_passthrough(subcommand: Self) -> bool { - matches!(subcommand, Self::Test | Self::Run | Self::Nextest { .. } | Self::NextestArchive) + matches!( + subcommand, + Self::Test | Self::Run | Self::Nextest { .. } | Self::NextestArchive | Self::WasmPack + ) } fn help_text(subcommand: Self) -> &'static str { @@ -959,6 +971,7 @@ impl Subcommand { Self::ShowEnv => CARGO_LLVM_COV_SHOW_ENV_USAGE, Self::Nextest { .. } => CARGO_LLVM_COV_NEXTEST_USAGE, Self::NextestArchive => CARGO_LLVM_COV_NEXTEST_ARCHIVE_USAGE, + Self::WasmPack => todo!(), Self::Demangle => "", // internal API } } @@ -973,6 +986,7 @@ impl Subcommand { Self::ShowEnv => "show-env", Self::Nextest { .. } => "nextest", Self::NextestArchive => "nextest-archive", + Self::WasmPack => "wasm-pack", Self::Demangle => "demangle", } } @@ -994,6 +1008,7 @@ impl FromStr for Subcommand { "show-env" => Ok(Self::ShowEnv), "nextest" => Ok(Self::Nextest { archive_file: false }), "nextest-archive" => Ok(Self::NextestArchive), + "wasm-pack" => Ok(Self::WasmPack), "demangle" => Ok(Self::Demangle), _ => bail!("unrecognized subcommand {s}"), } diff --git a/src/main.rs b/src/main.rs index c4a542cd..501d8d1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use std::{ collections::{BTreeSet, HashMap}, ffi::{OsStr, OsString}, io::{self, BufRead, Write}, - path::Path, + path::{Path, PathBuf}, time::SystemTime, }; @@ -105,6 +105,15 @@ fn try_main() -> Result<()> { create_dirs(cx)?; archive_nextest(cx)?; } + Subcommand::WasmPack => { + let cx = &Context::new(args)?; + clean::clean_partial(cx)?; + create_dirs(cx)?; + wasm_pack_test(cx)?; + if !cx.args.cov.no_report { + generate_report(cx)?; + } + } Subcommand::None | Subcommand::Test => { let cx = &Context::new(args)?; clean::clean_partial(cx)?; @@ -496,6 +505,72 @@ fn run_run(cx: &Context) -> Result<()> { Ok(()) } +// Sanitizes the crate name so we know which .ll file to compile +fn crate_name_to_llvm_name(crate_name: &str) -> String { + crate_name.replace('-', "_") +} + +fn compile_ll_file(cx: &Context, prefix: &str) -> Result<()> { + // There are multiple ll files generated per crate, but only one wasm which has + // an ll file with the same file stem sitting next to it. + // That's the ll file we want. + let path = wasm_target_dir(&cx.ws); + let glob = glob::glob( + Utf8Path::new(&glob::Pattern::escape(&path.to_string())) + .join(format!("{prefix}-*.wasm")) + .as_str(), + )?; + let Some(newest) = glob + .filter_map(Result::ok) + .max_by_key(|p| std::fs::metadata(p).unwrap().created().unwrap()) + else { + return Ok(()); + }; + + let stem = newest.file_stem().unwrap().to_str().unwrap(); + let src = format!("{path}/{stem}.ll"); + let dst = format!("{path}/{stem}.o"); + + let mut cmd = cmd!("clang"); + cmd.arg(src); + cmd.arg("-Wno-override-module"); + cmd.arg("-c"); + cmd.arg("-o"); + cmd.arg(dst); + cmd.run()?; + Ok(()) +} + +fn compile_ll_files(cx: &Context) -> Result<()> { + for id in &cx.ws.metadata.workspace_members { + let prefix = crate_name_to_llvm_name(&cx.ws.metadata.packages[&id].name); + compile_ll_file(cx, &prefix)?; + } + Ok(()) +} + +fn wasm_profraw_prefix(ws: &Workspace) -> String { + format!("{}-", ws.name) +} + +fn wasm_pack_test(cx: &Context) -> Result<()> { + let mut cmd = cmd!("wasm-pack"); + cmd.arg("test"); + cmd.arg("--coverage"); + cmd.args(["--profraw-out", &cx.ws.target_dir.to_string()]); + cmd.args(["--profraw-prefix", &wasm_profraw_prefix(&cx.ws)]); + cmd.args(cx.args.cargo_args.clone()); + cmd.args(["--target-dir", cx.ws.target_dir.as_ref()]); + + // Emit llvm-ir to obtain debug info (https://github.com/hknio/code-coverage-for-webassembly) + cmd.env("RUSTFLAGS", "-Cinstrument-coverage -Zno-profiler-runtime --emit=llvm-ir"); + cmd.run()?; + + compile_ll_files(cx)?; + + Ok(()) +} + fn stdout_to_stderr(cx: &Context, cargo: &mut ProcessBuilder) { if cx.args.cov.no_report || cx.args.cov.output_dir.is_some() @@ -509,9 +584,17 @@ fn stdout_to_stderr(cx: &Context, cargo: &mut ProcessBuilder) { } fn generate_report(cx: &Context) -> Result<()> { - merge_profraw(cx).context("failed to merge profile data")?; + let profraws = merge_profraw(cx).context("failed to merge profile data")?; - let object_files = object_files(cx).context("failed to collect object files")?; + let mut object_files = object_files(cx).context("failed to collect object files")?; + object_files.append(&mut wasm_object_files(&cx.ws, &profraws)); + if object_files.is_empty() { + warn!( + "not found object files (this may occur if \ + show-env subcommand is used incorrectly (see docs or other warnings), or unsupported \ + commands such as nextest archive are used", + ); + } let ignore_filename_regex = ignore_filename_regex(cx); let format = Format::from_args(cx); format @@ -613,6 +696,10 @@ fn generate_report(cx: &Context) -> Result<()> { Ok(()) } +fn wasm_target_dir(ws: &Workspace) -> Utf8PathBuf { + format!("{}/wasm32-unknown-unknown/debug/deps", ws.target_dir).into() +} + fn open_report(cx: &Context, path: &Utf8Path) -> Result<()> { match &cx.ws.config.doc.browser { Some(browser) => { @@ -627,7 +714,7 @@ fn open_report(cx: &Context, path: &Utf8Path) -> Result<()> { Ok(()) } -fn merge_profraw(cx: &Context) -> Result<()> { +fn merge_profraw(cx: &Context) -> Result> { // Convert raw profile data. let profraw_files = glob::glob( Utf8Path::new(&glob::Pattern::escape(cx.ws.target_dir.as_str())).join("*.profraw").as_str(), @@ -642,7 +729,7 @@ fn merge_profraw(cx: &Context) -> Result<()> { ); } let mut input_files = String::new(); - for path in profraw_files { + for path in &profraw_files { input_files.push_str( path.to_str().with_context(|| format!("{path:?} contains invalid utf-8 data"))?, ); @@ -666,7 +753,7 @@ fn merge_profraw(cx: &Context) -> Result<()> { status!("Running", "{cmd}"); } cmd.stdout_to_stderr().run()?; - Ok(()) + Ok(profraw_files) } fn object_files(cx: &Context) -> Result> { @@ -807,15 +894,26 @@ fn object_files(cx: &Context) -> Result> { // This sort is necessary to make the result of `llvm-cov show` match between macos and linux. files.sort_unstable(); + Ok(files) +} - if files.is_empty() { - warn!( - "not found object files (searched directories: {searched_dir}); this may occur if \ - show-env subcommand is used incorrectly (see docs or other warnings), or unsupported \ - commands such as nextest archive are used", - ); +fn wasm_object_files(ws: &Workspace, profraws: &[PathBuf]) -> Vec { + let mut ret = Vec::new(); + for file in profraws { + let fname = file.file_name().unwrap().to_str().unwrap(); + let prefix = format!("{}wbg-tmp-", wasm_profraw_prefix(ws)); + let Some(stem): Option = fname + .strip_prefix(&prefix) + .and_then(|s| s.strip_suffix(".wasm.profraw")) + .map(|s| s.into()) + else { + // Didn't match prefix or suffix + continue; + }; + let obj = wasm_target_dir(ws).join(format!("{}.o", stem.display())); + ret.push(obj.as_os_str().to_owned()) } - Ok(files) + ret } struct Targets { @@ -951,6 +1049,7 @@ impl Format { cmd.arg("-ignore-filename-regex"); cmd.arg(ignore_filename_regex); } + cmd.args(["--sources", "."]); match self { Self::Text | Self::Html => { diff --git a/tests/test.rs b/tests/test.rs index 692d871f..3e01eea2 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -341,7 +341,8 @@ fn invalid_arg() { )); } } - if !matches!(subcommand, "" | "test" | "run" | "nextest" | "nextest-archive") { + if !matches!(subcommand, "" | "test" | "run" | "nextest" | "nextest-archive" | "wasm-pack") + { for arg in [ "--bin=v", "--example=v", From 3bbfe641ca3901753c6f582a40ae65d82e43f5bc Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Thu, 18 Jan 2024 16:55:10 +0100 Subject: [PATCH 2/5] Remove --sources . --- src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 501d8d1a..c0f6e8a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1049,7 +1049,6 @@ impl Format { cmd.arg("-ignore-filename-regex"); cmd.arg(ignore_filename_regex); } - cmd.args(["--sources", "."]); match self { Self::Text | Self::Html => { From 490c8a07e15c680f2ec093dce9e80f60b2ea57b1 Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Thu, 18 Jan 2024 16:29:01 +0100 Subject: [PATCH 3/5] Add docs --- README.md | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4a7c4c57..bd37b565 100644 --- a/README.md +++ b/README.md @@ -16,21 +16,23 @@ This is a wrapper around rustc [`-C instrument-coverage`][instrument-coverage] a **Table of Contents:** -- [Usage](#usage) - - [Basic usage](#basic-usage) - - [Merge coverages generated under different test conditions](#merge-coverages-generated-under-different-test-conditions) - - [Get coverage of C/C++ code linked to Rust library/binary](#get-coverage-of-cc-code-linked-to-rust-librarybinary) - - [Get coverage of external tests](#get-coverage-of-external-tests) - - [Exclude file from coverage](#exclude-file-from-coverage) - - [Exclude function from coverage](#exclude-function-from-coverage) - - [Continuous Integration](#continuous-integration) - - [Display coverage in VS Code](#display-coverage-in-vs-code) - - [Environment variables](#environment-variables) - - [Additional JSON information](#additional-json-information) -- [Installation](#installation) -- [Known limitations](#known-limitations) -- [Related Projects](#related-projects) -- [License](#license) +- [cargo-llvm-cov](#cargo-llvm-cov) + - [Usage](#usage) + - [Basic usage](#basic-usage) + - [Merge coverages generated under different test conditions](#merge-coverages-generated-under-different-test-conditions) + - [Get coverage of `wasm-pack test`](#get-coverage-of-wasm-pack-test) + - [Get coverage of C/C++ code linked to Rust library/binary](#get-coverage-of-cc-code-linked-to-rust-librarybinary) + - [Get coverage of external tests](#get-coverage-of-external-tests) + - [Exclude file from coverage](#exclude-file-from-coverage) + - [Exclude function from coverage](#exclude-function-from-coverage) + - [Continuous Integration](#continuous-integration) + - [Display coverage in VS Code](#display-coverage-in-vs-code) + - [Environment variables](#environment-variables) + - [Additional JSON information](#additional-json-information) + - [Installation](#installation) + - [Known limitations](#known-limitations) + - [Related Projects](#related-projects) + - [License](#license) ## Usage @@ -420,6 +422,18 @@ cargo llvm-cov report --lcov # generate report without tests Note: To include coverage for doctests you also need to pass `--doctests` to `cargo llvm-cov report`. +### Get coverage of `wasm-pack test` + +You can use the `wasm-pack` subcommand to run `wasm-pack test` and get coverage output from that. + +```sh +cargo llvm-cov wasm-pack --chrome --headless +``` + +You can also merge this with normal `cargo test` [by following the instructions on merging](https://github.com/taiki-e/cargo-llvm-cov/pull/338). + +Note: This command is experimental and sometimes breaks requiring a `cargo clean` to continue. + ### Get coverage of C/C++ code linked to Rust library/binary Set `CC`, `CXX`, `LLVM_COV`, and `LLVM_PROFDATA` environment variables to Clang/LLVM compatible with the LLVM version used in rustc, and run cargo-llvm-cov with `--include-ffi` flag. From 51be2360bbdd2e48313b9ee37549ce15c9d1d6c2 Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Thu, 18 Jan 2024 16:23:41 +0100 Subject: [PATCH 4/5] Document unwraps --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index c0f6e8a1..50569a73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -900,6 +900,8 @@ fn object_files(cx: &Context) -> Result> { fn wasm_object_files(ws: &Workspace, profraws: &[PathBuf]) -> Vec { let mut ret = Vec::new(); for file in profraws { + // The profraws on this list definitely have file names which are valid utf8 + // otherwise they wouldn't have made it onto the list let fname = file.file_name().unwrap().to_str().unwrap(); let prefix = format!("{}wbg-tmp-", wasm_profraw_prefix(ws)); let Some(stem): Option = fname From dac1603acbd76c1e99b5dae00d552b15e0bb650a Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Thu, 18 Jan 2024 16:18:09 +0100 Subject: [PATCH 5/5] Add words to dictionary --- .github/.cspell/project-dictionary.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/.cspell/project-dictionary.txt b/.github/.cspell/project-dictionary.txt index 181b749a..d68793ec 100644 --- a/.github/.cspell/project-dictionary.txt +++ b/.github/.cspell/project-dictionary.txt @@ -14,6 +14,7 @@ nextest notcovered profdata profraw +profraws rustfilt TESTNAME trybuild @@ -21,3 +22,4 @@ winapi xargo Xdemangler xtask +Cinstrument