1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# Copyright (c) 2021-2025 Huawei Device Co., Ltd. 5# Licensed under the Apache License, Version 2.0 (the 'License'); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an 'AS IS' BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import logging 18import uuid 19import os 20from multiprocessing import current_process 21from pathlib import Path 22from typing import Optional, List, Dict 23from runner.code_coverage.coverage_dir import CoverageDir 24from runner.code_coverage.cmd_executor import CmdExecutor 25from runner.utils import write_list_to_file 26from runner.logger import Log 27 28_LOGGER = logging.getLogger("runner.code_coverage.llvm_cov_tool") 29 30 31class LlvmCovTool(): 32 def __init__(self, build_dir_path: Path, coverage_dir: CoverageDir, cmd_executor: CmdExecutor) -> None: 33 self.build_dir = build_dir_path 34 self.bin_dir = self.build_dir / "bin" 35 self.coverage_dir = coverage_dir 36 self.cmd_executor = cmd_executor 37 38 self.llvm_profdata_binary = self.cmd_executor.get_binary("llvm-profdata", os.getenv('LLVM_PROFDATA_VERSION')) 39 self.llvm_cov_binary = self.cmd_executor.get_binary("llvm-cov", os.getenv('LLVM_COV_VERSION')) 40 self.genhtml_binary = self.cmd_executor.get_binary("genhtml") 41 42 self.components: Dict[str, Path] = {} 43 44 def get_uniq_profraw_profdata_file_paths(self, component_name: Optional[str] = None) -> List[Path]: 45 file_name = f"{current_process().pid}-{uuid.uuid4()}" 46 file_path = self.coverage_dir.profdata_dir / file_name 47 48 if component_name is not None: 49 self.components[component_name] = Path() 50 component_profdata_dir = self.coverage_dir.profdata_dir / component_name 51 component_profdata_dir.mkdir(parents=True, exist_ok=True) 52 file_path = component_profdata_dir / f"{component_name}-{file_name}" 53 54 profraw_file = file_path.with_suffix(".profraw") 55 profdata_file = file_path.with_suffix(".profdata") 56 return [profraw_file, profdata_file] 57 58 def merge_and_delete_prowraw_files(self, profraw_file: Path, profdata_file: Path) -> None: 59 if profraw_file.is_file(): 60 self._execute_profdata_merge(["--sparse", str(profraw_file), "-o", str(profdata_file)]) 61 os.remove(profraw_file) 62 else: 63 Log.all(_LOGGER, f"File with name {profraw_file} not exist") 64 65 def make_profdata_list_file( 66 self, 67 profdata_root_dir_path: Optional[Path] = None, 68 profdata_files_list_file_path: Optional[Path] = None 69 ) -> None: 70 _profdata_files_list_file_path = profdata_files_list_file_path or self.coverage_dir.profdata_files_list_file 71 _profdata_root_dir_path = profdata_root_dir_path or self.coverage_dir.profdata_dir 72 73 profdata_files = list(_profdata_root_dir_path.rglob("*.profdata")) 74 75 write_list_to_file(profdata_files, _profdata_files_list_file_path) 76 77 def make_profdata_list_files_by_components(self) -> None: 78 for key in self.components: 79 component_profdata_dir = self.coverage_dir.profdata_dir / key 80 profdata_files_list_file_path = self._get_path_from_coverage_work_dir(f"{key}_profdatalist.txt") 81 self.components[key] = profdata_files_list_file_path 82 self.make_profdata_list_file(component_profdata_dir, profdata_files_list_file_path) 83 84 def merge_all_profdata_files( 85 self, 86 profdata_files_list_file_path: Optional[Path] = None, 87 merged_profdata_file_path: Optional[Path] = None 88 ) -> None: 89 _merged_profdata_file_path = merged_profdata_file_path or self.coverage_dir.profdata_merged_file 90 _profdata_files_list_file_path = profdata_files_list_file_path or self.coverage_dir.profdata_files_list_file 91 92 args = [ 93 "--sparse", 94 f"--input-files={_profdata_files_list_file_path}", 95 "-o", str(_merged_profdata_file_path)] 96 97 self._execute_profdata_merge(args) 98 99 def merge_all_profdata_files_by_components(self) -> None: 100 for component_name, profdata_files_list_file_path in self.components.items(): 101 merged_profdata_file_path = self._get_path_from_coverage_work_dir(f"{component_name}_merged.profdata") 102 103 self.components[component_name] = merged_profdata_file_path 104 105 self.merge_all_profdata_files(profdata_files_list_file_path, merged_profdata_file_path) 106 107 def export_to_info_file( 108 self, 109 merged_profdata_file_path: Optional[Path] = None, 110 dot_info_file_path: Optional[Path] = None, 111 component_name: Optional[str] = None, 112 exclude_regex: Optional[str] = None 113 ) -> None: 114 _merged_profdata_file_path = merged_profdata_file_path or self.coverage_dir.profdata_merged_file 115 _dot_info_file_path = dot_info_file_path or self.coverage_dir.info_file 116 117 command = [str(self.llvm_cov_binary), "export"] 118 llvm_cov_export_command_args = ["-format=lcov", "-Xdemangler", "c++filt"] 119 120 llvm_cov_export_command_args.extend(["-instr-profile", str(_merged_profdata_file_path)]) 121 122 if component_name is None: 123 for bin_path in self.bin_dir.rglob('*'): 124 if os.path.splitext(bin_path)[1] == '': 125 llvm_cov_export_command_args.extend(["--object", str(bin_path)]) 126 else: 127 bin_path = self.bin_dir / component_name 128 llvm_cov_export_command_args.extend(["--object", str(bin_path)]) 129 130 for so_path in self.build_dir.rglob('*.so'): 131 llvm_cov_export_command_args.extend(["--object", str(so_path)]) 132 133 if exclude_regex is not None: 134 llvm_cov_export_command_args.extend(["--ignore-filename-regex", exclude_regex]) 135 136 command.extend(llvm_cov_export_command_args) 137 138 with os.fdopen(os.open(_dot_info_file_path, os.O_WRONLY | os.O_CREAT, 0o755), 'w', encoding='utf-8') as file: 139 self.cmd_executor.run_command(command, stdout=file) 140 141 def export_to_info_file_by_components(self, exclude_regex: Optional[str] = None) -> None: 142 for component_name, merged_profdata_file_path in self.components.items(): 143 dot_info_file_path = self._get_path_from_coverage_work_dir(f"{component_name}_lcov_format.info") 144 self.components[component_name] = dot_info_file_path 145 self.export_to_info_file(merged_profdata_file_path, dot_info_file_path, component_name, exclude_regex) 146 147 def generage_html_report(self) -> None: 148 if len(self.components): 149 for component_name, dot_info_file in self.components.items(): 150 html_report_dir = Path(self.coverage_dir.html_report_dir).joinpath(component_name) 151 output_directory_option = f"--output-directory={html_report_dir}" 152 self._execute_genhtml([output_directory_option, str(dot_info_file)]) 153 else: 154 output_directory_option = f"--output-directory={self.coverage_dir.html_report_dir}" 155 self._execute_genhtml([output_directory_option, str(self.coverage_dir.info_file)]) 156 157 def _execute_genhtml(self, args: List[str]) -> None: 158 self.cmd_executor.run_command([str(self.genhtml_binary)] + args) 159 160 def _execute_profdata_merge(self, args: List[str]) -> None: 161 self.cmd_executor.run_command([str(self.llvm_profdata_binary), "merge"] + args) 162 163 def _get_path_from_coverage_work_dir(self, path_name: str) -> Path: 164 return Path(self.coverage_dir.coverage_work_dir).joinpath(path_name) 165