summaryrefslogtreecommitdiffstats
path: root/llvm/utils/check-coverage-regressions.py
blob: 031e25b0b3d37bd5a9993e16087c5d8540d6c20a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/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', 'Missed', 'Coverage', 'Functions', 'Executed'])

CoverageEntry = re.compile('^(.*) +(\d+) +(\d+) +([\d.]+)% +(\d+) +([\d.]+)%$')

def parse_file_coverage_line(line):
    '''Parse @line as a summary of a file's coverage information.

    >>> parse_file_coverage_line('foo.cpp 10 5 50.0% 10 0.0%')
    ('foo.cpp', FileCoverage(Regions=10, Missed=5, Coverage=50.0, Functions=10, Executed=0.0))
    '''

    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 = int(groups[2])
    coverage = float(groups[3])
    functions = int(groups[4])
    executed = float(groups[5])
    return (filename,
            FileCoverage(regions, missed, coverage, functions, executed))

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.Coverage < kThresh * old_fc.Coverage 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, 0), \
                         FileCoverage(10, 7, 30.0, 10, 0))
    Code coverage regression:
      File: foo.cpp
      Change in region coverage: -20.00%
      Change in function coverage: 0.00%
      No functions were added or removed.
      No regions were added or removed.

    >>> print_regression('foo.cpp', FileCoverage(10, 5, 50.0, 10, 0), \
                         FileCoverage(5, 4, 20.0, 5, 0))
    Code coverage regression:
      File: foo.cpp
      Change in region coverage: -30.00%
      Change in function coverage: 0.00%
      Change in the number of functions: -5
      Change in the number of regions: -5
    '''

    region_coverage_delta = new_fc.Coverage - old_fc.Coverage
    func_coverage_delta = new_fc.Executed - old_fc.Executed
    num_functions_delta = new_fc.Functions - old_fc.Functions
    num_regions_delta = new_fc.Regions - old_fc.Regions
    print("Code coverage regression:")
    print("  File:", filename)
    print("  Change in region coverage: {0:.2f}%".format(region_coverage_delta))
    print("  Change in function coverage: {0:.2f}%".format(func_coverage_delta))
    if num_functions_delta:
        print("  Change in the number of functions:", num_functions_delta)
    else:
        print("  No functions were added or removed.")
    if num_regions_delta:
        print("  Change in the number of regions:", num_regions_delta)
    else:
        print("  No regions were added or removed.")

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)
OpenPOWER on IntegriCloud