| #!/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() |