compiletest/runtest/
coverage.rs

1//! Code specific to the coverage test suites.
2
3use std::ffi::OsStr;
4use std::process::Command;
5
6use camino::{Utf8Path, Utf8PathBuf};
7use glob::glob;
8
9use crate::common::{UI_COVERAGE, UI_COVERAGE_MAP};
10use crate::runtest::{Emit, ProcRes, TestCx, WillExecute};
11use crate::util::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 = Command::new(coverage_dump_path);
31        dump_command.arg(llvm_ir_path);
32        let proc_res = self.run_command_to_procres(&mut 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        // Delete any existing profraw/profdata files to rule out unintended
74        // interference between repeated test runs.
75        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 == "coverage-run-rustdoc" {
95            self.run_doctests_for_coverage(&mut profraw_paths, &mut bin_paths);
96        }
97
98        // Run `llvm-profdata merge` to index the raw coverage output.
99        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        // Run `llvm-cov show` to produce a coverage report in text format.
110        let proc_res = self.run_llvm_tool("llvm-cov", |cmd| {
111            cmd.args(["show", "--format=text", "--show-line-counts-or-regions"]);
112
113            // Specify the demangler binary and its arguments.
114            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    /// Run any doctests embedded in this test file, and add any resulting
156    /// `.profraw` files and doctest executables to the given vectors.
157    fn run_doctests_for_coverage(
158        &self,
159        profraw_paths: &mut Vec<Utf8PathBuf>,
160        bin_paths: &mut Vec<Utf8PathBuf>,
161    ) {
162        // Put .profraw files and doctest executables in dedicated directories,
163        // to make it easier to glob them all later.
164        let profraws_dir = self.output_base_dir().join("doc_profraws");
165        let bins_dir = self.output_base_dir().join("doc_bins");
166
167        // Remove existing directories to prevent cross-run interference.
168        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        // In general there will be multiple doctest binaries running, so we
179        // tell the profiler runtime to write their coverage data into separate
180        // profraw files.
181        rustdoc_cmd.env("LLVM_PROFILE_FILE", profraws_dir.join("%p-%m.profraw"));
182
183        rustdoc_cmd.args(["--test", "-Cinstrument-coverage"]);
184
185        // Without this, the doctests complain about not being able to find
186        // their enclosing file's crate for some reason.
187        rustdoc_cmd.args(["--crate-name", "workaround_for_79771"]);
188
189        // Persist the doctest binaries so that `llvm-cov show` can read their
190        // embedded coverage mappings later.
191        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, self.testpaths);
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        // Find all profraw files in the profraw directory.
211        for p in glob_iter(profraws_dir.join("*.profraw")) {
212            profraw_paths.push(p);
213        }
214        // Find all executables in the `--persist-doctests` directory, while
215        // avoiding other file types (e.g. `.pdb` on Windows). This doesn't
216        // need to be perfect, as long as it can handle the files actually
217        // produced by `rustdoc --test`.
218        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(&self, name: &str, configure_cmd_fn: impl FnOnce(&mut Command)) -> ProcRes {
231        let tool_path = self
232            .config
233            .llvm_bin_dir
234            .as_ref()
235            .expect("this test expects the LLVM bin dir to be available")
236            .join(name);
237
238        let mut cmd = Command::new(tool_path);
239        configure_cmd_fn(&mut cmd);
240
241        self.run_command_to_procres(&mut cmd)
242    }
243
244    fn normalize_coverage_output(&self, coverage: &str) -> Result<String, String> {
245        let normalized = self.normalize_output(coverage, &[]);
246        let normalized = Self::anonymize_coverage_line_numbers(&normalized);
247
248        let mut lines = normalized.lines().collect::<Vec<_>>();
249
250        Self::sort_coverage_file_sections(&mut lines)?;
251        Self::sort_coverage_subviews(&mut lines)?;
252
253        let joined_lines = lines.iter().flat_map(|line| [line, "\n"]).collect::<String>();
254        Ok(joined_lines)
255    }
256
257    /// Replace line numbers in coverage reports with the placeholder `LL`,
258    /// so that the tests are less sensitive to lines being added/removed.
259    fn anonymize_coverage_line_numbers(coverage: &str) -> String {
260        // The coverage reporter prints line numbers at the start of a line.
261        // They are truncated or left-padded to occupy exactly 5 columns.
262        // (`LineNumberColumnWidth` in `SourceCoverageViewText.cpp`.)
263        // A pipe character `|` appears immediately after the final digit.
264        //
265        // Line numbers that appear inside expansion/instantiation subviews
266        // have an additional prefix of `  |` for each nesting level.
267        //
268        // Branch views also include the relevant line number, so we want to
269        // redact those too. (These line numbers don't have padding.)
270        //
271        // Note: The pattern `(?m:^)` matches the start of a line.
272
273        // `    1|` => `   LL|`
274        // `   10|` => `   LL|`
275        // `  100|` => `   LL|`
276        // `  | 1000|`    => `  |   LL|`
277        // `  |  | 1000|` => `  |  |   LL|`
278        let coverage = static_regex!(r"(?m:^)(?<prefix>(?:  \|)*) *[0-9]+\|")
279            .replace_all(&coverage, "${prefix}   LL|");
280
281        // `  |  Branch (1:`     => `  |  Branch (LL:`
282        // `  |  |  Branch (10:` => `  |  |  Branch (LL:`
283        let coverage = static_regex!(r"(?m:^)(?<prefix>(?:  \|)+  Branch \()[0-9]+:")
284            .replace_all(&coverage, "${prefix}LL:");
285
286        // `  |---> MC/DC Decision Region (1:30) to (2:`     => `  |---> MC/DC Decision Region (LL:30) to (LL:`
287        let coverage =
288            static_regex!(r"(?m:^)(?<prefix>(?:  \|)+---> MC/DC Decision Region \()[0-9]+:(?<middle>[0-9]+\) to \()[0-9]+:")
289            .replace_all(&coverage, "${prefix}LL:${middle}LL:");
290
291        // `  |     Condition C1 --> (1:`     => `  |     Condition C1 --> (LL:`
292        let coverage =
293            static_regex!(r"(?m:^)(?<prefix>(?:  \|)+     Condition C[0-9]+ --> \()[0-9]+:")
294                .replace_all(&coverage, "${prefix}LL:");
295
296        coverage.into_owned()
297    }
298
299    /// Coverage reports can describe multiple source files, separated by
300    /// blank lines. The order of these files is unpredictable (since it
301    /// depends on implementation details), so we need to sort the file
302    /// sections into a consistent order before comparing against a snapshot.
303    fn sort_coverage_file_sections(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
304        // Group the lines into file sections, separated by blank lines.
305        let mut sections = coverage_lines.split(|line| line.is_empty()).collect::<Vec<_>>();
306
307        // The last section should be empty, representing an extra trailing blank line.
308        if !sections.last().is_some_and(|last| last.is_empty()) {
309            return Err("coverage report should end with an extra blank line".to_owned());
310        }
311
312        // Sort the file sections (not including the final empty "section").
313        let except_last = sections.len() - 1;
314        (&mut sections[..except_last]).sort();
315
316        // Join the file sections back into a flat list of lines, with
317        // sections separated by blank lines.
318        let joined = sections.join(&[""] as &[_]);
319        assert_eq!(joined.len(), coverage_lines.len());
320        *coverage_lines = joined;
321
322        Ok(())
323    }
324
325    fn sort_coverage_subviews(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
326        let mut output_lines = Vec::new();
327
328        // We accumulate a list of zero or more "subviews", where each
329        // subview is a list of one or more lines.
330        let mut subviews: Vec<Vec<&str>> = Vec::new();
331
332        fn flush<'a>(subviews: &mut Vec<Vec<&'a str>>, output_lines: &mut Vec<&'a str>) {
333            if subviews.is_empty() {
334                return;
335            }
336
337            // Take and clear the list of accumulated subviews.
338            let mut subviews = std::mem::take(subviews);
339
340            // The last "subview" should be just a boundary line on its own,
341            // so exclude it when sorting the other subviews.
342            let except_last = subviews.len() - 1;
343            (&mut subviews[..except_last]).sort();
344
345            for view in subviews {
346                for line in view {
347                    output_lines.push(line);
348                }
349            }
350        }
351
352        for (line, line_num) in coverage_lines.iter().zip(1..) {
353            if line.starts_with("  ------------------") {
354                // This is a subview boundary line, so start a new subview.
355                subviews.push(vec![line]);
356            } else if line.starts_with("  |") {
357                // Add this line to the current subview.
358                subviews
359                    .last_mut()
360                    .ok_or_else(|| {
361                        format!("unexpected subview line outside of a subview on line {line_num}")
362                    })?
363                    .push(line);
364            } else {
365                // This line is not part of a subview, so sort and print any
366                // accumulated subviews, and then print the line as-is.
367                flush(&mut subviews, &mut output_lines);
368                output_lines.push(line);
369            }
370        }
371
372        flush(&mut subviews, &mut output_lines);
373        assert!(subviews.is_empty());
374
375        assert_eq!(output_lines.len(), coverage_lines.len());
376        *coverage_lines = output_lines;
377
378        Ok(())
379    }
380}