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 option for generating coverage reports #3954

Merged
merged 1 commit into from
Oct 28, 2024
Merged
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
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,20 @@ jobs:
- name: rustdoc
run: RUSTDOCFLAGS="-Dwarnings" ./miri doc --document-private-items

coverage:
name: coverage report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/workflows/setup
- name: coverage
run: ./miri test --coverage

# Summary job for the merge queue.
# ALL THE PREVIOUS JOBS NEED TO BE ADDED TO THE `needs` SECTION OF THIS JOB!
# And they should be added below in `cron-fail-notify` as well.
conclusion:
needs: [build, style]
needs: [build, style, coverage]
# We need to ensure this job does *not* get skipped if its dependencies fail,
# because a skipped job is considered a success by GitHub. So we have to
# overwrite `if:`. We use `!cancelled()` to ensure the job does still not get run
Expand All @@ -86,7 +95,7 @@ jobs:
contents: write
# ... and create a PR.
pull-requests: write
needs: [build, style]
needs: [build, style, coverage]
if: ${{ github.event_name == 'schedule' && failure() }}
steps:
# Send a Zulip notification
Expand Down
43 changes: 39 additions & 4 deletions miri-script/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions miri-script/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ rustc_version = "0.4"
dunce = "1.0.4"
directories = "5"
serde_json = "1"
tempfile = "3.13.0"
21 changes: 19 additions & 2 deletions miri-script/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ impl Command {
Command::Install { flags } => Self::install(flags),
Command::Build { flags } => Self::build(flags),
Command::Check { flags } => Self::check(flags),
Command::Test { bless, flags, target } => Self::test(bless, flags, target),
Command::Test { bless, flags, target, coverage } =>
Self::test(bless, flags, target, coverage),
Command::Run { dep, verbose, many_seeds, target, edition, flags } =>
Self::run(dep, verbose, many_seeds, target, edition, flags),
Command::Doc { flags } => Self::doc(flags),
Expand Down Expand Up @@ -458,9 +459,20 @@ impl Command {
Ok(())
}

fn test(bless: bool, mut flags: Vec<String>, target: Option<String>) -> Result<()> {
fn test(
bless: bool,
mut flags: Vec<String>,
target: Option<String>,
coverage: bool,
) -> Result<()> {
let mut e = MiriEnv::new()?;

let coverage = coverage.then_some(crate::coverage::CoverageReport::new()?);

if let Some(report) = &coverage {
report.add_env_vars(&mut e)?;
}

// Prepare a sysroot. (Also builds cargo-miri, which we need.)
e.build_miri_sysroot(/* quiet */ false, target.as_deref())?;

Expand All @@ -479,6 +491,11 @@ impl Command {
// Then test, and let caller control flags.
// Only in root project as `cargo-miri` has no tests.
e.test(".", &flags)?;

if let Some(coverage) = &coverage {
coverage.show_coverage_report(&e)?;
}

Ok(())
}

Expand Down
91 changes: 91 additions & 0 deletions miri-script/src/coverage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use std::path::PathBuf;

use anyhow::{Context, Result};
use path_macro::path;
use tempfile::TempDir;
use xshell::cmd;

use crate::util::MiriEnv;

/// CoverageReport can generate code coverage reports for miri.
pub struct CoverageReport {
/// path is a temporary directory where intermediate coverage artifacts will be stored.
/// (The final output will be stored in a permanent location.)
path: TempDir,
}

impl CoverageReport {
/// Creates a new CoverageReport.
///
/// # Errors
///
/// An error will be returned if a temporary directory could not be created.
pub fn new() -> Result<Self> {
Ok(Self { path: TempDir::new()? })
}

/// add_env_vars will add the required environment variables to MiriEnv `e`.
pub fn add_env_vars(&self, e: &mut MiriEnv) -> Result<()> {
let mut rustflags = e.sh.var("RUSTFLAGS")?;
rustflags.push_str(" -C instrument-coverage");
e.sh.set_var("RUSTFLAGS", rustflags);

// Copy-pasting from: https://doc.rust-lang.org/rustc/instrument-coverage.html#instrumentation-based-code-coverage
// The format symbols below have the following meaning:
// - %p - The process ID.
// - %Nm - the instrumented binary’s signature:
// The runtime creates a pool of N raw profiles, used for on-line
// profile merging. The runtime takes care of selecting a raw profile
// from the pool, locking it, and updating it before the program
// exits. N must be between 1 and 9, and defaults to 1 if omitted
// (with simply %m).
//
// Additionally the default for LLVM_PROFILE_FILE is default_%m_%p.profraw.
// So we just use the same template, replacing "default" with "miri".
let file_template = self.path.path().join("miri_%m_%p.profraw");
e.sh.set_var("LLVM_PROFILE_FILE", file_template);
Ok(())
}

/// show_coverage_report will print coverage information using the artifact
/// files in `self.path`.
pub fn show_coverage_report(&self, e: &MiriEnv) -> Result<()> {
let profraw_files = self.profraw_files()?;

let profdata_bin = path!(e.libdir / ".." / "bin" / "llvm-profdata");

let merged_file = path!(e.miri_dir / "target" / "coverage.profdata");

// Merge the profraw files
cmd!(e.sh, "{profdata_bin} merge -sparse {profraw_files...} -o {merged_file}")
.quiet()
.run()?;

// Create the coverage report.
let cov_bin = path!(e.libdir / ".." / "bin" / "llvm-cov");
let miri_bin =
e.build_get_binary(".").context("failed to get filename of miri executable")?;
cmd!(
e.sh,
"{cov_bin} report --instr-profile={merged_file} --object {miri_bin} --sources src/"
)
.run()?;

RalfJung marked this conversation as resolved.
Show resolved Hide resolved
println!("Profile data saved in {}", merged_file.display());
Ok(())
}

/// profraw_files returns the profraw files in `self.path`.
///
/// # Errors
///
/// An error will be returned if `self.path` can't be read.
fn profraw_files(&self) -> Result<Vec<PathBuf>> {
Ok(std::fs::read_dir(&self.path)?
.filter_map(|r| r.ok())
.filter(|e| e.file_type().is_ok_and(|t| t.is_file()))
.map(|e| e.path())
RalfJung marked this conversation as resolved.
Show resolved Hide resolved
.filter(|p| p.extension().is_some_and(|e| e == "profraw"))
.collect())
}
}
8 changes: 7 additions & 1 deletion miri-script/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

mod args;
mod commands;
mod coverage;
mod util;

use std::ops::Range;
Expand Down Expand Up @@ -34,6 +35,8 @@ pub enum Command {
/// The cross-interpretation target.
/// If none then the host is the target.
target: Option<String>,
/// Produce coverage report if set.
coverage: bool,
/// Flags that are passed through to the test harness.
flags: Vec<String>,
},
Expand Down Expand Up @@ -158,9 +161,12 @@ fn main() -> Result<()> {
let mut target = None;
let mut bless = false;
let mut flags = Vec::new();
let mut coverage = false;
loop {
if args.get_long_flag("bless")? {
bless = true;
} else if args.get_long_flag("coverage")? {
coverage = true;
} else if let Some(val) = args.get_long_opt("target")? {
target = Some(val);
} else if let Some(flag) = args.get_other() {
Expand All @@ -169,7 +175,7 @@ fn main() -> Result<()> {
break;
}
}
Command::Test { bless, flags, target }
Command::Test { bless, flags, target, coverage }
}
Some("run") => {
let mut dep = false;
Expand Down
7 changes: 5 additions & 2 deletions miri-script/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub struct MiriEnv {
pub sysroot: PathBuf,
/// The shell we use.
pub sh: Shell,
/// The library dir in the sysroot.
pub libdir: PathBuf,
}

impl MiriEnv {
Expand Down Expand Up @@ -96,7 +98,8 @@ impl MiriEnv {
// so that Windows can find the DLLs.
if cfg!(windows) {
let old_path = sh.var("PATH")?;
let new_path = env::join_paths(iter::once(libdir).chain(env::split_paths(&old_path)))?;
let new_path =
env::join_paths(iter::once(libdir.clone()).chain(env::split_paths(&old_path)))?;
sh.set_var("PATH", new_path);
}

Expand All @@ -111,7 +114,7 @@ impl MiriEnv {
std::process::exit(1);
}

Ok(MiriEnv { miri_dir, toolchain, sh, sysroot, cargo_extra_flags })
Ok(MiriEnv { miri_dir, toolchain, sh, sysroot, cargo_extra_flags, libdir })
}

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