//! 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//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 = 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!"); }