• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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