compiletest/runtest/
coverage.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
//! Code specific to the coverage test suites.

use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;

use glob::glob;

use crate::common::{UI_COVERAGE, UI_COVERAGE_MAP};
use crate::runtest::{Emit, ProcRes, TestCx, WillExecute};
use crate::util::static_regex;

impl<'test> TestCx<'test> {
    fn coverage_dump_path(&self) -> &Path {
        self.config
            .coverage_dump_path
            .as_deref()
            .unwrap_or_else(|| self.fatal("missing --coverage-dump"))
    }

    pub(super) fn run_coverage_map_test(&self) {
        let coverage_dump_path = self.coverage_dump_path();

        let (proc_res, llvm_ir_path) = self.compile_test_and_save_ir();
        if !proc_res.status.success() {
            self.fatal_proc_rec("compilation failed!", &proc_res);
        }
        drop(proc_res);

        let mut dump_command = Command::new(coverage_dump_path);
        dump_command.arg(llvm_ir_path);
        let proc_res = self.run_command_to_procres(&mut dump_command);
        if !proc_res.status.success() {
            self.fatal_proc_rec("coverage-dump failed!", &proc_res);
        }

        let kind = UI_COVERAGE_MAP;

        let expected_coverage_dump = self.load_expected_output(kind);
        let actual_coverage_dump = self.normalize_output(&proc_res.stdout, &[]);

        let coverage_dump_errors =
            self.compare_output(kind, &actual_coverage_dump, &expected_coverage_dump);

        if coverage_dump_errors > 0 {
            self.fatal_proc_rec(
                &format!("{coverage_dump_errors} errors occurred comparing coverage output."),
                &proc_res,
            );
        }
    }

    pub(super) fn run_coverage_run_test(&self) {
        let should_run = self.run_if_enabled();
        let proc_res = self.compile_test(should_run, Emit::None);

        if !proc_res.status.success() {
            self.fatal_proc_rec("compilation failed!", &proc_res);
        }
        drop(proc_res);

        if let WillExecute::Disabled = should_run {
            return;
        }

        let profraw_path = self.output_base_dir().join("default.profraw");
        let profdata_path = self.output_base_dir().join("default.profdata");

        // Delete any existing profraw/profdata files to rule out unintended
        // interference between repeated test runs.
        if profraw_path.exists() {
            std::fs::remove_file(&profraw_path).unwrap();
        }
        if profdata_path.exists() {
            std::fs::remove_file(&profdata_path).unwrap();
        }

        let proc_res = self.exec_compiled_test_general(
            &[("LLVM_PROFILE_FILE", &profraw_path.to_str().unwrap())],
            false,
        );
        if self.props.failure_status.is_some() {
            self.check_correct_failure_status(&proc_res);
        } else if !proc_res.status.success() {
            self.fatal_proc_rec("test run failed!", &proc_res);
        }
        drop(proc_res);

        let mut profraw_paths = vec![profraw_path];
        let mut bin_paths = vec![self.make_exe_name()];

        if self.config.suite == "coverage-run-rustdoc" {
            self.run_doctests_for_coverage(&mut profraw_paths, &mut bin_paths);
        }

        // Run `llvm-profdata merge` to index the raw coverage output.
        let proc_res = self.run_llvm_tool("llvm-profdata", |cmd| {
            cmd.args(["merge", "--sparse", "--output"]);
            cmd.arg(&profdata_path);
            cmd.args(&profraw_paths);
        });
        if !proc_res.status.success() {
            self.fatal_proc_rec("llvm-profdata merge failed!", &proc_res);
        }
        drop(proc_res);

        // Run `llvm-cov show` to produce a coverage report in text format.
        let proc_res = self.run_llvm_tool("llvm-cov", |cmd| {
            cmd.args(["show", "--format=text", "--show-line-counts-or-regions"]);

            // Specify the demangler binary and its arguments.
            let coverage_dump_path = self.coverage_dump_path();
            cmd.arg("--Xdemangler").arg(coverage_dump_path);
            cmd.arg("--Xdemangler").arg("--demangle");

            cmd.arg("--instr-profile");
            cmd.arg(&profdata_path);

            for bin in &bin_paths {
                cmd.arg("--object");
                cmd.arg(bin);
            }

            cmd.args(&self.props.llvm_cov_flags);
        });
        if !proc_res.status.success() {
            self.fatal_proc_rec("llvm-cov show failed!", &proc_res);
        }

        let kind = UI_COVERAGE;

        let expected_coverage = self.load_expected_output(kind);
        let normalized_actual_coverage =
            self.normalize_coverage_output(&proc_res.stdout).unwrap_or_else(|err| {
                self.fatal_proc_rec(&err, &proc_res);
            });

        let coverage_errors =
            self.compare_output(kind, &normalized_actual_coverage, &expected_coverage);

        if coverage_errors > 0 {
            self.fatal_proc_rec(
                &format!("{} errors occurred comparing coverage output.", coverage_errors),
                &proc_res,
            );
        }
    }

