Skip to content

Commit 3011a89

Browse files
committed
Some miscellaneous fixups
1 parent 6d88fd9 commit 3011a89

5 files changed

Lines changed: 141 additions & 93 deletions

File tree

llvm/utils/patch-coverage/README.md

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
# Patch-Based Code Coverage (LLVM Git Integration)
22

33
This tool provides patch-level code coverage for LLVM development workflows.
4-
It helps developers and reviewers understand which lines introduced by a patch are covered by tests and which are not.
5-
6-
It integrates with LLVM’s infrastructure (`llvm-lit`, coverage instrumentation, and CI) and works both locally and in CI environments.
7-
4+
It helps developers and reviewers understand which lines introduced by a patch
5+
are covered by tests and which are not.
86

97
## Overview
108

@@ -21,23 +19,14 @@ Given recent commits, the tool:
2119
* Covered lines introduced by the patch
2220
* Uncovered lines introduced by the patch
2321

24-
25-
## Key Features
26-
27-
* Patch-aware coverage (only modified lines)
28-
* Selective test execution (only affected tests)
29-
* Supports:
30-
31-
* `llvm-lit` tests
32-
* LLVM unit tests
33-
* Uses allowlist-based instrumentation for faster builds
34-
* Avoids unnecessary rebuilds via patch hashing
35-
36-
3722
## Setup
3823

3924
### 1. Build LLVM (standard workflow)
4025

26+
If you have an existing LLVM build, point to it with `-b`. If not, skip this step —
27+
the tool will automatically configure and build LLVM in the specified build directory
28+
with a minimal configuration when first run.
29+
4130
```bash
4231
git clone https://github.com/llvm/llvm-project.git
4332
cd llvm-project
@@ -104,21 +93,69 @@ git patch-coverage -b build -n 3 opt llvm/test
10493
| ------------------------------ | ----------------------------------------------------------- |
10594
| `-n, --num-commits` | Number of recent commits to analyze (default: 1) |
10695
| `--projects` | LLVM projects to enable (e.g. clang;mlir (default: None) |
107-
| `-i, --instrumented-build-dir` | Use pre-built instrumented directory (Deafult: `build_inst`)|
96+
| `-i, --instrumented-build-dir` | Use pre-built instrumented directory (Dafault: `build_inst`)|
10897
| `-b, --build-dir` | Path to LLVM build directory (default: `build`) |
10998
| `<test-path>` | One or more test suite paths (default: `build/test`) |
11099

111100

112101

113102
## How It Works
114-
115-
103+
### 1. Patch Extraction
104+
The tool runs `git diff HEAD~N HEAD` to generate a patch from the last N commits.
105+
It parses the diff to extract:
106+
- **Source files** — modified `.cpp`, `.c`, or `.mm` files outside test directories
107+
- **Test files** — modified lit tests and unit tests within the patch
108+
- **Modified lines** — the exact line numbers added or changed in each source file
109+
110+
### 2. Allowlist Generation
111+
To avoid instrumenting the entire codebase, the tool generates an allowlist
112+
containing only the source files present in the patch:
113+
source:/path/to/modified/file.cpp=allow
114+
default:skip
115+
116+
This is passed to the compiler via `-fprofile-list`, so profiling overhead is
117+
scoped only to what changed.
118+
119+
### 3. Instrumented Build
120+
The tool configures a second build directory (`build_inst`) with:
121+
- `LLVM_BUILD_INSTRUMENTED_COVERAGE=ON` — enables source-based coverage
122+
- `LLVM_INDIVIDUAL_TEST_COVERAGE=ON` — generates per-test `.profraw` files
123+
- The allowlist flags from step 2
124+
125+
Rebuilds are skipped if the patch hash (SHA-256 of the diff) matches the hash
126+
from the last successful build, stored in `build_inst/.last_patch_hash`.
127+
128+
### 4. Test Execution
129+
Only tests modified in the patch are run, against the instrumented binary.
130+
Each test writes its coverage data to a dedicated `.profraw` file under
131+
`build_inst/profiles/`, named by a hash of the test path to avoid collisions:
132+
133+
- **Lit tests** — run via `llvm-lit` with `LLVM_PROFILE_FILE` set per test
134+
- **Unit tests** — run directly as binaries with `LLVM_PROFILE_FILE` set
135+
136+
### 5. Coverage Processing
137+
For each `.profraw` file collected during test execution:
138+
1. `llvm-profdata merge` converts it to a `.profdata` file
139+
2. `llvm-cov show` produces a line-level coverage report for each patched
140+
source file, using the instrumented binary as the symbol source
141+
142+
### 6. Reporting
143+
The tool cross-references coverage results against the modified lines extracted
144+
in step 1. A line is considered:
145+
- **Covered** — if it was executed by at least one test
146+
- **Uncovered** — if it appears in the patch but was never executed
147+
- **Effectively covered** — if it was uncovered in some tests but covered in
148+
others (union of all runs wins)
149+
150+
Results are printed to the terminal in diff style with color coding, and the
151+
full details are written to `build_inst/patch_coverage.log`.
116152

117153
## Output
118154

119155
### `<build_inst>/patch_coverage.log`
120156

121157
```
158+
...
122159
Modified File: llvm/lib/IR/Example.cpp
123160
Covered Line: 42 : foo();
124161
Uncovered Line: 45 : bar();
@@ -127,20 +164,22 @@ Modified File: llvm/lib/IR/Example.cpp
127164
### Summary on Standard Output
128165

129166
```
130-
[code-coverage] Common uncovered lines for llvm/lib/IR/Example.cpp:
131-
Line 45: bar();
167+
[code-coverage] Coverage report for llvm/lib/IR/Example.cpp:
168+
Line 42 : foo();
169+
Line 45 : bar();
170+
132171
```
133172

134173
## Troubleshooting
135174

136175
### No coverage generated
137176

138-
* Ensure instrumented build succeeded and tests run
139-
* Check `.profraw` files in `build_inst` directory
177+
* Ensure instrumented build succeeded and tests run.
178+
* Check `.profraw` files in `build_inst` directory.
140179

141180
### Rebuild skipped unexpectedly
142181

143182
* Delete `binary` to force rebuild
144183

145184
### For any other doubt
146-
* Check `<build_inst>/patch_coverage.log`. It has full logging of tool
185+
* Check `<build_inst>/patch_coverage.log`. It has full logging of tool.

llvm/utils/patch-coverage/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def main():
157157
mark_build_success(inst_build_dir, patch_path)
158158
else:
159159
print("\n[patch-coverage] Skipping patch coverage (no changes)")
160-
sys.exit(1)
160+
sys.exit(0)
161161

162162
# Run all the test cases of patch with instrumented binary.
163163
inst_lit_path = os.path.join(inst_build_dir, "bin/llvm-lit")
Lines changed: 69 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,97 @@
11
import subprocess
22
import sys
33
import os
4+
from concurrent.futures import ThreadPoolExecutor, as_completed
45

56
from utils import classify_tests
67
from utils import log
7-
from utils import target_name
88

9-
# TODO: Can we process parallelly
109

