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
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down
23 changes: 19 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -619,6 +623,7 @@ impl Args {
| Subcommand::Run
| Subcommand::Nextest { .. }
| Subcommand::NextestArchive
| Subcommand::WasmPack
| Subcommand::ShowEnv => {}
_ => {
if no_cfg_coverage {
Expand All @@ -634,7 +639,8 @@ impl Args {
| Subcommand::Test
| Subcommand::Nextest { .. }
| Subcommand::NextestArchive
| Subcommand::Clean => {}
| Subcommand::Clean
| Subcommand::WasmPack => {}
_ => {
if workspace {
unexpected("--workspace", subcommand)?;
Expand Down Expand Up @@ -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,
}
Expand All @@ -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 {
Expand All @@ -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
}
}
Expand All @@ -973,6 +986,7 @@ impl Subcommand {
Self::ShowEnv => "show-env",
Self::Nextest { .. } => "nextest",
Self::NextestArchive => "nextest-archive",
Self::WasmPack => "wasm-pack",
Self::Demangle => "demangle",
}
}
Expand All @@ -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}"),
}
Expand Down
126 changes: 113 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{
collections::{BTreeSet, HashMap},
ffi::{OsStr, OsString},
io::{self, BufRead, Write},
path::Path,
path::{Path, PathBuf},
time::SystemTime,
};

Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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) => {
Expand All @@ -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(),
Expand All @@ -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"))?,
);
Expand All @@ -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>> {
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading