1#!/usr/bin/env python3
2#
3# Compare output of two gcovr JSON reports and report differences. To
4# generate the required output first:
5#   - create two build dirs with --enable-gcov
6#   - run set of tests in each
7#   - run make coverage-html in each
8#   - run gcovr --json --exclude-unreachable-branches \
9#           --print-summary -o coverage.json --root ../../ . *.p
10#
11# Author: Alex Bennée <alex.bennee@linaro.org>
12#
13# SPDX-License-Identifier: GPL-2.0-or-later
14#
15
16import argparse
17import json
18import sys
19from pathlib import Path
20
21def create_parser():
22    parser = argparse.ArgumentParser(
23        prog='compare_gcov_json',
24        description='analyse the differences in coverage between two runs')
25
26    parser.add_argument('-a', type=Path, default=None,
27                        help=('First file to check'))
28
29    parser.add_argument('-b', type=Path, default=None,
30                        help=('Second file to check'))
31
32    parser.add_argument('--verbose', action='store_true', default=False,
33                        help=('A minimal verbosity level that prints the '
34                              'overall result of the check/wait'))
35    return parser
36
37
38# See https://gcovr.com/en/stable/output/json.html#json-format-reference
39def load_json(json_file_path: Path, verbose = False) -> dict[str, set[int]]:
40
41    with open(json_file_path) as f:
42        data = json.load(f)
43
44    root_dir = json_file_path.absolute().parent
45    covered_lines = dict()
46
47    for filecov in data["files"]:
48        file_path = Path(filecov["file"])
49
50        # account for generated files - map into src tree
51        resolved_path = Path(file_path).absolute()
52        if resolved_path.is_relative_to(root_dir):
53            file_path = resolved_path.relative_to(root_dir)
54            # print(f"remapped {resolved_path} to {file_path}")
55
56        lines = filecov["lines"]
57
58        executed_lines = set(
59            linecov["line_number"]
60            for linecov in filecov["lines"]
61            if linecov["count"] != 0 and not linecov["gcovr/noncode"]
62        )
63
64        # if this file has any coverage add it to the system
65        if len(executed_lines) > 0:
66            if verbose:
67                print(f"file {file_path} {len(executed_lines)}/{len(lines)}")
68            covered_lines[str(file_path)] = executed_lines
69
70    return covered_lines
71
72def find_missing_files(first, second):
73    """
74    Return a list of files not covered in the second set
75    """
76    missing_files = []
77    for f in sorted(first):
78        file_a = first[f]
79        try:
80            file_b = second[f]
81        except KeyError:
82            missing_files.append(f)
83
84    return missing_files
85
86def main():
87    """
88    Script entry point
89    """
90    parser = create_parser()
91    args = parser.parse_args()
92
93    if not args.a or not args.b:
94        print("We need two files to compare")
95        sys.exit(1)
96
97    first_coverage = load_json(args.a, args.verbose)
98    second_coverage = load_json(args.b, args.verbose)
99
100    first_missing = find_missing_files(first_coverage,
101                                       second_coverage)
102
103    second_missing = find_missing_files(second_coverage,
104                                        first_coverage)
105
106    a_name = args.a.parent.name
107    b_name = args.b.parent.name
108
109    print(f"{b_name} missing coverage in {len(first_missing)} files")
110    for f in first_missing:
111        print(f"  {f}")
112
113    print(f"{a_name} missing coverage in {len(second_missing)} files")
114    for f in second_missing:
115        print(f"  {f}")
116
117
118if __name__ == '__main__':
119    main()
120