| #!/usr/bin/env python3 |
| # tools/gcov.py |
| |
| # SPDX-License-Identifier: Apache-2.0 |
| # |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. The |
| # ASF licenses this file to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance with the |
| # License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations |
| # under the License. |
| |
| import argparse |
| import os |
| import re |
| import shutil |
| import subprocess |
| import sys |
| |
| |
| def parse_gcda_data(path): |
| with open(path, "r") as file: |
| lines = file.read().strip().splitlines() |
| |
| gcda_path = path + "_covert" |
| os.makedirs(gcda_path, exist_ok=True) |
| |
| started = False |
| filename = "" |
| output = "" |
| size = 0 |
| |
| for line in lines: |
| if line.startswith("gcov start"): |
| started = True |
| match = re.search(r"filename:(.*?)\s+size:\s*(\d+)Byte", line) |
| if match: |
| filename = match.group(1) |
| size = int(match.group(2)) |
| continue |
| |
| if not started: |
| continue |
| |
| try: |
| if line.startswith("gcov end"): |
| started = False |
| if size != len(output) // 2: |
| raise ValueError( |
| f"Size mismatch for {filename}: expected {size} bytes, got {len(output) // 2} bytes" |
| ) |
| |
| match = re.search(r"checksum:\s*(0x[0-9a-fA-F]+)", line) |
| if match: |
| checksum = int(match.group(1), 16) |
| output = bytearray.fromhex(output) |
| expected = sum(output) % 65536 |
| if checksum != expected: |
| raise ValueError( |
| f"Checksum mismatch for {filename}: expected {checksum}, got {expected}" |
| ) |
| |
| outfile = os.path.join(gcda_path, "./" + filename) |
| os.makedirs(os.path.dirname(outfile), exist_ok=True) |
| |
| with open(outfile, "wb") as fp: |
| fp.write(output) |
| print(f"write {outfile} success") |
| output = "" |
| else: |
| output += line.strip() |
| except Exception as e: |
| print(f"Error processing {path}: {e}") |
| print(f"gcov start filename:{filename} size:{size}") |
| print(output) |
| print(f"gcov end filename:{filename} checksum:{checksum}") |
| |
| return gcda_path |
| |
| |
| def correct_content_path(file, shield: list, newpath): |
| with open(file, "r", encoding="utf-8") as f: |
| content = f.read() |
| |
| for i in shield: |
| content = content.replace(i, "") |
| |
| new_content = content |
| if newpath is not None: |
| pattern = r"SF:([^\s]*?)/nuttx/include/nuttx" |
| matches = re.findall(pattern, content) |
| |
| if matches: |
| new_content = content.replace(matches[0], newpath) |
| |
| with open(file, "w", encoding="utf-8") as f: |
| f.write(new_content) |
| |
| |
| def copy_file_endswith(endswith, source, target): |
| for root, dirs, files in os.walk(source, topdown=True): |
| if target in root: |
| continue |
| |
| for file in files: |
| if file.endswith(endswith): |
| src_file = os.path.join(root, file) |
| dst_file = os.path.join(target, os.path.relpath(src_file, source)) |
| os.makedirs(os.path.dirname(dst_file), exist_ok=True) |
| shutil.copy2(src_file, dst_file) |
| |
| |
| def run_lcov(data_dir, gcov_tool): |
| output = data_dir + ".info" |
| # lcov collect coverage data to coverage.info |
| command = [ |
| "lcov", |
| "-c", |
| "-o", |
| output, |
| "--rc", |
| "lcov_branch_coverage=1", |
| "--gcov-tool", |
| gcov_tool, |
| "--ignore-errors", |
| "gcov", |
| "--directory", |
| data_dir, |
| ] |
| print(command) |
| subprocess.run( |
| command, |
| check=True, |
| stdout=sys.stdout, |
| stderr=sys.stdout, |
| ) |
| |
| return output |
| |
| |
| def run_genhtml(info, report): |
| cmd = [ |
| "genhtml", |
| "--branch-coverage", |
| "-o", |
| report, |
| "--ignore-errors", |
| "source", |
| info, |
| ] |
| print(cmd) |
| subprocess.run( |
| cmd, |
| check=True, |
| stdout=sys.stdout, |
| stderr=sys.stdout, |
| ) |
| |
| |
| def run_merge(gcda_dir1, gcda_dir2, output, merge_tool): |
| command = [ |
| merge_tool, |
| "merge", |
| gcda_dir1, |
| gcda_dir2, |
| "-o", |
| output, |
| ] |
| print(command) |
| subprocess.run( |
| command, |
| check=True, |
| stdout=sys.stdout, |
| stderr=sys.stdout, |
| ) |
| |
| |
| def arg_parser(): |
| parser = argparse.ArgumentParser( |
| description="Code coverage generation tool.", add_help=False |
| ) |
| parser.add_argument("-t", dest="gcov_tool", help="Path to gcov tool") |
| parser.add_argument("-b", dest="base_dir", help="Compile base directory") |
| parser.add_argument("--debug", action="store_true", help="Enable debug mode") |
| parser.add_argument("--delete", action="store_true", help="Delete gcda files") |
| parser.add_argument( |
| "-s", |
| dest="gcno_dir", |
| default=".", |
| help="Directory containing gcno files", |
| ) |
| parser.add_argument( |
| "-a", |
| dest="gcda_dir", |
| default=".", |
| nargs="+", |
| help="Directory containing gcda files", |
| ) |
| parser.add_argument( |
| "-x", |
| dest="only_copy", |
| action="store_true", |
| help="Only copy *.gcno and *.gcda files", |
| ) |
| parser.add_argument( |
| "-o", |
| dest="result_dir", |
| default="gcov", |
| help="Directory to store gcov data and report", |
| ) |
| |
| return parser.parse_args() |
| |
| |
| def main(): |
| args = arg_parser() |
| |
| root_dir = os.getcwd() |
| gcno_dir = os.path.abspath(args.gcno_dir) |
| result_dir = os.path.abspath(args.result_dir) |
| |
| os.makedirs(result_dir, exist_ok=True) |
| merge_tool = args.gcov_tool + "-tool" |
| data_dir = os.path.join(result_dir, "data") |
| report_dir = os.path.join(result_dir, "report") |
| coverage_file = os.path.join(result_dir, "coverage.info") |
| |
| if args.debug: |
| debug_file = os.path.join(result_dir, "debug.log") |
| sys.stdout = open(debug_file, "w+") |
| |
| # lcov tool is required |
| if shutil.which("lcov") is None: |
| print( |
| "Error: Code coverage generation tool is not detected, please install lcov." |
| ) |
| sys.exit(1) |
| |
| gcda_dirs = [] |
| for i in args.gcda_dir: |
| if os.path.isfile(i): |
| gcda_dirs.append(parse_gcda_data(os.path.join(root_dir, i))) |
| if args.delete: |
| os.remove(i) |
| else: |
| gcda_dirs.append(os.path.abspath(i)) |
| |
| # Merge all gcda files |
| shutil.copytree(gcda_dirs[0], data_dir) |
| for gcda_dir in gcda_dirs[1:]: |
| run_merge(data_dir, gcda_dir, data_dir, merge_tool) |
| |
| # Copy gcno files and run lcov generate coverage info file |
| copy_file_endswith(".gcno", gcno_dir, data_dir) |
| coverage_file = run_lcov(data_dir, args.gcov_tool) |
| |
| # Only copy files |
| if args.only_copy: |
| sys.exit(0) |
| |
| try: |
| run_genhtml(coverage_file, report_dir) |
| |
| print( |
| "Copy the following link and open it in the browser to view the coverage report:" |
| ) |
| print(f"file://{os.path.join(report_dir, 'index.html')}") |
| |
| except subprocess.CalledProcessError: |
| print("Failed to generate coverage file.") |
| sys.exit(1) |
| |
| |
| if __name__ == "__main__": |
| main() |