Skip to content

Commit 534e6af

Browse files
committed
add coverage-tool crate that uses compiletest as a library
Adds a coverage-tool binary that depends on compiletest and uses collect_and_make_tests/parse_config to enumerate UI tests, then runs each through an instrumented rustc to collect LLVM profraw files and eagerly merges them into a combined profdata.
1 parent b750a8b commit 534e6af

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

src/tools/coverage-tool/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "coverage-tool"
3+
version = "0.0.0"
4+
edition = "2021"
5+
6+
[[bin]]
7+
name = "coverage-tool"
8+
path = "src/main.rs"
9+
10+
[dependencies]
11+
compiletest = { path = "../compiletest" }
12+
camino = "1"
13+
glob = "0.3"
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// coverage-tool: collects compiler coverage by running UI tests through an
2+
// instrumented stage1 rustc and merging the resulting profraw files eagerly.
3+
//
4+
// Usage:
5+
// coverage-tool --config <compiletest-args> [--suite tests/ui/generics] [--out coverage/]
6+
7+
use std::path::PathBuf;
8+
use std::process::Command;
9+
use std::sync::Arc;
10+
use std::{env, fs};
11+
12+
use compiletest::{collect_and_make_tests, parse_config};
13+
use compiletest::common::Config;
14+
15+
fn main() {
16+
let args: Vec<String> = env::args().collect();
17+
18+
// Minimal CLI: --suite <dir> --out <dir>
19+
// Everything else is passed through to compiletest's config parser.
20+
let suite = arg_value(&args, "--suite")
21+
.unwrap_or_else(|| "tests/ui".to_string());
22+
let out_dir = arg_value(&args, "--out")
23+
.unwrap_or_else(|| "coverage".to_string());
24+
25+
fs::create_dir_all(&out_dir).expect("failed to create output dir");
26+
27+
// Build a minimal compiletest Config pointing at the test suite.
28+
// In practice this would be constructed from bootstrap's config,
29+
// but for now we parse the same flags compiletest accepts.
30+
let config = build_config(&suite);
31+
let config = Arc::new(config);
32+
33+
// Use compiletest to collect the full test list with all directives resolved.
34+
let tests = collect_and_make_tests(Arc::clone(&config));
35+
36+
let total = tests.len();
37+
let mut merged = 0usize;
38+
let mut skipped = 0usize;
39+
40+
let profdata_path = PathBuf::from(&out_dir).join("combined.profdata");
41+
let tmpdir = tempdir();
42+
43+
for (i, test) in tests.iter().enumerate() {
44+
if test.desc.ignore {
45+
skipped += 1;
46+
continue;
47+
}
48+
49+
let test_file = test.testpaths.file.as_str();
50+
eprint!("[{}/{}] {} ", i + 1, total, test_file);
51+
52+
let profile_file = format!("{}/test_%p.profraw", tmpdir.display());
53+
54+
// Compile the test with LLVM_PROFILE_FILE set so the instrumented
55+
// rustc emits a profraw file.
56+
let _result = Command::new(config.rustc_path.as_str())
57+
.arg("--sysroot")
58+
.arg(config.sysroot_base.as_str())
59+
.arg(test_file)
60+
.arg("--edition")
61+
.arg(test.revision.as_deref().unwrap_or("2015"))
62+
.args(&["-o", "/dev/null", "--crate-type", "bin"])
63+
.env("LLVM_PROFILE_FILE", &profile_file)
64+
.output();
65+
66+
// Collect any profraw files written
67+
let profraws: Vec<PathBuf> = fs::read_dir(&tmpdir)
68+
.unwrap()
69+
.filter_map(|e| e.ok())
70+
.map(|e| e.path())
71+
.filter(|p| p.extension().map(|x| x == "profraw").unwrap_or(false))
72+
.collect();
73+
74+
if profraws.is_empty() {
75+
skipped += 1;
76+
eprintln!("skip");
77+
continue;
78+
}
79+
80+
// Eager merge into running profdata, then delete profraws
81+
merge_profraws(&profraws, &profdata_path);
82+
for f in &profraws {
83+
let _ = fs::remove_file(f);
84+
}
85+
86+
merged += 1;
87+
eprintln!("ok");
88+
}
89+
90+
eprintln!("\nDone. {merged} merged, {skipped} skipped.");
91+
eprintln!("Profdata: {}", profdata_path.display());
92+
}
93+
94+
fn merge_profraws(profraws: &[PathBuf], profdata: &PathBuf) {
95+
let llvm_profdata = find_llvm_profdata();
96+
let mut cmd = Command::new(&llvm_profdata);
97+
cmd.arg("merge").arg("--sparse").arg("-o").arg(profdata);
98+
if profdata.exists() {
99+
cmd.arg(profdata);
100+
}
101+
for p in profraws {
102+
cmd.arg(p);
103+
}
104+
let _ = cmd.output();
105+
}
106+
107+
fn find_llvm_profdata() -> String {
108+
// Look in the same place bootstrap puts it
109+
glob::glob("build/*/ci-llvm/bin/llvm-profdata")
110+
.unwrap()
111+
.filter_map(|p| p.ok())
112+
.next()
113+
.map(|p| p.display().to_string())
114+
.unwrap_or_else(|| "llvm-profdata".to_string())
115+
}
116+
117+
fn tempdir() -> PathBuf {
118+
let dir = std::env::temp_dir().join("cov_profraws");
119+
fs::create_dir_all(&dir).unwrap();
120+
dir
121+
}
122+
123+
fn arg_value(args: &[String], flag: &str) -> Option<String> {
124+
args.windows(2)
125+
.find(|w| w[0] == flag)
126+
.map(|w| w[1].clone())
127+
}
128+
129+
fn build_config(suite: &str) -> Config {
130+
// Construct a minimal Config from environment/args.
131+
// This is a stub — in practice bootstrap would pass the full config.
132+
parse_config(vec![
133+
"coverage-tool".to_string(),
134+
"--mode".to_string(), "ui".to_string(),
135+
"--suite-path".to_string(), suite.to_string(),
136+
"--rustc-path".to_string(), find_stage1_rustc(),
137+
])
138+
}
139+
140+
fn find_stage1_rustc() -> String {
141+
glob::glob("build/*/stage1/bin/rustc")
142+
.unwrap()
143+
.filter_map(|p| p.ok())
144+
.next()
145+
.map(|p| p.display().to_string())
146+
.unwrap_or_else(|| "rustc".to_string())
147+
}

0 commit comments

Comments
 (0)