11-
12-
def process_coverage_data(
13-
source_files, inst_build_dir, lit_binary, unit_binary, patch_path
10+
def process_single_profraw(
11+
profraw_file, source_files, binary, llvm_profdata, llvm_cov
1412
):
15-
coverage_files = {}
13+
results = {}
1614

17-
try:
18-
os.chdir(inst_build_dir)
19-
20-
unit_tests, lit_tests = classify_tests(patch_path)
21-
if not (unit_tests or lit_tests):
22-
return coverage_files
15+
profdata_output = os.path.splitext(profraw_file)[0] + ".profdata"
2316

24-
binary = unit_binary if unit_tests else lit_binary
17+
log("\nProfraw File:", profraw_file)
18+
log("Profdata File:", profdata_output)
2519

26-
for root, dirs, files in os.walk("."):
27-
for file in files:
28-
if os.path.basename(file) == "default.profraw":
29-
continue
20+
subprocess.check_call(
21+
[llvm_profdata, "merge", "-o", profdata_output, profraw_file]
22+
)
23+
log(f"Converted {profraw_file} to {profdata_output}")
3024

31-
if file.endswith(".profraw"):
32-
profraw_file = os.path.abspath(os.path.join(root, file))
33-
profdata_output = os.path.abspath(
34-
os.path.splitext(profraw_file)[0] + ".profdata"
35-
)
25+
for cpp_file in source_files:
26+
output_file = (
27+
os.path.splitext(profdata_output)[0]
28+
+ f"_{os.path.basename(cpp_file)}.txt"
29+
)
3630

37-
log("\nProfraw File:", profraw_file)
38-
log("Profdata File:", profdata_output)
31+
llvm_cov_cmd = [
32+
llvm_cov, "show",
33+
"-instr-profile", profdata_output,
34+
binary,
35+
"--format=text",
36+
cpp_file,
37+
]
3938

40-
llvm_profdata_cmd = [
41-
"./bin/llvm-profdata",
42-
"merge",
43-
"-o",
44-
profdata_output,
45-
profraw_file,
46-
]
39+
with open(output_file, "w") as output:
40+
subprocess.check_call(llvm_cov_cmd, stdout=output)
4741

48-
subprocess.check_call(llvm_profdata_cmd)
42+
log(f"Processed file saved as: {output_file}")
43+
results.setdefault(cpp_file, []).append(output_file)
4944

50-
log(f"Converted {profraw_file} to {profdata_output}")
45+
return results
5146

52-
for cpp_file in source_files:
53-
output_file = (
54-
os.path.splitext(profdata_output)[0]
55-
+ f"_{cpp_file.replace('/', '_')}.txt"
56-
)
5747

58-
parent_directory = os.path.dirname(os.getcwd())
59-
abs_cpp_file = os.path.abspath(
60-
os.path.join(parent_directory, cpp_file)
61-
)
48+
def process_coverage_data(
49+
source_files, inst_build_dir, lit_binary, unit_binary, patch_path
50+
):
51+
coverage_files = {}
6252

63-
llvm_cov_cmd = [
64-
"./bin/llvm-cov",
65-
"show",
66-
"-instr-profile",
67-
profdata_output,
68-
binary,
69-
"--format=text",
70-
abs_cpp_file,
71-
]
53+
unit_tests, lit_tests = classify_tests(patch_path)
54+
if not (unit_tests or lit_tests):
55+
return coverage_files
7256

73-
with open(output_file, "w") as output:
74-
subprocess.check_call(llvm_cov_cmd, stdout=output)
57+
binary = unit_binary if unit_tests else lit_binary
58+
llvm_profdata = os.path.join(inst_build_dir, "bin", "llvm-profdata")
59+
llvm_cov = os.path.join(inst_build_dir, "bin", "llvm-cov")
60+
profiles_dir = os.path.join(inst_build_dir, "profiles")
7561

76-
log(f"Processed file saved as: {output_file}")
62+
profraw_files = [
63+
os.path.abspath(os.path.join(profiles_dir, f))
64+
for f in os.listdir(profiles_dir)
65+
if f.endswith(".profraw")
66+
]
7767

78-
coverage_files.setdefault(abs_cpp_file, []).append(output_file)
68+
try:
69+
with ThreadPoolExecutor() as executor:
70+
futures = {
71+
executor.submit(
72+
process_single_profraw,
73+
profraw_file,
74+
source_files,
75+
binary,
76+
llvm_profdata,
77+
llvm_cov,
78+
): profraw_file
79+
for profraw_file in profraw_files
80+
}
81+
82+
for future in as_completed(futures):
83+
profraw_file = futures[future]
84+
try:
85+
result = future.result()
86+
for cpp_file, files in result.items():
87+
coverage_files.setdefault(cpp_file, []).extend(files)
88+
except subprocess.CalledProcessError as e:
89+
log(f"Error processing {profraw_file}: {e}")
90+
sys.exit(1)
7991

8092
log("\nConversion of profraw files to human-readable form is completed.\n")
81-
8293
return coverage_files
8394

84-
except subprocess.CalledProcessError as e:
85-
log("Error during profraw to profdata conversion:", e)
95+
except Exception as e:
96+
log("Error during coverage processing:", e)
8697
sys.exit(1)

llvm/utils/patch-coverage/test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from utils import log
88
from utils import target_name
99

10-
# TODO: Can we run llvm-lit in batch mode
10+
# TODO: We should run llvm-lit in batch mode.
1111

1212

1313
def run_single_test_with_coverage(llvm_lit_path, test_path, inst_build_dir):

llvm/utils/patch-coverage/utils.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,13 @@ def mark_build_success(build_dir, patch_path):
4141

4242

4343
def delete_profraw(inst_build_dir):
44-
cwd = os.getcwd()
45-
os.chdir(inst_build_dir)
46-
47-
command = 'find . -type f -name "*.profraw" -delete'
48-
os.chdir(cwd)
49-
44+
abs_dir = os.path.abspath(inst_build_dir)
5045
try:
51-
subprocess.run(command, shell=True, check=True)
52-
log("Older '.profraw' files are successfully deleted.")
46+
subprocess.run(
47+
["find", abs_dir, "-type", "f", "-name", "*.profraw", "-delete"],
48+
check=True
49+
)
50+
log("Older '.profraw' files successfully deleted.")
5351
except subprocess.CalledProcessError as e:
5452
log(f"Error: {e}")
5553

0 commit comments

Comments
 (0)