    /// Run any doctests embedded in this test file, and add any resulting
    /// `.profraw` files and doctest executables to the given vectors.
    fn run_doctests_for_coverage(
        &self,
        profraw_paths: &mut Vec<PathBuf>,
        bin_paths: &mut Vec<PathBuf>,
    ) {
        // Put .profraw files and doctest executables in dedicated directories,
        // to make it easier to glob them all later.
        let profraws_dir = self.output_base_dir().join("doc_profraws");
        let bins_dir = self.output_base_dir().join("doc_bins");

        // Remove existing directories to prevent cross-run interference.
        if profraws_dir.try_exists().unwrap() {
            std::fs::remove_dir_all(&profraws_dir).unwrap();
        }
        if bins_dir.try_exists().unwrap() {
            std::fs::remove_dir_all(&bins_dir).unwrap();
        }

        let mut rustdoc_cmd =
            Command::new(self.config.rustdoc_path.as_ref().expect("--rustdoc-path not passed"));

        // In general there will be multiple doctest binaries running, so we
        // tell the profiler runtime to write their coverage data into separate
        // profraw files.
        rustdoc_cmd.env("LLVM_PROFILE_FILE", profraws_dir.join("%p-%m.profraw"));

        rustdoc_cmd.args(["--test", "-Cinstrument-coverage"]);

        // Without this, the doctests complain about not being able to find
        // their enclosing file's crate for some reason.
        rustdoc_cmd.args(["--crate-name", "workaround_for_79771"]);

        // Persist the doctest binaries so that `llvm-cov show` can read their
        // embedded coverage mappings later.
        rustdoc_cmd.arg("-Zunstable-options");
        rustdoc_cmd.arg("--persist-doctests");
        rustdoc_cmd.arg(&bins_dir);

        rustdoc_cmd.arg("-L");
        rustdoc_cmd.arg(self.aux_output_dir_name());

        rustdoc_cmd.arg(&self.testpaths.file);

        let proc_res = self.compose_and_run_compiler(rustdoc_cmd, None, self.testpaths);
        if !proc_res.status.success() {
            self.fatal_proc_rec("rustdoc --test failed!", &proc_res)
        }

        fn glob_iter(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
            let path_str = path.as_ref().to_str().unwrap();
            let iter = glob(path_str).unwrap();
            iter.map(Result::unwrap)
        }

        // Find all profraw files in the profraw directory.
        for p in glob_iter(profraws_dir.join("*.profraw")) {
            profraw_paths.push(p);
        }
        // Find all executables in the `--persist-doctests` directory, while
        // avoiding other file types (e.g. `.pdb` on Windows). This doesn't
        // need to be perfect, as long as it can handle the files actually
        // produced by `rustdoc --test`.
        for p in glob_iter(bins_dir.join("**/*")) {
            let is_bin = p.is_file()
                && match p.extension() {
                    None => true,
                    Some(ext) => ext == OsStr::new("exe"),
                };
            if is_bin {
                bin_paths.push(p);
            }
        }
    }

    fn run_llvm_tool(&self, name: &str, configure_cmd_fn: impl FnOnce(&mut Command)) -> ProcRes {
        let tool_path = self
            .config
            .llvm_bin_dir
            .as_ref()
            .expect("this test expects the LLVM bin dir to be available")
            .join(name);

        let mut cmd = Command::new(tool_path);
        configure_cmd_fn(&mut cmd);

        self.run_command_to_procres(&mut cmd)
    }

    fn normalize_coverage_output(&self, coverage: &str) -> Result<String, String> {
        let normalized = self.normalize_output(coverage, &[]);
        let normalized = Self::anonymize_coverage_line_numbers(&normalized);

        let mut lines = normalized.lines().collect::<Vec<_>>();

        Self::sort_coverage_file_sections(&mut lines)?;
        Self::sort_coverage_subviews(&mut lines)?;

        let joined_lines = lines.iter().flat_map(|line| [line, "\n"]).collect::<String>();
        Ok(joined_lines)
    }

