Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 268b9a7

Browse files
committedMay 29, 2024
Extract coverage-specific code out of compiletest::runtest
This moves a few hundred lines of coverage-specific code out of the main module, making navigation a bit easier.
1 parent 5870f1c commit 268b9a7

File tree

3 files changed

+372
-426
lines changed

3 files changed

+372
-426
lines changed
 

‎src/tools/compiletest/src/runtest.rs

Lines changed: 5 additions & 358 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::common::{Codegen, CodegenUnits, DebugInfo, Debugger, Rustdoc};
99
use crate::common::{CompareMode, FailMode, PassMode};
1010
use crate::common::{Config, TestPaths};
1111
use crate::common::{CoverageMap, CoverageRun, Pretty, RunPassValgrind};
12-
use crate::common::{UI_COVERAGE, UI_COVERAGE_MAP, UI_RUN_STDERR, UI_RUN_STDOUT};
12+
use crate::common::{UI_RUN_STDERR, UI_RUN_STDOUT};
1313
use crate::compute_diff::{write_diff, write_filtered_diff};
1414
use crate::errors::{self, Error, ErrorKind};
1515
use crate::header::TestProps;
@@ -41,6 +41,7 @@ use tracing::*;
4141
use crate::extract_gdb_version;
4242
use crate::is_android_gdb_target;
4343

44+
mod coverage;
4445
mod debugger;
4546
use debugger::DebuggerCommands;
4647

@@ -53,6 +54,7 @@ macro_rules! static_regex {
5354
RE.get_or_init(|| ::regex::Regex::new($re).unwrap())
5455
}};
5556
}
57+
use static_regex;
5658

5759
const FAKE_SRC_BASE: &str = "fake-test-src-base";
5860

@@ -267,8 +269,8 @@ impl<'test> TestCx<'test> {
267269
MirOpt => self.run_mir_opt_test(),
268270
Assembly => self.run_assembly_test(),
269271
JsDocTest => self.run_js_doc_test(),
270-
CoverageMap => self.run_coverage_map_test(),
271-
CoverageRun => self.run_coverage_run_test(),
272+
CoverageMap => self.run_coverage_map_test(), // see self::coverage
273+
CoverageRun => self.run_coverage_run_test(), // see self::coverage
272274
Crashes => self.run_crash_test(),
273275
}
274276
}
@@ -504,224 +506,6 @@ impl<'test> TestCx<'test> {
504506
}
505507
}
506508

