xref: /aosp_15_r20/external/bazelbuild-rules_rust/util/collect_coverage/collect_coverage.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
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