Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support for wasm-pack #338

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/.cspell/project-dictionary.txt
Original file line number Diff line number Diff line change
@@ -14,10 +14,12 @@ nextest
notcovered
profdata
profraw
profraws
rustfilt
TESTNAME
trybuild
winapi
xargo
Xdemangler
xtask
Cinstrument
44 changes: 29 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion src/clean.rs
Original file line number Diff line number Diff line change
@@ -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<()> {
23 changes: 19 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -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}"),
}
126 changes: 113 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<PathBuf>> {
// 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<Vec<OsString>> {
@@ -807,15 +894,28 @@ fn object_files(cx: &Context) -> Result<Vec<OsString>> {

// 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<OsString> {
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<PathBuf> = 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 {
3 changes: 2 additions & 1 deletion tests/test.rs
Original file line number Diff line number Diff line change
@@ -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",