507-
fn run_coverage_map_test(&self) {
508-
let Some(coverage_dump_path) = &self.config.coverage_dump_path else {
509-
self.fatal("missing --coverage-dump");
510-
};
511-
512-
let (proc_res, llvm_ir_path) = self.compile_test_and_save_ir();
513-
if !proc_res.status.success() {
514-
self.fatal_proc_rec("compilation failed!", &proc_res);
515-
}
516-
drop(proc_res);
517-
518-
let mut dump_command = Command::new(coverage_dump_path);
519-
dump_command.arg(llvm_ir_path);
520-
let proc_res = self.run_command_to_procres(&mut dump_command);
521-
if !proc_res.status.success() {
522-
self.fatal_proc_rec("coverage-dump failed!", &proc_res);
523-
}
524-
525-
let kind = UI_COVERAGE_MAP;
526-
527-
let expected_coverage_dump = self.load_expected_output(kind);
528-
let actual_coverage_dump = self.normalize_output(&proc_res.stdout, &[]);
529-
530-
let coverage_dump_errors =
531-
self.compare_output(kind, &actual_coverage_dump, &expected_coverage_dump);
532-
533-
if coverage_dump_errors > 0 {
534-
self.fatal_proc_rec(
535-
&format!("{coverage_dump_errors} errors occurred comparing coverage output."),
536-
&proc_res,
537-
);
538-
}
539-
}
540-
541-
fn run_coverage_run_test(&self) {
542-
let should_run = self.run_if_enabled();
543-
let proc_res = self.compile_test(should_run, Emit::None);
544-
545-
if !proc_res.status.success() {
546-
self.fatal_proc_rec("compilation failed!", &proc_res);
547-
}
548-
drop(proc_res);
549-
550-
if let WillExecute::Disabled = should_run {
551-
return;
552-
}
553-
554-
let profraw_path = self.output_base_dir().join("default.profraw");
555-
let profdata_path = self.output_base_dir().join("default.profdata");
556-
557-
// Delete any existing profraw/profdata files to rule out unintended
558-
// interference between repeated test runs.
559-
if profraw_path.exists() {
560-
std::fs::remove_file(&profraw_path).unwrap();
561-
}
562-
if profdata_path.exists() {
563-
std::fs::remove_file(&profdata_path).unwrap();
564-
}
565-
566-
let proc_res = self.exec_compiled_test_general(
567-
&[("LLVM_PROFILE_FILE", &profraw_path.to_str().unwrap())],
568-
false,
569-
);
570-
if self.props.failure_status.is_some() {
571-
self.check_correct_failure_status(&proc_res);
572-
} else if !proc_res.status.success() {
573-
self.fatal_proc_rec("test run failed!", &proc_res);
574-
}
575-
drop(proc_res);
576-
577-
let mut profraw_paths = vec![profraw_path];
578-
let mut bin_paths = vec![self.make_exe_name()];
579-
580-
if self.config.suite == "coverage-run-rustdoc" {
581-
self.run_doctests_for_coverage(&mut profraw_paths, &mut bin_paths);
582-
}
583-
584-
// Run `llvm-profdata merge` to index the raw coverage output.
585-
let proc_res = self.run_llvm_tool("llvm-profdata", |cmd| {
586-
cmd.args(["merge", "--sparse", "--output"]);
587-
cmd.arg(&profdata_path);
588-
cmd.args(&profraw_paths);
589-
});
590-
if !proc_res.status.success() {
591-
self.fatal_proc_rec("llvm-profdata merge failed!", &proc_res);
592-
}
593-
drop(proc_res);
594-
595-
// Run `llvm-cov show` to produce a coverage report in text format.
596-
let proc_res = self.run_llvm_tool("llvm-cov", |cmd| {
597-
cmd.args(["show", "--format=text", "--show-line-counts-or-regions"]);
598-
599-
cmd.arg("--Xdemangler");
600-
cmd.arg(self.config.rust_demangler_path.as_ref().unwrap());
601-
602-
cmd.arg("--instr-profile");
603-
cmd.arg(&profdata_path);
604-
605-
for bin in &bin_paths {
606-
cmd.arg("--object");
607-
cmd.arg(bin);
608-
}
609-
610-
cmd.args(&self.props.llvm_cov_flags);
611-
});
612-
if !proc_res.status.success() {
613-
self.fatal_proc_rec("llvm-cov show failed!", &proc_res);
614-
}
615-
616-
let kind = UI_COVERAGE;
617-
618-
let expected_coverage = self.load_expected_output(kind);
619-
let normalized_actual_coverage =
620-
self.normalize_coverage_output(&proc_res.stdout).unwrap_or_else(|err| {
621-
self.fatal_proc_rec(&err, &proc_res);
622-
});
623-
624-
let coverage_errors =
625-
self.compare_output(kind, &normalized_actual_coverage, &expected_coverage);
626-
627-
if coverage_errors > 0 {
628-
self.fatal_proc_rec(
629-
&format!("{} errors occurred comparing coverage output.", coverage_errors),
630-
&proc_res,
631-
);
632-
}
633-
}
634-
635-
/// Run any doctests embedded in this test file, and add any resulting
636-
/// `.profraw` files and doctest executables to the given vectors.
637-
fn run_doctests_for_coverage(
638-
&self,
639-
profraw_paths: &mut Vec<PathBuf>,
640-
bin_paths: &mut Vec<PathBuf>,
641-
) {
642-
// Put .profraw files and doctest executables in dedicated directories,
643-
// to make it easier to glob them all later.
644-
let profraws_dir = self.output_base_dir().join("doc_profraws");
645-
let bins_dir = self.output_base_dir().join("doc_bins");
646-
647-
// Remove existing directories to prevent cross-run interference.
648-
if profraws_dir.try_exists().unwrap() {
649-
std::fs::remove_dir_all(&profraws_dir).unwrap();
650-
}
651-
if bins_dir.try_exists().unwrap() {
652-
std::fs::remove_dir_all(&bins_dir).unwrap();
653-
}
654-
655-
let mut rustdoc_cmd =
656-
Command::new(self.config.rustdoc_path.as_ref().expect("--rustdoc-path not passed"));
657-
658-
// In general there will be multiple doctest binaries running, so we
659-
// tell the profiler runtime to write their coverage data into separate
660-
// profraw files.
661-
rustdoc_cmd.env("LLVM_PROFILE_FILE", profraws_dir.join("%p-%m.profraw"));
662-
663-
rustdoc_cmd.args(["--test", "-Cinstrument-coverage"]);
664-
665-
// Without this, the doctests complain about not being able to find
666-
// their enclosing file's crate for some reason.
667-
rustdoc_cmd.args(["--crate-name", "workaround_for_79771"]);
668-
669-
// Persist the doctest binaries so that `llvm-cov show` can read their
670-
// embedded coverage mappings later.
671-
rustdoc_cmd.arg("-Zunstable-options");
672-
rustdoc_cmd.arg("--persist-doctests");
673-
rustdoc_cmd.arg(&bins_dir);
674-
675-
rustdoc_cmd.arg("-L");
676-
rustdoc_cmd.arg(self.aux_output_dir_name());
677-
678-
rustdoc_cmd.arg(&self.testpaths.file);
679-
680-
let proc_res = self.compose_and_run_compiler(rustdoc_cmd, None);
681-
if !proc_res.status.success() {
682-
self.fatal_proc_rec("rustdoc --test failed!", &proc_res)
683-
}
684-
685-
fn glob_iter(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
686-
let path_str = path.as_ref().to_str().unwrap();
687-
let iter = glob(path_str).unwrap();
688-
iter.map(Result::unwrap)
689-
}
690-
691-
// Find all profraw files in the profraw directory.
692-
for p in glob_iter(profraws_dir.join("*.profraw")) {
693-
profraw_paths.push(p);
694-
}
695-
// Find all executables in the `--persist-doctests` directory, while
696-
// avoiding other file types (e.g. `.pdb` on Windows). This doesn't
697-
// need to be perfect, as long as it can handle the files actually
698-
// produced by `rustdoc --test`.
699-
for p in glob_iter(bins_dir.join("**/*")) {
700-
let is_bin = p.is_file()
701-
&& match p.extension() {
702-
None => true,
703-
Some(ext) => ext == OsStr::new("exe"),
704-
};
705-
if is_bin {
706-
bin_paths.push(p);
707-
}
708-
}
709-
}
710-
711-
fn run_llvm_tool(&self, name: &str, configure_cmd_fn: impl FnOnce(&mut Command)) -> ProcRes {
712-
let tool_path = self
713-
.config
714-
.llvm_bin_dir
715-
.as_ref()
716-
.expect("this test expects the LLVM bin dir to be available")
717-
.join(name);
718-
719-
let mut cmd = Command::new(tool_path);
720-
configure_cmd_fn(&mut cmd);
721-
722-
self.run_command_to_procres(&mut cmd)
723-
}
724-
725509
fn run_command_to_procres(&self, cmd: &mut Command) -> ProcRes {
726510
let output = cmd.output().unwrap_or_else(|e| panic!("failed to exec `{cmd:?}`: {e:?}"));
727511

@@ -737,143 +521,6 @@ impl<'test> TestCx<'test> {
737521
proc_res
738522
}
739523

740-
fn normalize_coverage_output(&self, coverage: &str) -> Result<String, String> {
741-
let normalized = self.normalize_output(coverage, &[]);
742-
let normalized = Self::anonymize_coverage_line_numbers(&normalized);
743-
744-
let mut lines = normalized.lines().collect::<Vec<_>>();
745-
746-
Self::sort_coverage_file_sections(&mut lines)?;
747-
Self::sort_coverage_subviews(&mut lines)?;
748-
749-
let joined_lines = lines.iter().flat_map(|line| [line, "\n"]).collect::<String>();
750-
Ok(joined_lines)
751-
}
752-
753-
/// Replace line numbers in coverage reports with the placeholder `LL`,
754-
/// so that the tests are less sensitive to lines being added/removed.
755-
fn anonymize_coverage_line_numbers(coverage: &str) -> String {
756-
// The coverage reporter prints line numbers at the start of a line.
757-
// They are truncated or left-padded to occupy exactly 5 columns.
758-
// (`LineNumberColumnWidth` in `SourceCoverageViewText.cpp`.)
759-
// A pipe character `|` appears immediately after the final digit.
760-
//
761-
// Line numbers that appear inside expansion/instantiation subviews
762-
// have an additional prefix of ` |` for each nesting level.
763-
//
764-
// Branch views also include the relevant line number, so we want to
765-
// redact those too. (These line numbers don't have padding.)
766-
//
767-
// Note: The pattern `(?m:^)` matches the start of a line.
768-
769-
// ` 1|` => ` LL|`
770-
// ` 10|` => ` LL|`
771-
// ` 100|` => ` LL|`
772-
// ` | 1000|` => ` | LL|`
773-
// ` | | 1000|` => ` | | LL|`
774-
let coverage = static_regex!(r"(?m:^)(?<prefix>(?: \|)*) *[0-9]+\|")
775-
.replace_all(&coverage, "${prefix} LL|");
776-
777-
// ` | Branch (1:` => ` | Branch (LL:`
778-
// ` | | Branch (10:` => ` | | Branch (LL:`
779-
let coverage = static_regex!(r"(?m:^)(?<prefix>(?: \|)+ Branch \()[0-9]+:")
780-
.replace_all(&coverage, "${prefix}LL:");
781-
782-
// ` |---> MC/DC Decision Region (1:30) to (2:` => ` |---> MC/DC Decision Region (LL:30) to (LL:`
783-
let coverage =
784-
static_regex!(r"(?m:^)(?<prefix>(?: \|)+---> MC/DC Decision Region \()[0-9]+:(?<middle>[0-9]+\) to \()[0-9]+:")
785-
.replace_all(&coverage, "${prefix}LL:${middle}LL:");
786-
787-
// ` | Condition C1 --> (1:` => ` | Condition C1 --> (LL:`
788-
let coverage =
789-
static_regex!(r"(?m:^)(?<prefix>(?: \|)+ Condition C[0-9]+ --> \()[0-9]+:")
790-
.replace_all(&coverage, "${prefix}LL:");
791-
792-
coverage.into_owned()
793-
}
794-
795-
/// Coverage reports can describe multiple source files, separated by
796-
/// blank lines. The order of these files is unpredictable (since it
797-
/// depends on implementation details), so we need to sort the file
798-
/// sections into a consistent order before comparing against a snapshot.
799-
fn sort_coverage_file_sections(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
800-
// Group the lines into file sections, separated by blank lines.
801-
let mut sections = coverage_lines.split(|line| line.is_empty()).collect::<Vec<_>>();
802-
803-
// The last section should be empty, representing an extra trailing blank line.
804-
if !sections.last().is_some_and(|last| last.is_empty()) {
805-
return Err("coverage report should end with an extra blank line".to_owned());
806-
}
807-
808-
// Sort the file sections (not including the final empty "section").
809-
let except_last = sections.len() - 1;
810-
(&mut sections[..except_last]).sort();
811-
812-
// Join the file sections back into a flat list of lines, with
813-
// sections separated by blank lines.
814-
let joined = sections.join(&[""] as &[_]);
815-
assert_eq!(joined.len(), coverage_lines.len());
816-
*coverage_lines = joined;
817-
818-
Ok(())
819-
}
820-
821-
fn sort_coverage_subviews(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
822-
let mut output_lines = Vec::new();
823-
824-
// We accumulate a list of zero or more "subviews", where each
825-
// subview is a list of one or more lines.
826-
let mut subviews: Vec<Vec<&str>> = Vec::new();
827-
828-
fn flush<'a>(subviews: &mut Vec<Vec<&'a str>>, output_lines: &mut Vec<&'a str>) {
829-
if subviews.is_empty() {
830-
return;
831-
}
832-
833-
// Take and clear the list of accumulated subviews.
834-
let mut subviews = std::mem::take(subviews);
835-
836-
// The last "subview" should be just a boundary line on its own,
837-
// so exclude it when sorting the other subviews.
838-
let except_last = subviews.len() - 1;
839-
(&mut subviews[..except_last]).sort();
840-
841-
for view in subviews {
842-
for line in view {
843-
output_lines.push(line);
844-
}
845-
}
846-
}
847-
848-
for (line, line_num) in coverage_lines.iter().zip(1..) {
849-
if line.starts_with(" ------------------") {
850-
// This is a subview boundary line, so start a new subview.
851-
subviews.push(vec![line]);
852-
} else if line.starts_with(" |") {
853-
// Add this line to the current subview.
854-
subviews
855-
.last_mut()
856-
.ok_or(format!(
857-
"unexpected subview line outside of a subview on line {line_num}"
858-
))?
859-
.push(line);
860-
} else {
861-
// This line is not part of a subview, so sort and print any
862-
// accumulated subviews, and then print the line as-is.
863-
flush(&mut subviews, &mut output_lines);
864-
output_lines.push(line);
865-
}
866-
}
867-
868-
flush(&mut subviews, &mut output_lines);
869-
assert!(subviews.is_empty());
870-
871-
assert_eq!(output_lines.len(), coverage_lines.len());
872-
*coverage_lines = output_lines;
873-
874-
Ok(())
875-
}
876-
877524
fn run_pretty_test(&self) {
878525
if self.props.pp_exact.is_some() {
879526
logv(self.config, "testing for exact pretty-printing".to_owned());
Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
//! Code specific to the coverage test suites.
2+
3+
use std::ffi::OsStr;
4+
use std::path::{Path, PathBuf};
5+
use std::process::Command;
6+
7+
use glob::glob;
8+
9+
use crate::common::{UI_COVERAGE, UI_COVERAGE_MAP};
10+
use crate::runtest::{static_regex, Emit, ProcRes, TestCx, WillExecute};
11+
12+
impl<'test> TestCx<'test> {
13+
pub(crate) fn run_coverage_map_test(&self) {
14+
let Some(coverage_dump_path) = &self.config.coverage_dump_path else {
15+
self.fatal("missing --coverage-dump");
16+
};
17+
18+
let (proc_res, llvm_ir_path) = self.compile_test_and_save_ir();
19+
if !proc_res.status.success() {
20+
self.fatal_proc_rec("compilation failed!", &proc_res);
21+
}
22+
drop(proc_res);
23+
24+
let mut dump_command = Command::new(coverage_dump_path);
25+
dump_command.arg(llvm_ir_path);
26+
let proc_res = self.run_command_to_procres(&mut dump_command);
27+
if !proc_res.status.success() {
28+
self.fatal_proc_rec("coverage-dump failed!", &proc_res);
29+
}
30+
31+
let kind = UI_COVERAGE_MAP;
32+
33+
let expected_coverage_dump = self.load_expected_output(kind);
34+
let actual_coverage_dump = self.normalize_output(&proc_res.stdout, &[]);
35+
36+
let coverage_dump_errors =
37+
self.compare_output(kind, &actual_coverage_dump, &expected_coverage_dump);
38+
39+
if coverage_dump_errors > 0 {
40+
self.fatal_proc_rec(
41+
&format!("{coverage_dump_errors} errors occurred comparing coverage output."),
42+
&proc_res,
43+
);
44+
}
45+
}
46+
47+
pub(crate) fn run_coverage_run_test(&self) {
48+
let should_run = self.run_if_enabled();
49+
let proc_res = self.compile_test(should_run, Emit::None);
50+
51+
if !proc_res.status.success() {
52+
self.fatal_proc_rec("compilation failed!", &proc_res);
53+
}
54+
drop(proc_res);
55+
56+
if let WillExecute::Disabled = should_run {
57+
return;
58+
}
59+
60+
let profraw_path = self.output_base_dir().join("default.profraw");
61+
let profdata_path = self.output_base_dir().join("default.profdata");
62+
63+
// Delete any existing profraw/profdata files to rule out unintended
64+
// interference between repeated test runs.
65+
if profraw_path.exists() {
66+
std::fs::remove_file(&profraw_path).unwrap();
67+
}
68+
if profdata_path.exists() {
69+
std::fs::remove_file(&profdata_path).unwrap();
70+
}
71+
72+
let proc_res = self.exec_compiled_test_general(
73+
&[("LLVM_PROFILE_FILE", &profraw_path.to_str().unwrap())],
74+
false,
75+
);
76+
if self.props.failure_status.is_some() {
77+
self.check_correct_failure_status(&proc_res);
78+
} else if !proc_res.status.success() {
79+
self.fatal_proc_rec("test run failed!", &proc_res);
80+
}
81+
drop(proc_res);
82+
83+
let mut profraw_paths = vec![profraw_path];
84+
let mut bin_paths = vec![self.make_exe_name()];
85+
86+
if self.config.suite == "coverage-run-rustdoc" {
87+
self.run_doctests_for_coverage(&mut profraw_paths, &mut bin_paths);
88+
}
89+
90+
// Run `llvm-profdata merge` to index the raw coverage output.
91+
let proc_res = self.run_llvm_tool("llvm-profdata", |cmd| {
92+
cmd.args(["merge", "--sparse", "--output"]);
93+
cmd.arg(&profdata_path);
94+
cmd.args(&profraw_paths);
95+
});
96+
if !proc_res.status.success() {
97+
self.fatal_proc_rec("llvm-profdata merge failed!", &proc_res);
98+
}
99+
drop(proc_res);
100+
101+
// Run `llvm-cov show` to produce a coverage report in text format.
102+
let proc_res = self.run_llvm_tool("llvm-cov", |cmd| {
103+
cmd.args(["show", "--format=text", "--show-line-counts-or-regions"]);
104+
105+
cmd.arg("--Xdemangler");
106+
cmd.arg(self.config.rust_demangler_path.as_ref().unwrap());
107+
108+
cmd.arg("--instr-profile");
109+
cmd.arg(&profdata_path);
110+
111+
for bin in &bin_paths {
112+
cmd.arg("--object");
113+
cmd.arg(bin);
114+
}
115+
116+
cmd.args(&self.props.llvm_cov_flags);
117+
});
118+
if !proc_res.status.success() {
119+
self.fatal_proc_rec("llvm-cov show failed!", &proc_res);
120+
}
121+
122+
let kind = UI_COVERAGE;
123+
124+
let expected_coverage = self.load_expected_output(kind);
125+
let normalized_actual_coverage =
126+
self.normalize_coverage_output(&proc_res.stdout).unwrap_or_else(|err| {
127+
self.fatal_proc_rec(&err, &proc_res);
128+
});
129+
130+
let coverage_errors =
131+
self.compare_output(kind, &normalized_actual_coverage, &expected_coverage);
132+
133+
if coverage_errors > 0 {
134+
self.fatal_proc_rec(
135+
&format!("{} errors occurred comparing coverage output.", coverage_errors),
136+
&proc_res,
137+
);
138+
}
139+
}
140+
141+
/// Run any doctests embedded in this test file, and add any resulting
142+
/// `.profraw` files and doctest executables to the given vectors.
143+
fn run_doctests_for_coverage(
144+
&self,
145+
profraw_paths: &mut Vec<PathBuf>,
146+
bin_paths: &mut Vec<PathBuf>,
147+
) {
148+
// Put .profraw files and doctest executables in dedicated directories,
149+
// to make it easier to glob them all later.
150+
let profraws_dir = self.output_base_dir().join("doc_profraws");
151+
let bins_dir = self.output_base_dir().join("doc_bins");
152+
153+
// Remove existing directories to prevent cross-run interference.
154+
if profraws_dir.try_exists().unwrap() {
155+
std::fs::remove_dir_all(&profraws_dir).unwrap();
156+
}
157+
if bins_dir.try_exists().unwrap() {
158+
std::fs::remove_dir_all(&bins_dir).unwrap();
159+
}
160+
161+
let mut rustdoc_cmd =
162+
Command::new(self.config.rustdoc_path.as_ref().expect("--rustdoc-path not passed"));
163+
164+
// In general there will be multiple doctest binaries running, so we
165+
// tell the profiler runtime to write their coverage data into separate
166+
// profraw files.
167+
rustdoc_cmd.env("LLVM_PROFILE_FILE", profraws_dir.join("%p-%m.profraw"));
168+
169+
rustdoc_cmd.args(["--test", "-Cinstrument-coverage"]);
170+
171+
// Without this, the doctests complain about not being able to find
172+
// their enclosing file's crate for some reason.
173+
rustdoc_cmd.args(["--crate-name", "workaround_for_79771"]);
174+
175+
// Persist the doctest binaries so that `llvm-cov show` can read their
176+
// embedded coverage mappings later.
177+
rustdoc_cmd.arg("-Zunstable-options");
178+
rustdoc_cmd.arg("--persist-doctests");
179+
rustdoc_cmd.arg(&bins_dir);
180+
181+
rustdoc_cmd.arg("-L");
182+
rustdoc_cmd.arg(self.aux_output_dir_name());
183+
184+
rustdoc_cmd.arg(&self.testpaths.file);
185+
186+
let proc_res = self.compose_and_run_compiler(rustdoc_cmd, None);
187+
if !proc_res.status.success() {
188+
self.fatal_proc_rec("rustdoc --test failed!", &proc_res)
189+
}
190+
191+
fn glob_iter(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
192+
let path_str = path.as_ref().to_str().unwrap();
193+
let iter = glob(path_str).unwrap();
194+
iter.map(Result::unwrap)
195+
}
196+
197+
// Find all profraw files in the profraw directory.
198+
for p in glob_iter(profraws_dir.join("*.profraw")) {
199+
profraw_paths.push(p);
200+
}
201+
// Find all executables in the `--persist-doctests` directory, while
202+
// avoiding other file types (e.g. `.pdb` on Windows). This doesn't
203+
// need to be perfect, as long as it can handle the files actually
204+
// produced by `rustdoc --test`.
205+
for p in glob_iter(bins_dir.join("**/*")) {
206+
let is_bin = p.is_file()
207+
&& match p.extension() {
208+
None => true,
209+
Some(ext) => ext == OsStr::new("exe"),
210+
};
211+
if is_bin {
212+
bin_paths.push(p);
213+
}
214+
}
215+
}
216+
217+
fn run_llvm_tool(&self, name: &str, configure_cmd_fn: impl FnOnce(&mut Command)) -> ProcRes {
218+
let tool_path = self
219+
.config
220+
.llvm_bin_dir
221+
.as_ref()
222+
.expect("this test expects the LLVM bin dir to be available")
223+
.join(name);
224+
225+
let mut cmd = Command::new(tool_path);
226+
configure_cmd_fn(&mut cmd);
227+
228+
self.run_command_to_procres(&mut cmd)
229+
}
230+
231+
fn normalize_coverage_output(&self, coverage: &str) -> Result<String, String> {
232+
let normalized = self.normalize_output(coverage, &[]);
233+
let normalized = Self::anonymize_coverage_line_numbers(&normalized);
234+
235+
let mut lines = normalized.lines().collect::<Vec<_>>();
236+
237+
Self::sort_coverage_file_sections(&mut lines)?;
238+
Self::sort_coverage_subviews(&mut lines)?;
239+
240+
let joined_lines = lines.iter().flat_map(|line| [line, "\n"]).collect::<String>();
241+
Ok(joined_lines)
242+
}
243+
244+
/// Replace line numbers in coverage reports with the placeholder `LL`,
245+
/// so that the tests are less sensitive to lines being added/removed.
246+
fn anonymize_coverage_line_numbers(coverage: &str) -> String {
247+
// The coverage reporter prints line numbers at the start of a line.
248+
// They are truncated or left-padded to occupy exactly 5 columns.
249+
// (`LineNumberColumnWidth` in `SourceCoverageViewText.cpp`.)
250+
// A pipe character `|` appears immediately after the final digit.
251+
//
252+
// Line numbers that appear inside expansion/instantiation subviews
253+
// have an additional prefix of ` |` for each nesting level.
254+
//
255+
// Branch views also include the relevant line number, so we want to
256+
// redact those too. (These line numbers don't have padding.)
257+
//
258+
// Note: The pattern `(?m:^)` matches the start of a line.
259+
260+
// ` 1|` => ` LL|`
261+
// ` 10|` => ` LL|`
262+
// ` 100|` => ` LL|`
263+
// ` | 1000|` => ` | LL|`
264+
// ` | | 1000|` => ` | | LL|`
265+
let coverage = static_regex!(r"(?m:^)(?<prefix>(?: \|)*) *[0-9]+\|")
266+
.replace_all(&coverage, "${prefix} LL|");
267+
268+
// ` | Branch (1:` => ` | Branch (LL:`
269+
// ` | | Branch (10:` => ` | | Branch (LL:`
270+
let coverage = static_regex!(r"(?m:^)(?<prefix>(?: \|)+ Branch \()[0-9]+:")
271+
.replace_all(&coverage, "${prefix}LL:");
272+
273+
// ` |---> MC/DC Decision Region (1:30) to (2:` => ` |---> MC/DC Decision Region (LL:30) to (LL:`
274+
let coverage =
275+
static_regex!(r"(?m:^)(?<prefix>(?: \|)+---> MC/DC Decision Region \()[0-9]+:(?<middle>[0-9]+\) to \()[0-9]+:")
276+
.replace_all(&coverage, "${prefix}LL:${middle}LL:");
277+
278+
// ` | Condition C1 --> (1:` => ` | Condition C1 --> (LL:`
279+
let coverage =
280+
static_regex!(r"(?m:^)(?<prefix>(?: \|)+ Condition C[0-9]+ --> \()[0-9]+:")
281+
.replace_all(&coverage, "${prefix}LL:");
282+
283+
coverage.into_owned()
284+
}
285+
286+
/// Coverage reports can describe multiple source files, separated by
287+
/// blank lines. The order of these files is unpredictable (since it
288+
/// depends on implementation details), so we need to sort the file
289+
/// sections into a consistent order before comparing against a snapshot.
290+
fn sort_coverage_file_sections(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
291+
// Group the lines into file sections, separated by blank lines.
292+
let mut sections = coverage_lines.split(|line| line.is_empty()).collect::<Vec<_>>();
293+
294+
// The last section should be empty, representing an extra trailing blank line.
295+
if !sections.last().is_some_and(|last| last.is_empty()) {
296+
return Err("coverage report should end with an extra blank line".to_owned());
297+
}
298+
299+
// Sort the file sections (not including the final empty "section").
300+
let except_last = sections.len() - 1;
301+
(&mut sections[..except_last]).sort();
302+
303+
// Join the file sections back into a flat list of lines, with
304+
// sections separated by blank lines.
305+
let joined = sections.join(&[""] as &[_]);
306+
assert_eq!(joined.len(), coverage_lines.len());
307+
*coverage_lines = joined;
308+
309+
Ok(())
310+
}
311+
312+
fn sort_coverage_subviews(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
313+
let mut output_lines = Vec::new();
314+
315+
// We accumulate a list of zero or more "subviews", where each
316+
// subview is a list of one or more lines.
317+
let mut subviews: Vec<Vec<&str>> = Vec::new();
318+
319+
fn flush<'a>(subviews: &mut Vec<Vec<&'a str>>, output_lines: &mut Vec<&'a str>) {
320+
if subviews.is_empty() {
321+
return;
322+
}
323+
324+
// Take and clear the list of accumulated subviews.
325+
let mut subviews = std::mem::take(subviews);
326+
327+
// The last "subview" should be just a boundary line on its own,
328+
// so exclude it when sorting the other subviews.
329+
let except_last = subviews.len() - 1;
330+
(&mut subviews[..except_last]).sort();
331+
332+
for view in subviews {
333+
for line in view {
334+
output_lines.push(line);
335+
}
336+
}
337+
}
338+
339+
for (line, line_num) in coverage_lines.iter().zip(1..) {
340+
if line.starts_with(" ------------------") {
341+
// This is a subview boundary line, so start a new subview.
342+
subviews.push(vec![line]);
343+
} else if line.starts_with(" |") {
344+
// Add this line to the current subview.
345+
subviews
346+
.last_mut()
347+
.ok_or(format!(
348+
"unexpected subview line outside of a subview on line {line_num}"
349+
))?
350+
.push(line);
351+
} else {
352+
// This line is not part of a subview, so sort and print any
353+
// accumulated subviews, and then print the line as-is.
354+
flush(&mut subviews, &mut output_lines);
355+
output_lines.push(line);
356+
}
357+
}
358+
359+
flush(&mut subviews, &mut output_lines);
360+
assert!(subviews.is_empty());
361+
362+
assert_eq!(output_lines.len(), coverage_lines.len());
363+
*coverage_lines = output_lines;
364+
365+
Ok(())
366+
}
367+
}

‎src/tools/compiletest/src/runtest/tests.rs

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -48,71 +48,3 @@ fn normalize_platform_differences() {
4848
r#"println!("test\ntest")"#,
4949
);
5050
}
51-
52-
/// Test for anonymizing line numbers in coverage reports, especially for
53-
/// MC/DC regions.
54-
///
55-
/// FIXME(#123409): This test can be removed when we have examples of MC/DC
56-
/// coverage in the actual coverage test suite.
57-
#[test]
58-
fn anonymize_coverage_line_numbers() {
59-
let anon = |coverage| TestCx::anonymize_coverage_line_numbers(coverage);
60-
61-
let input = r#"
62-
7| 2|fn mcdc_check_neither(a: bool, b: bool) {
63-
8| 2| if a && b {
64-
^0
65-
------------------
66-
|---> MC/DC Decision Region (8:8) to (8:14)
67-
|
68-
| Number of Conditions: 2
69-
| Condition C1 --> (8:8)
70-
| Condition C2 --> (8:13)
71-
|
72-
| Executed MC/DC Test Vectors:
73-
|
74-
| C1, C2 Result
75-
| 1 { F, - = F }
76-
|
77-
| C1-Pair: not covered
78-
| C2-Pair: not covered
79-
| MC/DC Coverage for Decision: 0.00%
80-
|
81-
------------------
82-
9| 0| say("a and b");
83-
10| 2| } else {
84-
11| 2| say("not both");
85-
12| 2| }
86-
13| 2|}
87-
"#;
88-
89-
let expected = r#"
90-
LL| 2|fn mcdc_check_neither(a: bool, b: bool) {
91-
LL| 2| if a && b {
92-
^0
93-
------------------
94-
|---> MC/DC Decision Region (LL:8) to (LL:14)
95-
|
96-
| Number of Conditions: 2
97-
| Condition C1 --> (LL:8)
98-
| Condition C2 --> (LL:13)
99-
|
100-
| Executed MC/DC Test Vectors:
101-
|
102-
| C1, C2 Result
103-
| 1 { F, - = F }
104-
|
105-
| C1-Pair: not covered
106-
| C2-Pair: not covered
107-
| MC/DC Coverage for Decision: 0.00%
108-
|
109-
------------------
110-
LL| 0| say("a and b");
111-
LL| 2| } else {
112-
LL| 2| say("not both");
113-
LL| 2| }
114-
LL| 2|}
115-
"#;
116-
117-
assert_eq!(anon(input), expected);
118-
}

0 commit comments

Comments
 (0)
This repository has been archived.