| #!/usr/bin/env vpython3 |
| # Copyright 2026 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Compiles profiles and generates coverage reports for local test binaries.""" |
| |
| import argparse |
| import os |
| import pathlib |
| import subprocess |
| import sys |
| |
| |
| def find_llvm_tool(tool_name: str) -> pathlib.Path: |
| """Locates pinned LLVM tool inside third_party. |
| |
| Args: |
| tool_name: Executable name (e.g., 'llvm-profdata'). |
| |
| Returns: |
| Absolute path to the pinned tool executable. |
| """ |
| base = pathlib.Path(__file__).resolve().parents[4] |
| tool_path = base / 'third_party' / 'llvm-build' / 'Release+Asserts' / 'bin' |
| exec_path = tool_path / tool_name |
| if not exec_path.exists(): |
| print( |
| f'Downloading missing LLVM coverage tool ({tool_name})...', |
| file=sys.stderr, |
| ) |
| update_script = base / 'tools' / 'clang' / 'scripts' / 'update.py' |
| try: |
| subprocess.check_call([ |
| sys.executable, |
| str(update_script), |
| '--package', |
| 'coverage_tools', |
| ]) |
| except subprocess.CalledProcessError as e: |
| print(f'Failed to download LLVM coverage tools: {e}', file=sys.stderr) |
| sys.exit(1) |
| |
| if not exec_path.exists(): |
| print(f'LLVM tool still not found at {exec_path}', file=sys.stderr) |
| sys.exit(1) |
| return exec_path |
| |
| |
| def run_test_binary( |
| binary_path: pathlib.Path, |
| profraw_dir: pathlib.Path, |
| extra_args: list[str], |
| ) -> int: |
| """Runs local test binary with LLVM_PROFILE_FILE environment variable. |
| |
| Args: |
| binary_path: Path to executable test binary. |
| profraw_dir: Scratch directory to store raw profiles. |
| extra_args: Additional arguments passed to the binary. |
| |
| Returns: |
| Subprocess return code integer. |
| """ |
| env = os.environ.copy() |
| prof_template = profraw_dir / f'{binary_path.name}.%4m%c.profraw' |
| env['LLVM_PROFILE_FILE'] = str(prof_template) |
| cmd = [str(binary_path)] + extra_args |
| res = subprocess.run(cmd, env=env, check=False) |
| return res.returncode |
| |
| |
| def merge_profiles( |
| profraw_dir: pathlib.Path, |
| output_profdata: pathlib.Path, |
| ) -> bool: |
| """Merges profraw files into indexed profdata artifact. |
| |
| Args: |
| profraw_dir: Directory containing .profraw files. |
| output_profdata: Output .profdata file path. |
| |
| Returns: |
| True if successful, False otherwise. |
| """ |
| profdata = find_llvm_tool('llvm-profdata') |
| raw_files = list(profraw_dir.glob('*.profraw')) |
| if not raw_files: |
| print(f'No profraw files found in {profraw_dir}', file=sys.stderr) |
| return False |
| cmd = [str(profdata), 'merge', '-o', str(output_profdata), '--sparse'] |
| cmd += [str(f) for f in raw_files] |
| res = subprocess.run(cmd, check=False) |
| return res.returncode == 0 |
| |
| |
| def export_coverage( |
| binary_path: pathlib.Path, |
| profdata_path: pathlib.Path, |
| source_file: str | None, |
| ) -> str | None: |
| """Runs llvm-cov export to return text report or summary. |
| |
| Args: |
| binary_path: Path to instrumented binary. |
| profdata_path: Path to merged .profdata artifact. |
| source_file: Optional source file filter. |
| |
| Returns: |
| Exported coverage text string or None on error. |
| """ |
| cov_tool = find_llvm_tool('llvm-cov') |
| cmd = [ |
| str(cov_tool), 'show', |
| str(binary_path), f'-instr-profile={profdata_path}' |
| ] |
| if source_file: |
| cmd.append(source_file) |
| res = subprocess.run(cmd, capture_output=True, text=True, check=False) |
| if res.returncode != 0: |
| print(f'llvm-cov failed: {res.stderr}', file=sys.stderr) |
| return None |
| return res.stdout |
| |
| |
| def main() -> None: |
| """Parses CLI args and executes rapid local coverage generation.""" |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument('--binary', |
| required=True, |
| type=pathlib.Path, |
| help='Path to test binary') |
| parser.add_argument('--scratch', |
| type=pathlib.Path, |
| default=pathlib.Path('scratch/local_prof'), |
| help='Scratch directory for profiles') |
| parser.add_argument('--source', help='Optional source file filter') |
| parser.add_argument('test_args', |
| nargs=argparse.REMAINDER, |
| help='Arguments passed to test binary') |
| args = parser.parse_args() |
| |
| args.scratch.mkdir(parents=True, exist_ok=True) |
| code = run_test_binary(args.binary, args.scratch, args.test_args) |
| if code != 0: |
| print(f'Warning: test binary exited with {code}', file=sys.stderr) |
| |
| profdata = args.scratch / f'{args.binary.name}.profdata' |
| if not merge_profiles(args.scratch, profdata): |
| sys.exit(1) |
| |
| report = export_coverage(args.binary, profdata, args.source) |
| if report: |
| print(report) |
| |
| |
| if __name__ == '__main__': |
| main() |