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