Skip to content

Commit 609d897

Browse files
committed
Add option for generating coverage reports
Add a `--coverage` option in the `test` subcommand of the miri script. This option, when set, will generate a coverage report after running the tests. `cargo-binutils` is needed as a dependency to generate the reports.
1 parent 0bf9284 commit 609d897

File tree

7 files changed

+175
-11
lines changed

7 files changed

+175
-11
lines changed

.github/workflows/ci.yml

+11-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,17 @@ jobs:
5858
- name: rustdoc
5959
run: RUSTDOCFLAGS="-Dwarnings" ./miri doc --document-private-items
6060

61+
coverage:
62+
name: Coverage report
63+
runs-on: ubuntu-latest
64+
steps:
65+
- uses: actions/checkout@v4
66+
- uses: ./.github/workflows/setup
67+
- name: coverage
68+
run: ./miri test --coverage
69+
6170
conclusion:
62-
needs: [build, style]
71+
needs: [build, style, coverage]
6372
# We need to ensure this job does *not* get skipped if its dependencies fail,
6473
# because a skipped job is considered a success by GitHub. So we have to
6574
# overwrite `if:`. We use `!cancelled()` to ensure the job does still not get run
@@ -85,7 +94,7 @@ jobs:
8594
contents: write
8695
# ... and create a PR.
8796
pull-requests: write
88-
needs: [build, style]
97+
needs: [build, style, coverage]
8998
if: github.event_name == 'schedule' && failure()
9099
steps:
91100
# Send a Zulip notification

miri-script/Cargo.lock

+39-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

miri-script/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ rustc_version = "0.4"
2424
dunce = "1.0.4"
2525
directories = "5"
2626
serde_json = "1"
27+
tempfile = "3.13.0"

miri-script/src/commands.rs

