compiletest/runtest/
coverage.rs1use std::ffi::OsStr;
4use std::process::Command;
5
6use camino::{Utf8Path, Utf8PathBuf};
7use glob::glob;
8
9use crate::common::{TestSuite, UI_COVERAGE, UI_COVERAGE_MAP};
10use crate::runtest::{Emit, ProcRes, TestCx, WillExecute};
11use crate::util::{ArgFileCommand, static_regex};
12
13impl<'test> TestCx<'test> {
14 fn coverage_dump_path(&self) -> &Utf8Path {
15 self.config
16 .coverage_dump_path
17 .as_deref()
18 .unwrap_or_else(|| self.fatal("missing --coverage-dump"))
19 }
20
21 pub(super) fn run_coverage_map_test(&self) {
22 let coverage_dump_path = self.coverage_dump_path();
23
24 let (proc_res, llvm_ir_path) = self.compile_test_and_save_ir();
25 if !proc_res.status.success() {
26 self.fatal_proc_rec("compilation failed!", &proc_res);
27 }
28 drop(proc_res);
29
30 let mut dump_command = ArgFileCommand::new(coverage_dump_path);
31 dump_command.arg(llvm_ir_path);
32 let proc_res = self.run_command_to_procres(dump_command);
33 if !proc_res.status.success() {
34 self.fatal_proc_rec("coverage-dump failed!", &proc_res);
35 }
36
37 let kind = UI_COVERAGE_MAP;
38
39 let expected_coverage_dump = self.load_expected_output(kind);
40 let actual_coverage_dump = self.normalize_output(&proc_res.stdout, &[]);
41
42 let coverage_dump_compare_outcome = self.compare_output(
43 kind,
44 &actual_coverage_dump,
45 &proc_res.stdout,
46 &expected_coverage_dump,
47 );
48
49 if coverage_dump_compare_outcome.should_error() {
50 self.fatal_proc_rec(
51 &format!("an error occurred comparing coverage output."),
52 &proc_res,
53 );
54 }
55 }
56
57 pub(super) fn run_coverage_run_test(&self) {
58 let should_run = self.run_if_enabled();
59 let proc_res = self.compile_test(should_run, Emit::None);
60
61 if !proc_res.status.success() {
62 self.fatal_proc_rec("compilation failed!", &proc_res);
63 }
64 drop(proc_res);
65
66 if let WillExecute::Disabled = should_run {
67 return;
68 }
69
70 let profraw_path = self.output_base_dir().join("default.profraw");
71 let profdata_path = self.output_base_dir().join("default.profdata");
72
73 if profraw_path.exists() {
76 std::fs::remove_file(&profraw_path).unwrap();
77 }
78 if profdata_path.exists() {
79 std::fs::remove_file(&profdata_path).unwrap();
80 }
81
82 let proc_res =
83 self.exec_compiled_test_general(&[("LLVM_PROFILE_FILE", profraw_path.as_str())], false);
84 if self.props.failure_status.is_some() {
85 self.check_correct_failure_status(&proc_res);
86 } else if !proc_res.status.success() {
87 self.fatal_proc_rec("test run failed!", &proc_res);
88 }
89 drop(proc_res);
90
91 let mut profraw_paths = vec![profraw_path];
92 let mut bin_paths = vec![self.make_exe_name()];
93
94 if self.config.suite == TestSuite::CoverageRunRustdoc {
95 self.run_doctests_for_coverage(&mut profraw_paths, &mut bin_paths);
96 }
97
98 let proc_res = self.run_llvm_tool("llvm-profdata", |cmd| {
100 cmd.args(["merge", "--sparse", "--output"]);
101 cmd.arg(&profdata_path);
102 cmd.args(&profraw_paths);
103 });
104 if !proc_res.status.success() {
105 self.fatal_proc_rec("llvm-profdata merge failed!", &proc_res);
106 }
107 drop(proc_res);
108
109 let proc_res = self.run_llvm_tool("llvm-cov", |cmd| {
111 cmd.args(["show", "--format=text", "--show-line-counts-or-regions"]);
112
113 let coverage_dump_path = self.coverage_dump_path();
115 cmd.arg("--Xdemangler").arg(coverage_dump_path);
116 cmd.arg("--Xdemangler").arg("--demangle");
117
118 cmd.arg("--instr-profile");
119 cmd.arg(&profdata_path);
120
121 for bin in &bin_paths {
122 cmd.arg("--object");
123 cmd.arg(bin);
124 }
125
126 cmd.args(&self.props.llvm_cov_flags);
127 });
128 if !proc_res.status.success() {
129 self.fatal_proc_rec("llvm-cov show failed!", &proc_res);
130 }
131
132 let kind = UI_COVERAGE;
133
134 let expected_coverage = self.load_expected_output(kind);
135 let normalized_actual_coverage =
136 self.normalize_coverage_output(&proc_res.stdout).unwrap_or_else(|err| {
137 self.fatal_proc_rec(&err, &proc_res);
138 });
139
140 let coverage_dump_compare_outcome = self.compare_output(
141 kind,
142 &normalized_actual_coverage,
143 &proc_res.stdout,
144 &expected_coverage,
145 );
146
147 if coverage_dump_compare_outcome.should_error() {
148 self.fatal_proc_rec(
149 &format!("an error occurred comparing coverage output."),
150 &proc_res,
151 );
152 }
153 }
154
155 fn run_doctests_for_coverage(
158 &self,
159 profraw_paths: &mut Vec<Utf8PathBuf>,
160 bin_paths: &mut Vec<Utf8PathBuf>,
161 ) {
162 let profraws_dir = self.output_base_dir().join("doc_profraws");
165 let bins_dir = self.output_base_dir().join("doc_bins");
166
167 if profraws_dir.try_exists().unwrap() {
169 std::fs::remove_dir_all(&profraws_dir).unwrap();
170 }
171 if bins_dir.try_exists().unwrap() {
172 std::fs::remove_dir_all(&bins_dir).unwrap();
173 }
174
175 let mut rustdoc_cmd =
176 Command::new(self.config.rustdoc_path.as_ref().expect("--rustdoc-path not passed"));
177
178 rustdoc_cmd.env("LLVM_PROFILE_FILE", profraws_dir.join("%p-%m.profraw"));
182
183 rustdoc_cmd.args(["--test", "-Cinstrument-coverage"]);
184
185 rustdoc_cmd.args(["--crate-name", "workaround_for_79771"]);
188
189 rustdoc_cmd.arg("-Zunstable-options");
192 rustdoc_cmd.arg("--persist-doctests");
193 rustdoc_cmd.arg(&bins_dir);
194
195 rustdoc_cmd.arg("-L");
196 rustdoc_cmd.arg(self.aux_output_dir_name());
197
198 rustdoc_cmd.arg(&self.testpaths.file);
199
200 let proc_res = self.compose_and_run_compiler(rustdoc_cmd, None);
201 if !proc_res.status.success() {
202 self.fatal_proc_rec("rustdoc --test failed!", &proc_res)
203 }
204
205 fn glob_iter(path: impl AsRef<Utf8Path>) -> impl Iterator<Item = Utf8PathBuf> {
206 let iter = glob(path.as_ref().as_str()).unwrap();
207 iter.map(Result::unwrap).map(Utf8PathBuf::try_from).map(Result::unwrap)
208 }
209
210 for p in glob_iter(profraws_dir.join("*.profraw")) {
212 profraw_paths.push(p);
213 }
214 for p in glob_iter(bins_dir.join("**/*")) {
219 let is_bin = p.is_file()
220 && match p.extension() {
221 None => true,
222 Some(ext) => ext == OsStr::new("exe"),
223 };
224 if is_bin {
225 bin_paths.push(p);
226 }
227 }
228 }
229
230 fn run_llvm_tool(
231 &self,
232 name: &str,
233 configure_cmd_fn: impl FnOnce(&mut ArgFileCommand),
234 ) -> ProcRes {
235 let tool_path = self
236 .config
237 .llvm_bin_dir
238 .as_ref()
239 .expect("this test expects the LLVM bin dir to be available")
240 .join(name);
241
242 let mut cmd = ArgFileCommand::new(tool_path);
243 configure_cmd_fn(&mut cmd);
244
245 self.run_command_to_procres(cmd)
246 }
247
248 fn normalize_coverage_output(&self, coverage: &str) -> Result<String, String> {
249 let normalized = self.normalize_output(coverage, &[]);
250 let normalized = Self::anonymize_coverage_line_numbers(&normalized);
251
252 let mut lines = normalized.lines().collect::<Vec<_>>();
253
254 Self::sort_coverage_file_sections(&mut lines)?;
255 Self::sort_coverage_subviews(&mut lines)?;
256
257 let joined_lines = lines.iter().flat_map(|line| [line, "\n"]).collect::<String>();
258 Ok(joined_lines)
259 }
260
261 fn anonymize_coverage_line_numbers(coverage: &str) -> String {
264 let coverage = static_regex!(r"(?m:^)(?<prefix>(?: \|)*) *[0-9]+\|")
283 .replace_all(&coverage, "${prefix} LL|");
284
285 let coverage = static_regex!(r"(?m:^)(?<prefix>(?: \|)+ Branch \()[0-9]+:")
288 .replace_all(&coverage, "${prefix}LL:");
289
290 let coverage =
292 static_regex!(r"(?m:^)(?<prefix>(?: \|)+---> MC/DC Decision Region \()[0-9]+:(?<middle>[0-9]+\) to \()[0-9]+:")
293 .replace_all(&coverage, "${prefix}LL:${middle}LL:");
294
295 let coverage =
297 static_regex!(r"(?m:^)(?<prefix>(?: \|)+ Condition C[0-9]+ --> \()[0-9]+:")
298 .replace_all(&coverage, "${prefix}LL:");
299
300 coverage.into_owned()
301 }
302
303 fn sort_coverage_file_sections(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
308 let mut sections = coverage_lines.split(|line| line.is_empty()).collect::<Vec<_>>();
310
311 if !sections.last().is_some_and(|last| last.is_empty()) {
313 return Err("coverage report should end with an extra blank line".to_owned());
314 }
315
316 let except_last = sections.len() - 1;
318 (&mut sections[..except_last]).sort();
319
320 let joined = sections.join(&[""] as &[_]);
323 assert_eq!(joined.len(), coverage_lines.len());
324 *coverage_lines = joined;
325
326 Ok(())
327 }
328
329 fn sort_coverage_subviews(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
330 let mut output_lines = Vec::new();
331
332 let mut subviews: Vec<Vec<&str>> = Vec::new();
335
336 fn flush<'a>(subviews: &mut Vec<Vec<&'a str>>, output_lines: &mut Vec<&'a str>) {
337 if subviews.is_empty() {
338 return;
339 }
340
341 let mut subviews = std::mem::take(subviews);
343
344 let except_last = subviews.len() - 1;
347 (&mut subviews[..except_last]).sort();
348
349 for view in subviews {
350 for line in view {
351 output_lines.push(line);
352 }
353 }
354 }
355
356 for (line, line_num) in coverage_lines.iter().zip(1..) {
357 if line.starts_with(" ------------------") {
358 subviews.push(vec![line]);
360 } else if line.starts_with(" |") {
361 subviews
363 .last_mut()
364 .ok_or_else(|| {
365 format!("unexpected subview line outside of a subview on line {line_num}")
366 })?
367 .push(line);
368 } else {
369 flush(&mut subviews, &mut output_lines);
372 output_lines.push(line);
373 }
374 }
375
376 flush(&mut subviews, &mut output_lines);
377 assert!(subviews.is_empty());
378
379 assert_eq!(output_lines.len(), coverage_lines.len());
380 *coverage_lines = output_lines;
381
382 Ok(())
383 }
384}