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