#!/usr/bin/env python '''Compare two coverage summaries for regressions. You can create a coverage summary by using the `llvm-cov report` command. Alternatively, you can use `utils/prepare-code-coverage-artifact.py` which creates summaries as well as file-based html reports. ''' from __future__ import print_function import argparse import collections import re import sys # This threshold must be in [0, 1]. The lower the threshold, the less likely # it is that a regression will be flagged and vice versa. kThresh = 1.0 FileCoverage = collections.namedtuple('FileCoverage', ['Regions', 'MissedRegions', 'RegionCoverage', 'Functions', 'MissedFunctions', 'Executed', 'Lines', 'MissedLines', 'LineCoverage']) CoverageEntry = re.compile(r'^(.*)' r' +(\d+) +(\d+) +([\d.]+)%' r' +(\d+) +(\d+) +([\d.]+)%' r' +(\d+) +(\d+) +([\d.]+)%$') def parse_file_coverage_line(line): '''Parse @line as a summary of a file's coverage information. >>> parse_file_coverage_line('report.cpp 5 2 60.00% 4 1 75.00% 13 4 69.23%') ('report.cpp', FileCoverage(\ Regions=5, MissedRegions=2, RegionCoverage=60.0, \ Functions=4, MissedFunctions=1, Executed=75.0, \ Lines=13, MissedLines=4, LineCoverage=69.23)) ''' m = re.match(CoverageEntry, line) if not m: print("Could not read coverage summary:", line) exit(1) groups = m.groups() filename = groups[0].strip() regions = int(groups[1]) missed_regions = int(groups[2]) region_coverage = float(groups[3]) functions = int(groups[4]) missed_functions = int(groups[5]) executed = float(groups[6]) lines = int(groups[7]) missed_lines = int(groups[8]) line_coverage = float(groups[9]) return (filename, FileCoverage(regions, missed_regions, region_coverage, functions, missed_functions, executed, lines, missed_lines, line_coverage)) def parse_summary(path): '''Parse the summary at @path. Return a dictionary mapping filenames to FileCoverage instances.''' with open(path, 'r') as f: lines = f.readlines() # Drop the header and the cell dividers. Include "TOTAL" in this list. file_coverages = lines[2:-2] + [lines[-1]] summary = {} for line in file_coverages: filename, fc = parse_file_coverage_line(line) summary[filename] = fc return summary def find_coverage_regressions(old_coverage, new_coverage): '''Given two coverage summaries, generate coverage regressions of the form: (filename, old FileCoverage, new FileCoverage).''' for filename in old_coverage.keys(): if filename not in new_coverage: continue old_fc = old_coverage[filename] new_fc = new_coverage[filename] if new_fc.RegionCoverage < kThresh * old_fc.RegionCoverage or \ new_fc.Executed < kThresh * old_fc.Executed: yield (filename, old_fc, new_fc) def print_regression(filename, old_fc, new_fc): '''Pretty-print a coverage regression in @filename. @old_fc is the old FileCoverage and @new_fc is the new one. >>> print_regression('foo.cpp', \ FileCoverage(10, 5, 50.0, 10, 10, 0, 20, 10, 50.0), \ FileCoverage(10, 7, 30.0, 10, 10, 0, 20, 14, 30.0)) Code coverage regression: File: foo.cpp Change in function coverage: 0.00% (0/10 -> 0/10) Change in line coverage : -20.00% (10/20 -> 6/20) Change in region coverage : -20.00% (5/10 -> 3/10) ''' func_coverage_delta = new_fc.Executed - old_fc.Executed line_coverage_delta = new_fc.LineCoverage - old_fc.LineCoverage region_coverage_delta = new_fc.RegionCoverage - old_fc.RegionCoverage print("Code coverage regression:") print(" File:", filename) print(" Change in function coverage: {0:.2f}% ({1}/{2} -> {3}/{4})".format( func_coverage_delta, old_fc.Functions - old_fc.MissedFunctions, old_fc.Functions, new_fc.Functions - new_fc.MissedFunctions, new_fc.Functions)) print(" Change in line coverage : {0:.2f}% ({1}/{2} -> {3}/{4})".format( line_coverage_delta, old_fc.Lines - old_fc.MissedLines, old_fc.Lines, new_fc.Lines - new_fc.MissedLines, new_fc.Lines)) print(" Change in region coverage : {0:.2f}% ({1}/{2} -> {3}/{4})".format( region_coverage_delta, old_fc.Regions - old_fc.MissedRegions, old_fc.Regions, new_fc.Regions - new_fc.MissedRegions, new_fc.Regions)) if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('old_summary', help='Path to the old coverage summary') parser.add_argument('new_summary', help='Path to the new coverage summary') args = parser.parse_args() old_coverage = parse_summary(args.old_summary) new_coverage = parse_summary(args.new_summary) num_regressions = 0 for filename, old_fc, new_fc in \ find_coverage_regressions(old_coverage, new_coverage): print_regression(filename, old_fc, new_fc) num_regressions += 1 if num_regressions > 0: exit(1) exit(0)