136 lines
3.9 KiB
Python
136 lines
3.9 KiB
Python
"""
|
|
Generates a report of code coverage using llvm's line-based coverage tool.
|
|
|
|
This merges together/indexes all the profraw files: https://clang.llvm.org/docs/SourceBasedCodeCoverage.html#creating-coverage-reports
|
|
|
|
Then it generates reports.
|
|
TODO(review): should this maybe be two separate things? idk!
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from dataclasses import dataclass
|
|
import glob
|
|
import tempfile
|
|
import subprocess
|
|
import shlex
|
|
import logging
|
|
|
|
log = logging.getLogger(__name__)
|
|
log.setLevel(logging.INFO)
|
|
fmt = logging.Formatter(
|
|
"{asctime} {levelname} {name}: {message}", datefmt="%b %d %H:%M:%S", style="{"
|
|
)
|
|
hand = logging.StreamHandler()
|
|
hand.setFormatter(fmt)
|
|
log.addHandler(hand)
|
|
|
|
|
|
def run(args, *rest, check: bool = True, **kwargs):
|
|
# stringify all path values
|
|
args = [str(a) for a in args]
|
|
|
|
logging.info("Run: %s", shlex.join(args))
|
|
return subprocess.run(args, *rest, check=check, **kwargs)
|
|
|
|
|
|
@dataclass
|
|
class LLVMCovArgs:
|
|
llvm_cov: Path
|
|
objects: list[str]
|
|
source_root: Path
|
|
profdata: Path
|
|
|
|
def to_opts(self) -> list[str]:
|
|
args = [self.objects[0]]
|
|
for obj in self.objects:
|
|
args.extend(["-object", obj])
|
|
|
|
args.extend([f"-compilation-dir={self.source_root}", f"-instr-profile={self.profdata}"])
|
|
|
|
return args
|
|
|
|
def show_html(self, out_dir: Path, *args):
|
|
run(
|
|
[
|
|
self.llvm_cov,
|
|
"show",
|
|
*self.to_opts(),
|
|
"-format=html",
|
|
f"-output-dir={out_dir}",
|
|
# by default, it doesn't show coverage of particular
|
|
# instantiations of template functions, but let's turn it on for
|
|
# fun!
|
|
"-show-instantiation-summary",
|
|
*args,
|
|
]
|
|
)
|
|
|
|
def export_lcov(self, out_file: Path, *args):
|
|
with out_file.open("w") as h:
|
|
run([self.llvm_cov, "export", *self.to_opts(), "-format=lcov"], stdout=h)
|
|
|
|
|
|
def main() -> int:
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--profraw-dir", type=Path, help="Directory of the .profraw files to ingest"
|
|
)
|
|
parser.add_argument(
|
|
"--out-dir", type=Path, help="Output directory for reports and intermediates (indexes)"
|
|
)
|
|
parser.add_argument(
|
|
"--source-root", type=Path, help="Source root, given to llvm-cov as -compilation-dir"
|
|
)
|
|
parser.add_argument("--llvm-cov", type=Path, help="llvm-cov executable")
|
|
parser.add_argument("--llvm-profdata", type=Path, help="llvm-profdata executable")
|
|
parser.add_argument("objects", nargs="+")
|
|
args = parser.parse_args()
|
|
|
|
profraw_dir: Path = args.profraw_dir
|
|
out_dir: Path = args.out_dir
|
|
source_root: Path = args.source_root
|
|
llvm_profdata: Path = args.llvm_profdata
|
|
llvm_cov: Path = args.llvm_cov
|
|
objects: list[str] = args.objects
|
|
|
|
profraw_dir.mkdir(parents=True, exist_ok=True)
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
all_profraws = sorted(glob.glob(str(profraw_dir / "**/*.profraw"), recursive=True))
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
|
|
inputs_file = tmp / "inputs"
|
|
inputs_file.write_text("\n".join(all_profraws) + "\n")
|
|
|
|
profdata = out_dir / "merged.profdata"
|
|
|
|
run(
|
|
[
|
|
llvm_profdata,
|
|
"merge",
|
|
# ostensibly faster or smaller according to LLVM docs
|
|
"-sparse",
|
|
# TODO: '--failure-mode=warn' ?
|
|
"-o",
|
|
profdata,
|
|
f"--input-files={inputs_file}",
|
|
]
|
|
)
|
|
|
|
llvm_cov_args = LLVMCovArgs(
|
|
llvm_cov=llvm_cov, objects=objects, source_root=source_root, profdata=profdata
|
|
)
|
|
llvm_cov_args.show_html(out_dir=out_dir)
|
|
llvm_cov_args.export_lcov(out_file=out_dir / "coverage.lcov")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|