Skip to content

Commit 22e119b

Browse files
committed
Add a custom run-coverage mode to compiletest
1 parent a42bbd0 commit 22e119b

File tree

3 files changed

+232
-3
lines changed

3 files changed

+232
-3
lines changed

src/tools/compiletest/src/common.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ string_enum! {
6666
JsDocTest => "js-doc-test",
6767
MirOpt => "mir-opt",
6868
Assembly => "assembly",
69+
RunCoverage => "run-coverage",
6970
}
7071
}
7172

@@ -626,6 +627,7 @@ pub const UI_EXTENSIONS: &[&str] = &[
626627
UI_STDERR_64,
627628
UI_STDERR_32,
628629
UI_STDERR_16,
630+
UI_COVERAGE,
629631
];
630632
pub const UI_STDERR: &str = "stderr";
631633
pub const UI_STDOUT: &str = "stdout";
@@ -635,6 +637,7 @@ pub const UI_RUN_STDOUT: &str = "run.stdout";
635637
pub const UI_STDERR_64: &str = "64bit.stderr";
636638
pub const UI_STDERR_32: &str = "32bit.stderr";
637639
pub const UI_STDERR_16: &str = "16bit.stderr";
640+
pub const UI_COVERAGE: &str = "coverage";
638641

639642
/// Absolute path to the directory where all output for all tests in the given
640643
/// `relative_dir` group should reside. Example:

src/tools/compiletest/src/header.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,10 +612,25 @@ pub fn line_directive<'line>(
612612
}
613613

