blob: fcd3c0ad4f098a19847d9c0db1d52c70ec64d11c [file] [log] [blame]
//! This script collects code coverage data for Rust sources, after the tests
//! were executed.
//!
//! By taking advantage of Bazel C++ code coverage collection, this script is
//! able to be executed by the existing coverage collection mechanics.
//!
//! Bazel uses the lcov tool for gathering coverage data. There is also
//! an experimental support for clang llvm coverage, which uses the .profraw
//! data files to compute the coverage report.
//!
//! This script assumes the following environment variables are set:
//! - COVERAGE_DIR Directory containing metadata files needed for
//! coverage collection (e.g. gcda files, profraw).
//! - COVERAGE_OUTPUT_FILE The coverage action output path.
//! - ROOT Location from where the code coverage collection
//! was invoked.
//! - RUNFILES_DIR Location of the test's runfiles.
//! - VERBOSE_COVERAGE Print debug info from the coverage scripts
//!
//! The script looks in $COVERAGE_DIR for the Rust metadata coverage files
//! (profraw) and uses lcov to get the coverage data. The coverage data
//! is placed in $COVERAGE_DIR as a `coverage.dat` file.
use std::env;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process;
macro_rules! log {
($($arg:tt)*) => {
if env::var("VERBOSE_COVERAGE").is_ok() {
eprintln!($($arg)*);
}
};
}
fn find_metadata_file(execroot: &Path, runfiles_dir: &Path, path: &str) -> PathBuf {
if execroot.join(path).exists() {
return execroot.join(path);
}
log!(
"File does not exist in execroot, falling back to runfiles: {}",
path
);
runfiles_dir.join(path)
}
fn find_test_binary(execroot: &Path, runfiles_dir: &Path) -> PathBuf {
let test_binary = runfiles_dir
.join(env::var("TEST_WORKSPACE").unwrap())
.join(env::var("TEST_BINARY").unwrap());
if !test_binary.exists() {
let configuration = runfiles_dir
.strip_prefix(execroot)
.expect("RUNFILES_DIR should be relative to ROOT")
.components()
.enumerate()
.filter_map(|(i, part)| {
// Keep only `bazel-out/<configuration>/bin`
if i < 3 {
Some(PathBuf::from(part.as_os_str()))
} else {
None
}
})
.fold(PathBuf::new(), |mut path, part| {
path.push(part);
path
});
let test_binary = execroot
.join(configuration)
.join(env::var("TEST_BINARY").unwrap());
log!(
"TEST_BINARY is not found in runfiles. Falling back to: {}",
test_binary.display()
);
test_binary
} else {
test_binary
}
}
fn main() {
let coverage_dir = PathBuf::from(env::var("COVERAGE_DIR").unwrap());
let execroot = PathBuf::from(env::var("ROOT").unwrap());
let mut runfiles_dir = PathBuf::from(env::var("RUNFILES_DIR").unwrap());
if !runfiles_dir.is_absolute() {
runfiles_dir = execroot.join(runfiles_dir);
}
log!("ROOT: {}", execroot.display());
log!("RUNFILES_DIR: {}", runfiles_dir.display());
let coverage_output_file = coverage_dir.join("coverage.dat");
let profdata_file = coverage_dir.join("coverage.profdata");
let llvm_cov = find_metadata_file(
&execroot,
&runfiles_dir,
&env::var("RUST_LLVM_COV").unwrap(),
);
let llvm_profdata = find_metadata_file(
&execroot,
&runfiles_dir,
&env::var("RUST_LLVM_PROFDATA").unwrap(),
);
let test_binary = find_test_binary(&execroot, &runfiles_dir);
let profraw_files: Vec<PathBuf> = fs::read_dir(coverage_dir)
.unwrap()
.flatten()
.filter_map(|entry| {
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "profraw" {
return Some(path);
}
}
None
})
.collect();
let mut llvm_profdata_cmd = process::Command::new(llvm_profdata);
llvm_profdata_cmd
.arg("merge")
.arg("--sparse")
.args(profraw_files)
.arg("--output")
.arg(&profdata_file);
log!("Spawning {:#?}", llvm_profdata_cmd);
let status = llvm_profdata_cmd
.status()
.expect("Failed to spawn llvm-profdata process");
if !status.success() {
process::exit(status.code().unwrap_or(1));
}
let mut llvm_cov_cmd = process::Command::new(llvm_cov);
llvm_cov_cmd
.arg("export")
.arg("-format=lcov")
.arg("-instr-profile")
.arg(&profdata_file)
.arg("-ignore-filename-regex='.*external/.+'")
.arg("-ignore-filename-regex='/tmp/.+'")
.arg(format!("-path-equivalence=.,'{}'", execroot.display()))
.arg(test_binary)
.stdout(process::Stdio::piped());
log!("Spawning {:#?}", llvm_cov_cmd);
let child = llvm_cov_cmd
.spawn()
.expect("Failed to spawn llvm-cov process");
let output = child.wait_with_output().expect("llvm-cov process failed");
// Parse the child process's stdout to a string now that it's complete.
log!("Parsing llvm-cov output");
let report_str = std::str::from_utf8(&output.stdout).expect("Failed to parse llvm-cov output");
log!("Writing output to {}", coverage_output_file.display());
fs::write(
coverage_output_file,
report_str
.replace("#/proc/self/cwd/", "")
.replace(&execroot.display().to_string(), ""),
)
.unwrap();
// Destroy the intermediate binary file so lcov_merger doesn't parse it twice.
log!("Cleaning up {}", profdata_file.display());
fs::remove_file(profdata_file).unwrap();
log!("Success!");
}