1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# Copyright (c) 2021-2024 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 subprocess 18import sys 19import uuid 20import os 21import logging 22 23from pathlib import Path 24from os import getenv, path, remove 25from multiprocessing import current_process 26from typing import Optional, List, Any, TextIO, Union 27 28from runner.logger import Log 29from runner.plugins.work_dir import WorkDir 30 31_LOGGER = logging.getLogger("runner.code_coverage.coverage") 32 33LLVM_PROFDATA_VERSION: Optional[str] = getenv('LLVM_PROFDATA_VERSION') 34LLVM_COV_VERSION: Optional[str] = getenv('LLVM_COV_VERSION') 35 36IGNORE_REGEX: Optional[str] = getenv('IGNORE_REGEX') 37 38LLVM_PROFDATA_BINARY = path.join('llvm-profdata-14') 39LLVM_COV_PATH_BINARY = path.join('llvm-cov-14') 40 41GENHTML_BINARY = 'genhtml' 42 43if LLVM_PROFDATA_VERSION is not None: 44 LLVM_PROFDATA_BINARY = f"{LLVM_PROFDATA_BINARY}-{LLVM_PROFDATA_VERSION}" 45 46if LLVM_COV_VERSION is not None: 47 LLVM_COV_PATH_BINARY = f"{LLVM_COV_PATH_BINARY}-{LLVM_COV_VERSION}" 48 49 50class LinuxCommands: 51 def run_command(self, command: List[str], stdout: Union[TextIO, int] = subprocess.PIPE) -> Union[TextIO, int]: 52 with self._popen( 53 args=command, 54 stdout=stdout, 55 stderr=subprocess.PIPE, 56 ) as proc: 57 try: 58 stdout = proc.communicate(timeout=5400) 59 except subprocess.TimeoutExpired: 60 Log.all(_LOGGER, f"Timeout when try execute {command}") 61 return stdout 62 63 def do_genhtml(self, args: List[str]) -> Union[TextIO, int]: 64 command = ['genhtml'] + args 65 return self.run_command(command) 66 67 def _popen(self, **kwargs: Any) -> Any: 68 if sys.version_info.major == 3 and sys.version_info.minor >= 6: 69 return subprocess.Popen(encoding=sys.stdout.encoding, **kwargs) 70 return subprocess.Popen(**kwargs) 71 72 73class LlvmCovCommands: 74 def llvm_prof_merge_command(self, args: List[str]) -> List[str]: 75 return [LLVM_PROFDATA_BINARY, 'merge'] + args 76 77 def llvm_cov_export_command(self, args: List[str]) -> List[str]: 78 return [LLVM_COV_PATH_BINARY, 'export'] + args 79 80 81class LlvmCov: 82 def __init__(self, build_dir: str, work_dir: WorkDir) -> None: 83 self.build_dir = build_dir 84 self.coverage_dir = work_dir.coverage_dir 85 self.llvm_cov_commands = LlvmCovCommands() 86 self.linux_commands = LinuxCommands() 87 88 def do_find(self, search_directory: Path, extension: str) -> List[Path]: 89 return list(Path(search_directory).rglob(extension)) 90 91 def get_uniq_profraw_profdata_file_paths(self) -> List[str]: 92 pid = current_process().pid 93 hash_code = uuid.uuid4() 94 file_path = f"{self.coverage_dir.profdata_dir}/{pid}-{hash_code}" 95 profraw_file = os.extsep.join([file_path, 'profraw']) 96 profdata_file = os.extsep.join([file_path, 'profdata']) 97 return [profraw_file, profdata_file] 98 99 def llvm_profdata_merge(self, args: List[str]) -> None: 100 prof_merge_command = self.llvm_cov_commands.llvm_prof_merge_command(args) 101 self.linux_commands.run_command(prof_merge_command) 102 103 def merge_and_delete_prowraw_files(self, profraw_file: str, profdata_file: str) -> None: 104 file = Path(profraw_file) 105 if file.is_file(): 106 self.llvm_profdata_merge(['--sparse', profraw_file, '-o', profdata_file]) 107 remove(profraw_file) 108 else: 109 Log.all(_LOGGER, f"File with name {profraw_file} not exist") 110 111 def make_profdata_list_file(self) -> None: 112 results = self.do_find(self.coverage_dir.profdata_dir, '*.profdata') 113 with os.fdopen(os.open(self.coverage_dir.profdata_files_list_file, os.O_APPEND | os.O_CREAT, 0o755), 114 "a", encoding="utf-8") as the_file: 115 for i in results: 116 the_file.write(str(i) + '\n') 117 118 def merge_all_profdata_files(self) -> None: 119 input_files = f"--input-files={self.coverage_dir.profdata_files_list_file}" 120 self.llvm_profdata_merge(['--sparse', input_files, '-o', str(self.coverage_dir.profdata_merged_file)]) 121 122 def llvm_cov_export_to_info_file(self) -> None: 123 instr_profile = f"-instr-profile={self.coverage_dir.profdata_merged_file}" 124 125 bin_dir = f"{self.build_dir}/bin" 126 127 bins = ['ark', 'ark_aot', 'ark_asm', 'ark_disasm', 'c2abc', 128 'c2p', 'es2panda', 'irtoc_ecmascript_fastpath_exec', 129 'irtoc_fastpath_exec', 'irtoc_interpreter_exec', 130 'panda', 'pandasm', 'paoc', 'verifier' 131 ] 132 133 args = ['-format=lcov', '-Xdemangler', 'c++filt', instr_profile] 134 135 for bin_n in bins: 136 args.append(f"--object={bin_dir}/{bin_n}") 137 138 for so_path in Path(self.build_dir).rglob('*.so'): 139 args.append(f"--object={so_path}") 140 141 if IGNORE_REGEX is not None: 142 ignore_filename_regex = f"--ignore-filename-regex=\"{IGNORE_REGEX}\"" 143 args.append(ignore_filename_regex) 144 145 command = self.llvm_cov_commands.llvm_cov_export_command(args) 146 147 with os.fdopen(os.open(self.coverage_dir.info_file, os.O_WRONLY | os.O_CREAT, 0o755), \ 148 "w", encoding="utf-8") as file_dot_info: 149 self.linux_commands.run_command(command, stdout=file_dot_info) 150 151 def genhtml(self) -> None: 152 output_directory = f"--output-directory={self.coverage_dir.html_report_dir}" 153 self.linux_commands.do_genhtml([output_directory, str(self.coverage_dir.info_file)]) 154