614614
fn iter_header<R: Read>(testfile: &Path, rdr: R, it: &mut dyn FnMut(Option<&str>, &str, usize)) {
615+
iter_header_extra(testfile, rdr, &[], it)
616+
}
617+
618+
fn iter_header_extra(
619+
testfile: &Path,
620+
rdr: impl Read,
621+
extra_directives: &[&str],
622+
it: &mut dyn FnMut(Option<&str>, &str, usize),
623+
) {
615624
if testfile.is_dir() {
616625
return;
617626
}
618627

628+
// Process any extra directives supplied by the caller (e.g. because they
629+
// are implied by the test mode), with a dummy line number of 0.
630+
for directive in extra_directives {
631+
it(None, directive, 0);
632+
}
633+
619634
let comment = if testfile.extension().map(|e| e == "rs") == Some(true) { "//" } else { "#" };
620635

621636
let mut rdr = BufReader::new(rdr);
@@ -894,7 +909,27 @@ pub fn make_test_description<R: Read>(
894909
let mut ignore_message = None;
895910
let mut should_fail = false;
896911

897-
iter_header(path, src, &mut |revision, ln, line_number| {
912+
let extra_directives: &[&str] = match config.mode {
913+
// The run-coverage tests are treated as having these extra directives,
914+
// without needing to specify them manually in every test file.
915+
// (Some of the comments below have been copied over from
916+
// `tests/run-make/coverage-reports/Makefile`.)
917+
Mode::RunCoverage => {
918+
&[
919+
"needs-profiler-support",
920+
// FIXME(mati865): MinGW GCC miscompiles compiler-rt profiling library but with Clang it works
921+
// properly. Since we only have GCC on the CI ignore the test for now.
922+
"ignore-windows-gnu",
923+
// FIXME(pietroalbini): this test currently does not work on cross-compiled
924+
// targets because remote-test is not capable of sending back the *.profraw
925+
// files generated by the LLVM instrumentation.
926+
"ignore-cross-compile",
927+
]
928+
}
929+
_ => &[],
930+
};
931+
932+
iter_header_extra(path, src, extra_directives, &mut |revision, ln, line_number| {
898933
if revision.is_some() && revision != cfg {
899934
return;
900935
}

src/tools/compiletest/src/runtest.rs

Lines changed: 193 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use crate::common::{Assembly, Incremental, JsDocTest, MirOpt, RunMake, RustdocJs
66
use crate::common::{Codegen, CodegenUnits, DebugInfo, Debugger, Rustdoc};
77
use crate::common::{CompareMode, FailMode, PassMode};
88
use crate::common::{Config, TestPaths};
9-
use crate::common::{Pretty, RunPassValgrind};
10-
use crate::common::{UI_RUN_STDERR, UI_RUN_STDOUT};
9+
use crate::common::{Pretty, RunCoverage, RunPassValgrind};
10+
use crate::common::{UI_COVERAGE, UI_RUN_STDERR, UI_RUN_STDOUT};
1111
use crate::compute_diff::{write_diff, write_filtered_diff};
1212
use crate::errors::{self, Error, ErrorKind};
1313
use crate::header::TestProps;
@@ -253,6 +253,7 @@ impl<'test> TestCx<'test> {
253253
MirOpt => self.run_mir_opt_test(),
254254
Assembly => self.run_assembly_test(),
255255
JsDocTest => self.run_js_doc_test(),
256+
RunCoverage => self.run_coverage_test(),
256257
}
257258
}
258259

@@ -465,6 +466,184 @@ impl<'test> TestCx<'test> {
465466
}
466467
}
467468

469+
fn run_coverage_test(&self) {
470+
let should_run = self.run_if_enabled();
471+
let proc_res = self.compile_test(should_run, Emit::None);
472+
473+
if !proc_res.status.success() {
474+
self.fatal_proc_rec("compilation failed!", &proc_res);
475+
}
476+
drop(proc_res);
477+
478+
if let WillExecute::Disabled = should_run {
479+
return;
480+
}
481+
482+
let profraw_path = self.output_base_dir().join("default.profraw");
483+
let profdata_path = self.output_base_dir().join("default.profdata");
484+
485+
// Delete any existing profraw/profdata files to rule out unintended
486+
// interference between repeated test runs.
487+
if profraw_path.exists() {
488+
std::fs::remove_file(&profraw_path).unwrap();
489+
}
490+
if profdata_path.exists() {
491+
std::fs::remove_file(&profdata_path).unwrap();
492+
}
493+
494+
let proc_res = self.exec_compiled_test_general(
495+
&[("LLVM_PROFILE_FILE", &profraw_path.to_str().unwrap())],
496+
false,
497+
);
498+
if self.props.failure_status.is_some() {
499+
self.check_correct_failure_status(&proc_res);
500+
} else if !proc_res.status.success() {
501+
self.fatal_proc_rec("test run failed!", &proc_res);
502+
}
503+
drop(proc_res);
504+
505+
// Run `llvm-profdata merge` to index the raw coverage output.
506+
let proc_res = self.run_llvm_tool("llvm-profdata", |cmd| {
507+
cmd.args(["merge", "--sparse", "--output"]);
508+
cmd.arg(&profdata_path);
509+
cmd.arg(&profraw_path);
510+
});
511+
if !proc_res.status.success() {
512+
self.fatal_proc_rec("llvm-profdata merge failed!", &proc_res);
513+
}
514+
drop(proc_res);
515+
516+
// Run `llvm-cov show` to produce a coverage report in text format.
517+
let proc_res = self.run_llvm_tool("llvm-cov", |cmd| {
518+
cmd.args(["show", "--format=text", "--show-line-counts-or-regions"]);
519+
520+
cmd.arg("--Xdemangler");
521+
cmd.arg(self.config.rust_demangler_path.as_ref().unwrap());
522+
523+
cmd.arg("--instr-profile");
524+
cmd.arg(&profdata_path);
525+
526+
cmd.arg("--object");
527+
cmd.arg(&self.make_exe_name());
528+
});
529+
if !proc_res.status.success() {
530+
self.fatal_proc_rec("llvm-cov show failed!", &proc_res);
531+
}
532+
533+
let kind = UI_COVERAGE;
534+
535+
let expected_coverage = self.load_expected_output(kind);
536+
let normalized_actual_coverage =
537+
self.normalize_coverage_output(&proc_res.stdout).unwrap_or_else(|err| {
538+
self.fatal_proc_rec(&err, &proc_res);
539+
});
540+
541+
let coverage_errors = self.compare_output(
542+
kind,
543+
&normalized_actual_coverage,
544+
&expected_coverage,
545+
self.props.compare_output_lines_by_subset,
546+
);
547+
548+
if coverage_errors > 0 {
549+
self.fatal_proc_rec(
550+
&format!("{} errors occurred comparing coverage output.", coverage_errors),
551+
&proc_res,
552+
);
553+
}
554+
}
555+
556+
fn run_llvm_tool(&self, name: &str, configure_cmd_fn: impl FnOnce(&mut Command)) -> ProcRes {
557+
let tool_path = self
558+
.config
559+
.llvm_bin_dir
560+
.as_ref()
561+
.expect("this test expects the LLVM bin dir to be available")
562+
.join(name);
563+
564+
let mut cmd = Command::new(tool_path);
565+
configure_cmd_fn(&mut cmd);
566+
567+
let output = cmd.output().unwrap_or_else(|_| panic!("failed to exec `{cmd:?}`"));
568+
569+
let proc_res = ProcRes {
570+
status: output.status,
571+
stdout: String::from_utf8(output.stdout).unwrap(),
572+
stderr: String::from_utf8(output.stderr).unwrap(),
573+
cmdline: format!("{cmd:?}"),
574+
};
575+
self.dump_output(&proc_res.stdout, &proc_res.stderr);
576+
577+
proc_res
578+
}
579+
580+
fn normalize_coverage_output(&self, coverage: &str) -> Result<String, String> {
581+
let normalized = self.normalize_output(coverage, &[]);
582+
583+
let mut lines = normalized.lines().collect::<Vec<_>>();
584+
585+
Self::sort_coverage_subviews(&mut lines)?;
586+
587+
let joined_lines = lines.iter().flat_map(|line| [line, "\n"]).collect::<String>();
588+
Ok(joined_lines)
589+
}
590+
591+
fn sort_coverage_subviews(coverage_lines: &mut Vec<&str>) -> Result<(), String> {
592+
let mut output_lines = Vec::new();
593+
594+
// We accumulate a list of zero or more "subviews", where each
595+
// subview is a list of one or more lines.
596+
let mut subviews: Vec<Vec<&str>> = Vec::new();
597+
598+
fn flush<'a>(subviews: &mut Vec<Vec<&'a str>>, output_lines: &mut Vec<&'a str>) {
599+
if subviews.is_empty() {
600+
return;
601+
}
602+
603+
// Take and clear the list of accumulated subviews.
604+
let mut subviews = std::mem::take(subviews);
605+
606+
// The last "subview" should be just a boundary line on its own,
607+
// so exclude it when sorting the other subviews.
608+
let except_last = subviews.len() - 1;
609+
(&mut subviews[..except_last]).sort();
610+
611+
for view in subviews {
612+
for line in view {
613+
output_lines.push(line);
614+
}
615+
}
616+
}
617+
618+
for (line, line_num) in coverage_lines.iter().zip(1..) {
619+
if line.starts_with(" ------------------") {
620+
// This is a subview boundary line, so start a new subview.
621+
subviews.push(vec![line]);
622+
} else if line.starts_with(" |") {
623+
// Add this line to the current subview.
624+
subviews
625+
.last_mut()
626+
.ok_or(format!(
627+
"unexpected subview line outside of a subview on line {line_num}"
628+
))?
629+
.push(line);
630+
} else {
631+
// This line is not part of a subview, so sort and print any
632+
// accumulated subviews, and then print the line as-is.
633+
flush(&mut subviews, &mut output_lines);
634+
output_lines.push(line);
635+
}
636+
}
637+
638+
flush(&mut subviews, &mut output_lines);
639+
assert!(subviews.is_empty());
640+
641+
assert_eq!(output_lines.len(), coverage_lines.len());
642+
*coverage_lines = output_lines;
643+
644+
Ok(())
645+
}
646+
468647
fn run_pretty_test(&self) {
469648
if self.props.pp_exact.is_some() {
470649
logv(self.config, "testing for exact pretty-printing".to_owned());
@@ -1822,6 +2001,7 @@ impl<'test> TestCx<'test> {
18222001
|| self.is_vxworks_pure_static()
18232002
|| self.config.target.contains("bpf")
18242003
|| !self.config.target_cfg().dynamic_linking
2004+
|| self.config.mode == RunCoverage
18252005
{
18262006
// We primarily compile all auxiliary libraries as dynamic libraries
18272007
// to avoid code size bloat and large binaries as much as possible
@@ -1832,6 +2012,10 @@ impl<'test> TestCx<'test> {
18322012
// dynamic libraries so we just go back to building a normal library. Note,
18332013
// however, that for MUSL if the library is built with `force_host` then
18342014
// it's ok to be a dylib as the host should always support dylibs.
2015+
//
2016+
// Coverage tests want static linking by default so that coverage
2017+
// mappings in auxiliary libraries can be merged into the final
2018+
// executable.
18352019
(false, Some("lib"))
18362020
} else {
18372021
(true, Some("dylib"))
@@ -2009,6 +2193,10 @@ impl<'test> TestCx<'test> {
20092193
}
20102194
}
20112195
DebugInfo => { /* debuginfo tests must be unoptimized */ }
2196+
RunCoverage => {
2197+
// Coverage reports are affected by optimization level, and
2198+
// the current snapshots assume no optimization by default.
2199+
}
20122200
_ => {
20132201
rustc.arg("-O");
20142202
}
@@ -2075,6 +2263,9 @@ impl<'test> TestCx<'test> {
20752263

20762264
rustc.arg(dir_opt);
20772265
}
2266+
RunCoverage => {
2267+
rustc.arg("-Cinstrument-coverage");
2268+
}
20782269
RunPassValgrind | Pretty | DebugInfo | Codegen | Rustdoc | RustdocJson | RunMake
20792270
| CodegenUnits | JsDocTest | Assembly => {
20802271
// do not use JSON output

0 commit comments

Comments
 (0)