Merge pull request #3954 from Mandragorian/coverage-report
Add option for generating coverage reports
This commit is contained in:
commit
9023e35fc4
13
src/tools/miri/.github/workflows/ci.yml
vendored
13
src/tools/miri/.github/workflows/ci.yml
vendored
@ -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
|
||||
@ -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
|
||||
|
@ -63,6 +63,12 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.12"
|
||||
@ -100,9 +106,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
version = "0.2.159"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
@ -138,11 +144,18 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"serde_json",
|
||||
"shell-words",
|
||||
"tempfile",
|
||||
"walkdir",
|
||||
"which",
|
||||
"xshell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@ -195,9 +208,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.34"
|
||||
version = "0.38.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
@ -276,6 +289,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.57"
|
||||
@ -357,6 +383,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
|
@ -24,3 +24,4 @@ rustc_version = "0.4"
|
||||
dunce = "1.0.4"
|
||||
directories = "5"
|
||||
serde_json = "1"
|
||||
tempfile = "3.13.0"
|
||||
|
@ -172,7 +172,8 @@ pub fn exec(self) -> Result<()> {
|
||||
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),
|
||||
@ -458,9 +459,20 @@ fn clippy(flags: Vec<String>) -> Result<()> {
|
||||
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())?;
|
||||
|
||||
@ -479,6 +491,11 @@ fn test(bless: bool, mut flags: Vec<String>, target: Option<String>) -> Result<(
|
||||
// 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(())
|
||||
}
|
||||
|
||||
|
91
src/tools/miri/miri-script/src/coverage.rs
Normal file
91
src/tools/miri/miri-script/src/coverage.rs
Normal file
@ -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()?;
|
||||
|
||||
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())
|
||||
.filter(|p| p.extension().is_some_and(|e| e == "profraw"))
|
||||
.collect())
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
mod args;
|
||||
mod commands;
|
||||
mod coverage;
|
||||
mod util;
|
||||
|
||||
use std::ops::Range;
|
||||
@ -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>,
|
||||
},
|
||||
@ -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() {
|
||||
@ -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;
|
||||
|
@ -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 {
|
||||
@ -96,7 +98,8 @@ pub fn new() -> Result<Self> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -111,7 +114,7 @@ pub fn new() -> Result<Self> {
|
||||
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<'_> {
|
||||
|
Loading…
Reference in New Issue
Block a user