    /// Replace line numbers in coverage reports with the placeholder `LL`,
    /// so that the tests are less sensitive to lines being added/removed.
    fn anonymize_coverage_line_numbers(coverage: &str) -> String {
        // The coverage reporter prints line numbers at the start of a line.
        // They are truncated or left-padded to occupy exactly 5 columns.
        // (`LineNumberColumnWidth` in `SourceCoverageViewText.cpp`.)
        // A pipe character `|` appears immediately after the final digit.
        //
        // Line numbers that appear inside expansion/instantiation subviews
        // have an additional prefix of `  |` for each nesting level.
        //
        // Branch views also include the relevant line number, so we want to
        // redact those too. (These line numbers don't have padding.)
        //
        // Note: The pattern `(?m:^)` matches the start of a line.

        // `    1|` => `   LL|`
        // `   10|` => `   LL|`
        // `  100|` => `   LL|`
        // `  | 1000|`    => `  |   LL|`
        // `  |  | 1000|` => `  |  |   LL|`
        let coverage = static_regex!(r"(?m:^)(?<prefix>(?:  \|)*) *[0-9]+\|")
            .replace_all(&coverage, "${prefix}   LL|");

        // `  |  Branch (1:`     => `  |  Branch (LL:`
        // `  |  |  Branch (10:` => `  |  |  Branch (LL:`
        let coverage = static_regex!(r"(?m:^)(?<prefix>(?:  \|)+  Branch \()[0-9]+:")
            .replace_all(&coverage, "${prefix}LL:");

        // `  |---> MC/DC Decision Region (1:30) to (2:`     => `  |---> MC/DC Decision Region (LL:30) to (LL:`
        let coverage =
            static_regex!(r"(?m:^)(?<prefix>(?:  \|)+---> MC/DC Decision Region \()[0-9]+:(?<middle>[0-9]+\) to \()[0-9]+:")
            .replace_all(&coverage, "${prefix}LL:${middle}LL:");

        // `  |     Condition C1 --> (1:`     => `  |     Condition C1 --> (LL:`
        let coverage =
            static_regex!(r"(?m:^)(?<prefix>(?:  \|)+     Condition C[0-9]+ --> \()[0-9]+:")
                .replace_all(&coverage, "${prefix}LL:");

        coverage.into_owned()
    }

    /// Coverage reports can describe multiple source files, separated by
    /// blank lines. The order of these files is unpredictable (since it
    /// depends on implementation details), so we need to sort the file
    /// sections into a consistent order before comparing against a snapshot.
    fn sort_coverage_file_sections(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
        // Group the lines into file sections, separated by blank lines.
        let mut sections = coverage_lines.split(|line| line.is_empty()).collect::<Vec<_>>();

        // The last section should be empty, representing an extra trailing blank line.
        if !sections.last().is_some_and(|last| last.is_empty()) {
            return Err("coverage report should end with an extra blank line".to_owned());
        }

        // Sort the file sections (not including the final empty "section").
        let except_last = sections.len() - 1;
        (&mut sections[..except_last]).sort();

        // Join the file sections back into a flat list of lines, with
        // sections separated by blank lines.
        let joined = sections.join(&[""] as &[_]);
        assert_eq!(joined.len(), coverage_lines.len());
        *coverage_lines = joined;

        Ok(())
    }

    fn sort_coverage_subviews(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
        let mut output_lines = Vec::new();

        // We accumulate a list of zero or more "subviews", where each
        // subview is a list of one or more lines.
        let mut subviews: Vec<Vec<&str>> = Vec::new();

        fn flush<'a>(subviews: &mut Vec<Vec<&'a str>>, output_lines: &mut Vec<&'a str>) {
            if subviews.is_empty() {
                return;
            }

            // Take and clear the list of accumulated subviews.
            let mut subviews = std::mem::take(subviews);

            // The last "subview" should be just a boundary line on its own,
            // so exclude it when sorting the other subviews.
            let except_last = subviews.len() - 1;
            (&mut subviews[..except_last]).sort();

            for view in subviews {
                for line in view {
                    output_lines.push(line);
                }
            }
        }

        for (line, line_num) in coverage_lines.iter().zip(1..) {
            if line.starts_with("  ------------------") {
                // This is a subview boundary line, so start a new subview.
                subviews.push(vec![line]);
            } else if line.starts_with("  |") {
                // Add this line to the current subview.
                subviews
                    .last_mut()
                    .ok_or(format!(
                        "unexpected subview line outside of a subview on line {line_num}"
                    ))?
                    .push(line);
            } else {
                // This line is not part of a subview, so sort and print any
                // accumulated subviews, and then print the line as-is.
                flush(&mut subviews, &mut output_lines);
                output_lines.push(line);
            }
        }

        flush(&mut subviews, &mut output_lines);
        assert!(subviews.is_empty());

        assert_eq!(output_lines.len(), coverage_lines.len());
        *coverage_lines = output_lines;

        Ok(())
    }
}