1 //! This script collects code coverage data for Rust sources, after the tests
2 //! were executed.
3 //!
4 //! By taking advantage of Bazel C++ code coverage collection, this script is
5 //! able to be executed by the existing coverage collection mechanics.
6 //!
7 //! Bazel uses the lcov tool for gathering coverage data. There is also
8 //! an experimental support for clang llvm coverage, which uses the .profraw
9 //! data files to compute the coverage report.
10 //!
11 //! This script assumes the following environment variables are set:
12 //! - COVERAGE_DIR Directory containing metadata files needed for
13 //! coverage collection (e.g. gcda files, profraw).
14 //! - COVERAGE_OUTPUT_FILE The coverage action output path.
15 //! - ROOT Location from where the code coverage collection
16 //! was invoked.
17 //! - RUNFILES_DIR Location of the test's runfiles.
18 //! - VERBOSE_COVERAGE Print debug info from the coverage scripts
19 //!
20 //! The script looks in $COVERAGE_DIR for the Rust metadata coverage files
21 //! (profraw) and uses lcov to get the coverage data. The coverage data
22 //! is placed in $COVERAGE_DIR as a `coverage.dat` file.
23
24 use std::env;
25 use std::fs;
26 use std::path::Path;
27 use std::path::PathBuf;
28 use std::process;
29
30 macro_rules! log {
31 ($($arg:tt)*) => {
32 if env::var("VERBOSE_COVERAGE").is_ok() {
33 eprintln!($($arg)*);
34 }
35 };
36 }
37
find_metadata_file(execroot: &Path, runfiles_dir: &Path, path: &str) -> PathBuf38 fn find_metadata_file(execroot: &Path, runfiles_dir: &Path, path: &str) -> PathBuf {
39 if execroot.join(path).exists() {
40 return execroot.join(path);
41 }
42
43 log!(
44 "File does not exist in execroot, falling back to runfiles: {}",
45 path
46 );
47
48 runfiles_dir.join(path)
49 }
50
find_test_binary(execroot: &Path, runfiles_dir: &Path) -> PathBuf51 fn find_test_binary(execroot: &Path, runfiles_dir: &Path) -> PathBuf {
52 let test_binary = runfiles_dir
53 .join(env::var("TEST_WORKSPACE").unwrap())
54 .join(env::var("TEST_BINARY").unwrap());
55
56 if !test_binary.exists() {
57 let configuration = runfiles_dir
58 .strip_prefix(execroot)
59 .expect("RUNFILES_DIR should be relative to ROOT")
60 .components()
61 .enumerate()
62 .filter_map(|(i, part)| {
63 // Keep only `bazel-out/<configuration>/bin`
64 if i < 3 {
65 Some(PathBuf::from(part.as_os_str()))
66 } else {
67 None
68 }
69 })
70 .fold(PathBuf::new(), |mut path, part| {
71 path.push(part);
72 path
73 });
74
75 let test_binary = execroot
76 .join(configuration)
77 .join(env::var("TEST_BINARY").unwrap());
78
79 log!(
80 "TEST_BINARY is not found in runfiles. Falling back to: {}",
81 test_binary.display()
82 );
83
84 test_binary
85 } else {
86 test_binary
87 }
88 }
89
main()90 fn main() {
91 let coverage_dir = PathBuf::from(env::var("COVERAGE_DIR").unwrap());
92 let execroot = PathBuf::from(env::var("ROOT").unwrap());
93 let mut runfiles_dir = PathBuf::from(env::var("RUNFILES_DIR").unwrap());
94
95 if !runfiles_dir.is_absolute() {
96 runfiles_dir = execroot.join(runfiles_dir);
97 }
98
99 log!("ROOT: {}", execroot.display());
100 log!("RUNFILES_DIR: {}", runfiles_dir.display());
101
102 let coverage_output_file = coverage_dir.join("coverage.dat");
103 let profdata_file = coverage_dir.join("coverage.profdata");
104 let llvm_cov = find_metadata_file(
105 &execroot,
106 &runfiles_dir,
107 &env::var("RUST_LLVM_COV").unwrap(),
108 );
109 let llvm_profdata = find_metadata_file(
110 &execroot,
111 &runfiles_dir,
112 &env::var("RUST_LLVM_PROFDATA").unwrap(),
113 );
114 let test_binary = find_test_binary(&execroot, &runfiles_dir);
115 let profraw_files: Vec<PathBuf> = fs::read_dir(coverage_dir)
116 .unwrap()
117 .flatten()
118 .filter_map(|entry| {
119 let path = entry.path();
120 if let Some(ext) = path.extension() {
121 if ext == "profraw" {
122 return Some(path);
123 }
124 }
125 None
126 })
127 .collect();
128
129 let mut llvm_profdata_cmd = process::Command::new(llvm_profdata);
130 llvm_profdata_cmd
131 .arg("merge")
132 .arg("--sparse")
133 .args(profraw_files)
134 .arg("--output")
135 .arg(&profdata_file);
136
137 log!("Spawning {:#?}", llvm_profdata_cmd);
138 let status = llvm_profdata_cmd
139 .status()
140 .expect("Failed to spawn llvm-profdata process");
141
142 if !status.success() {
143 process::exit(status.code().unwrap_or(1));
144 }
145
146 let mut llvm_cov_cmd = process::Command::new(llvm_cov);
147 llvm_cov_cmd
148 .arg("export")
149 .arg("-format=lcov")
150 .arg("-instr-profile")
151 .arg(&profdata_file)
152 .arg("-ignore-filename-regex='.*external/.+'")
153 .arg("-ignore-filename-regex='/tmp/.+'")
154 .arg(format!("-path-equivalence=.,'{}'", execroot.display()))
155 .arg(test_binary)
156 .stdout(process::Stdio::piped());
157
158 log!("Spawning {:#?}", llvm_cov_cmd);
159 let child = llvm_cov_cmd
160 .spawn()
161 .expect("Failed to spawn llvm-cov process");
162
163 let output = child.wait_with_output().expect("llvm-cov process failed");
164
165 // Parse the child process's stdout to a string now that it's complete.
166 log!("Parsing llvm-cov output");
167 let report_str = std::str::from_utf8(&output.stdout).expect("Failed to parse llvm-cov output");
168
169 log!("Writing output to {}", coverage_output_file.display());
170 fs::write(
171 coverage_output_file,
172 report_str
173 .replace("#/proc/self/cwd/", "")
174 .replace(&execroot.display().to_string(), ""),
175 )
176 .unwrap();
177
178 // Destroy the intermediate binary file so lcov_merger doesn't parse it twice.
179 log!("Cleaning up {}", profdata_file.display());
180 fs::remove_file(profdata_file).unwrap();
181
182 log!("Success!");
183 }
184