#!/usr/bin/env python3 # ----------------------------------------------------------------------------------------------------------- # Copyright (c) 2025 Huawei Technologies Co., Ltd. # This program is free software, you can redistribute it and/or modify it under the terms and conditions of # CANN Open Software License Agreement Version 2.0 (the "License"). # Please refer to the License for details. You may not use this file except in compliance with the License. # THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. # See LICENSE in the root of the software repository for the full text of the License. # ----------------------------------------------------------------------------------------------------------- """ 生成覆盖率 """ import argparse import dataclasses import logging import os import subprocess from pathlib import Path import yaml class GenCoverage: @dataclasses.dataclass class Param: source_dir: Path | None = None data_dir: Path | None = None info_file: Path | None = None info_file_filtered: Path | None = None html_report_dir: Path | None = None filter_str: str = "" @staticmethod def get_exclude_paths_from_yaml(yaml_path: Path): with open(yaml_path, encoding="utf-8") as file: data = yaml.safe_load(file) exclude_paths = set() def extract_from_dict(obj, current_key=None): if isinstance(obj, dict): if "exclude" in obj and isinstance(obj["exclude"], list): for path in obj["exclude"]: exclude_paths.add(path) if "ut_cov_exclude" in obj and isinstance(obj["ut_cov_exclude"], list): for path in obj["ut_cov_exclude"]: exclude_paths.add(path) # 递归处理所有值 for key, value in obj.items(): extract_from_dict(value, key) elif isinstance(obj, list): for item in obj: extract_from_dict(item, current_key) extract_from_dict(data) return exclude_paths def init_filter_str(self, fs: list[list[str]] | None): if not fs: return for fl in fs: self.filter_str += f"{fl[0]} " def init_filter_str_from_yaml(self, source_dir: Path, yaml_path: Path): exclude_paths = self.get_exclude_paths_from_yaml(yaml_path) for path in sorted(exclude_paths): if path.startswith("*"): lcov_path = path else: full_path = source_dir / Path(path) if full_path.is_dir(): lcov_path = f"{full_path}/*" else: lcov_path = f"{full_path}" self.filter_str += f"{lcov_path} " @classmethod def main(cls): # 参数注册 parser = argparse.ArgumentParser(description="Generate Coverage", epilog="Best Regards!") parser.add_argument( "-s", "--source_base_dir", required=True, nargs=1, type=Path, help="Explicitly specify the source base directory.", ) parser.add_argument( "-c", "--coverage_data_dir", required=True, nargs=1, type=Path, help="Explicitly specify the *.da's base directory.", ) parser.add_argument( "-i", "--info_file", required=False, nargs=1, type=Path, help="Explicitly specify coverage info file path." ) # 考虑最低支持 Python 版本为 3.7, 此处用 append 而非 extend parser.add_argument( "-f", "--filter", required=False, action="append", nargs="*", type=str, help="Explicitly specify filter file/dir in coverage info.", ) parser.add_argument( "-y", "--yaml", required=False, nargs=1, type=Path, help="Explicitly specify filter file/dir from tests/test_config.yaml.", ) parser.add_argument( "--html_report", required=False, nargs=1, type=Path, help="Explicitly specify coverage html report dir." ) # 参数解析, 默认值处理 p = cls.Param() args = parser.parse_args() p.source_dir = Path(args.source_base_dir[0]).absolute() p.data_dir = Path(args.coverage_data_dir[0]).absolute() if args.info_file: p.info_file = Path(args.info_file[0]).absolute() p.info_file_filtered = Path(p.info_file.parent, f"{p.info_file.stem}_filtered{p.info_file.suffix}") else: p.info_file = Path(p.data_dir, "cov_result/coverage.info") p.info_file_filtered = p.info_file p.html_report_dir = args.html_report[0] if args.html_report else Path(p.info_file.parent, "html_report") p.html_report_dir = Path(p.html_report_dir).absolute() p.init_filter_str(fs=args.filter) p.init_filter_str_from_yaml(source_dir=p.source_dir, yaml_path=args.yaml[0]) if args.yaml else None logging.debug("[DEBUG] filter_str=%s", p.filter_str) # 参数检查 if not p.data_dir.exists(): logging.error("[ERROR] The dir(%s) required to find the .da files not exist.", p.data_dir) exit(1) if not p.info_file.exists(): p.info_file.parent.mkdir(parents=True, exist_ok=True) if not p.html_report_dir.exists(): p.html_report_dir.mkdir(parents=True, exist_ok=True) # 环境检查 if not cls._chk_env(): exit(1) # 生成覆盖率数据 cls._gen_cov(param=p) @classmethod def _chk_env(cls): try: ret = subprocess.run(["lcov", "--version"], capture_output=True, check=True, encoding="utf-8") ret.check_returncode() except FileNotFoundError: logging.error("[ERROR] lcov is required to generate coverage data, please install.") return False try: ret = subprocess.run(["genhtml", "--version"], capture_output=True, check=True, encoding="utf-8") ret.check_returncode() except FileNotFoundError: logging.error("[ERROR] genhtml is required to generate coverage html report, please install.") return False return True @classmethod def _gen_cov(cls, param: Param): """ 使用 lcov 生成覆盖率 """ # 当 log 等级小于 INFO 时,lcov 不带 -q 标签 lcov_log_tag = "" if logging.getLogger().level <= logging.INFO else "-q" logging.critical("================================================================================") logging.critical("Coverage Report") logging.critical("================================================================================") # 生成覆盖率 cmd = f"lcov -c -d {param.data_dir} -o {param.info_file} {lcov_log_tag}" logging.debug("[DEBUG] Generate origin coverage file, cmd=`%s`", cmd) ret = subprocess.run(cmd.split(), capture_output=False, check=True, encoding="utf-8") ret.check_returncode() if param.info_file.stat().st_size == 0: logging.critical("No file found in origin coverage file.") return logging.debug("[DEBUG] Generated origin coverage file %s", param.info_file) # 滤掉某些文件/路径的覆盖率信息 cmd = f"lcov --remove {param.info_file} {param.filter_str} -o {param.info_file_filtered} {lcov_log_tag}" logging.debug("[DEBUG] Generate filtered coverage file, cmd=`%s`", cmd) ret = subprocess.run(cmd.split(), capture_output=False, check=True, encoding="utf-8") ret.check_returncode() logging.debug("[DEBUG] Generated filtered coverage file %s", param.info_file_filtered) logging.info("[INFO] Generated coverage result in %s", os.path.dirname(param.info_file)) if param.info_file_filtered.stat().st_size == 0: logging.critical("No file found in filtered coverage file.") return # 生成 html 报告 sub_cmd_prefix = f"-p {param.source_dir}" if param.source_dir else "" cmd = f"genhtml {param.info_file_filtered} {sub_cmd_prefix} -o {param.html_report_dir} {lcov_log_tag}" logging.debug("[DEBUG] Generate filtered coverage html report, cmd=`%s`", cmd) ret = subprocess.run(cmd.split(), capture_output=False, check=True, encoding="utf-8") ret.check_returncode() logging.info("[INFO] Generated filtered coverage html report. %s", param.html_report_dir) # 输出覆盖率数据到终端 cmd = f"lcov --list {param.info_file_filtered}" ret = subprocess.run(cmd.split(), capture_output=False, check=True, encoding="utf-8") logging.critical("================================================================================") ret.check_returncode() if __name__ == "__main__": # 将环境变量中的 ASCEND_GLOBAL_LOG_LEVEL 换算成 python 的 log 等级 log_level = (int(os.getenv("ASCEND_GLOBAL_LOG_LEVEL", "3")) + 1) * 10 logging.basicConfig(format="[%(asctime)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", level=log_level) GenCoverage.main()