blob: 1b92dc2c8c37f8c277528cc5721f8e15dbd4b951 [file] [log] [blame]
Alex Bennée899c3fc2023-04-03 14:49:10 +01001#!/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()