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