aboutsummaryrefslogtreecommitdiff
path: root/scripts/coverage/compare_gcov_json.py
blob: 1b92dc2c8c37f8c277528cc5721f8e15dbd4b951 (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
#!/usr/bin/env python3
#
# Compare output of two gcovr JSON reports and report differences. To
# generate the required output first:
#   - create two build dirs with --enable-gcov
#   - run set of tests in each
#   - run make coverage-html in each
#   - run gcovr --json --exclude-unreachable-branches \
#           --print-summary -o coverage.json --root ../../ . *.p
#
# Author: Alex Bennée <alex.bennee@linaro.org>
#
# SPDX-License-Identifier: GPL-2.0-or-later
#

import argparse
import json
import sys
from pathlib import Path

def create_parser():
    parser = argparse.ArgumentParser(
        prog='compare_gcov_json',
        description='analyse the differences in coverage between two runs')

    parser.add_argument('-a', type=Path, default=None,
                        help=('First file to check'))

    parser.add_argument('-b', type=Path, default=None,
                        help=('Second file to check'))

    parser.add_argument('--verbose', action='store_true', default=False,
                        help=('A minimal verbosity level that prints the '
                              'overall result of the check/wait'))
    return parser


# See https://gcovr.com/en/stable/output/json.html#json-format-reference
def load_json(json_file_path: Path, verbose = False) -> dict[str, set[int]]:

    with open(json_file_path) as f:
        data = json.load(f)

    root_dir = json_file_path.absolute().parent
    covered_lines = dict()

    for filecov in data["files"]:
        file_path = Path(filecov["file"])

        # account for generated files - map into src tree
        resolved_path = Path(file_path).absolute()
        if resolved_path.is_relative_to(root_dir):
            file_path = resolved_path.relative_to(root_dir)
            # print(f"remapped {resolved_path} to {file_path}")

        lines = filecov["lines"]

        executed_lines = set(
            linecov["line_number"]
            for linecov in filecov["lines"]
            if linecov["count"] != 0 and not linecov["gcovr/noncode"]
        )

        # if this file has any coverage add it to the system
        if len(executed_lines) > 0:
            if verbose:
                print(f"file {file_path} {len(executed_lines)}/{len(lines)}")
            covered_lines[str(file_path)] = executed_lines

    return covered_lines

def find_missing_files(first, second):
    """
    Return a list of files not covered in the second set
    """
    missing_files = []
    for f in sorted(first):
        file_a = first[f]
        try:
            file_b = second[f]
        except KeyError:
            missing_files.append(f)

    return missing_files

def main():
    """
    Script entry point
    """
    parser = create_parser()
    args = parser.parse_args()

    if not args.a or not args.b:
        print("We need two files to compare")
        sys.exit(1)

    first_coverage = load_json(args.a, args.verbose)
    second_coverage = load_json(args.b, args.verbose)

    first_missing = find_missing_files(first_coverage,
                                       second_coverage)

    second_missing = find_missing_files(second_coverage,
                                        first_coverage)

    a_name = args.a.parent.name
    b_name = args.b.parent.name

    print(f"{b_name} missing coverage in {len(first_missing)} files")
    for f in first_missing:
        print(f"  {f}")

    print(f"{a_name} missing coverage in {len(second_missing)} files")
    for f in second_missing:
        print(f"  {f}")


if __name__ == '__main__':
    main()