blob: 8a695495be30788ef090030965b8f3ffb030bc7e [file]
#!/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()