+22-2
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,13 @@ impl Command {
172172
Command::Install { flags } => Self::install(flags),
173173
Command::Build { flags } => Self::build(flags),
174174
Command::Check { flags } => Self::check(flags),
175-
Command::Test { bless, flags, target } => Self::test(bless, flags, target),
175+
Command::Test { bless, flags, target, coverage } =>
176+
Self::test(
177+
bless,
178+
flags,
179+
target,
180+
coverage.then_some(crate::coverage::CoverageReport::new()?),
181+
),
176182
Command::Run { dep, verbose, many_seeds, target, edition, flags } =>
177183
Self::run(dep, verbose, many_seeds, target, edition, flags),
178184
Command::Doc { flags } => Self::doc(flags),
@@ -458,9 +464,18 @@ impl Command {
458464
Ok(())
459465
}
460466

461-
fn test(bless: bool, mut flags: Vec<String>, target: Option<String>) -> Result<()> {
467+
fn test(
468+
bless: bool,
469+
mut flags: Vec<String>,
470+
target: Option<String>,
471+
coverage: Option<crate::coverage::CoverageReport>,
472+
) -> Result<()> {
462473
let mut e = MiriEnv::new()?;
463474

475+
if let Some(report) = &coverage {
476+
report.add_env_vars(&mut e)?;
477+
}
478+
464479
// Prepare a sysroot. (Also builds cargo-miri, which we need.)
465480
e.build_miri_sysroot(/* quiet */ false, target.as_deref())?;
466481

@@ -479,6 +494,11 @@ impl Command {
479494
// Then test, and let caller control flags.
480495
// Only in root project as `cargo-miri` has no tests.
481496
e.test(".", &flags)?;
497+
498+
if let Some(coverage) = &coverage {
499+
coverage.show_coverage_report(&e)?;
500+
}
501+
482502
Ok(())
483503
}
484504

miri-script/src/coverage.rs

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use std::ffi::OsString;
2+
3+
use anyhow::{Context, Result};
4+
use path_macro::path;
5+
use tempfile::TempDir;
6+
use xshell::cmd;
7+
8+
use crate::util::MiriEnv;
9+
10+
/// CoverageReport can generate code coverage reports for miri.
11+
pub struct CoverageReport {
12+
/// path is a temporary directory where coverage artifacts will be stored.
13+
path: TempDir,
14+
}
15+
16+
impl CoverageReport {
17+
/// new creates a new CoverageReport
18+
///
19+
/// # Errors
20+
///
21+
/// An error will be returned if a temporary directory could not be created.
22+
pub fn new() -> Result<Self> {
23+
Ok(Self { path: TempDir::new()? })
24+
}
25+
26+
/// add_env_vars will add the required environment variables to MiriEnv `e`.
27+
pub fn add_env_vars(&self, e: &mut MiriEnv) -> Result<()> {
28+
let mut rustflags = e.sh.var("RUSTFLAGS")?;
29+
rustflags.push_str(" -C instrument-coverage");
30+
e.sh.set_var("RUSTFLAGS", rustflags);
31+
32+
// Copy-pasting from: https://doc.rust-lang.org/rustc/instrument-coverage.html#instrumentation-based-code-coverage
33+
// The format symbols below have the following meaning:
34+
// - %p - The process ID.
35+
// - %Nm - the instrumented binary’s signature:
36+
// The runtime creates a pool of N raw profiles, used for on-line
37+
// profile merging. The runtime takes care of selecting a raw profile
38+
// from the pool, locking it, and updating it before the program
39+
// exits. N must be between 1 and 9, and defaults to 1 if omitted
40+
// (with simply %m).
41+
//
42+
// Additionally the default for LLVM_PROFILE_FILE is default_%m_%p.profraw.
43+
// So we just use the same template, replacing "default" with "miri".
44+
let file_template = self.path.path().join("miri_%m_%p.profraw");
45+
e.sh.set_var("LLVM_PROFILE_FILE", file_template);
46+
Ok(())
47+
}
48+
49+
/// show_coverage_report will print coverage information using the artifact
50+
/// files in `self.path`.
51+
pub fn show_coverage_report(&self, e: &MiriEnv) -> Result<()> {
52+
let profraw_files: Vec<_> = self.profraw_files()?;
53+
54+
let profdata_bin = path!(e.libdir / ".." / "bin" / "llvm-profdata");
55+
56+
let merged_file = path!(e.miri_dir / "target" / "coverage.profdata");
57+
58+
// Merge the profraw files
59+
let profraw_files_cloned = profraw_files.iter();
60+
cmd!(e.sh, "{profdata_bin} merge -sparse {profraw_files_cloned...} -o {merged_file}")
61+
.quiet()
62+
.run()?;
63+
64+
// Create the coverage report.
65+
let cov_bin = path!(e.libdir / ".." / "bin" / "llvm-cov");
66+
let miri_bin =
67+
e.build_get_binary(".").context("failed to get filename of miri executable")?;
68+
cmd!(
69+
e.sh,
70+
"{cov_bin} report --instr-profile={merged_file} --object {miri_bin} --sources src/"
71+
)
72+
.run()?;
73+
74+
Ok(())
75+
}
76+
77+
/// profraw_files returns the profraw files in `self.path`.
78+
///
79+
/// # Errors
80+
///
81+
/// An error will be returned if `self.path` can't be read.
82+
fn profraw_files(&self) -> Result<Vec<OsString>> {
83+
Ok(std::fs::read_dir(&self.path)?
84+
.filter_map(|r| r.ok())
85+
.map(|e| e.path())
86+
.filter(|p| p.extension().map(|e| e == "profraw").unwrap_or(false))
87+
.map(|p| p.as_os_str().to_os_string())
88+
.collect())
89+
}
90+
}

miri-script/src/main.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
mod args;
44
mod commands;
5+
mod coverage;
56
mod util;
67

78
use std::ops::Range;
@@ -34,6 +35,8 @@ pub enum Command {
3435
/// The cross-interpretation target.
3536
/// If none then the host is the target.
3637
target: Option<String>,
38+
/// Produce coverage report if set.
39+
coverage: bool,
3740
/// Flags that are passed through to the test harness.
3841
flags: Vec<String>,
3942
},
@@ -158,9 +161,12 @@ fn main() -> Result<()> {
158161
let mut target = None;
159162
let mut bless = false;
160163
let mut flags = Vec::new();
164+
let mut coverage = false;
161165
loop {
162166
if args.get_long_flag("bless")? {
163167
bless = true;
168+
} else if args.get_long_flag("coverage")? {
169+
coverage = true;
164170
} else if let Some(val) = args.get_long_opt("target")? {
165171
target = Some(val);
166172
} else if let Some(flag) = args.get_other() {
@@ -169,7 +175,7 @@ fn main() -> Result<()> {
169175
break;
170176
}
171177
}
172-
Command::Test { bless, flags, target }
178+
Command::Test { bless, flags, target, coverage }
173179
}
174180
Some("run") => {
175181
let mut dep = false;

miri-script/src/util.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ pub struct MiriEnv {
4141
pub sysroot: PathBuf,
4242
/// The shell we use.
4343
pub sh: Shell,
44+
/// The library dir in the sysroot.
45+
pub libdir: PathBuf,
4446
}
4547

4648
impl MiriEnv {
@@ -96,7 +98,8 @@ impl MiriEnv {
9698
// so that Windows can find the DLLs.
9799
if cfg!(windows) {
98100
let old_path = sh.var("PATH")?;
99-
let new_path = env::join_paths(iter::once(libdir).chain(env::split_paths(&old_path)))?;
101+
let new_path =
102+
env::join_paths(iter::once(libdir.clone()).chain(env::split_paths(&old_path)))?;
100103
sh.set_var("PATH", new_path);
101104
}
102105

@@ -111,7 +114,7 @@ impl MiriEnv {
111114
std::process::exit(1);
112115
}
113116

114-
Ok(MiriEnv { miri_dir, toolchain, sh, sysroot, cargo_extra_flags })
117+
Ok(MiriEnv { miri_dir, toolchain, sh, sysroot, cargo_extra_flags, libdir })
115118
}
116119

117120
pub fn cargo_cmd(&self, crate_dir: impl AsRef<OsStr>, cmd: &str) -> Cmd<'_> {

0 commit comments

Comments
 (0)