1# Copyright 2022, The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Code coverage instrumentation and collection functionality.""" 15 16import logging 17import os 18import subprocess 19 20from pathlib import Path 21from typing import List, Set 22 23from atest import atest_utils 24from atest import constants 25from atest import module_info 26from atest.test_finders import test_info 27 28CLANG_VERSION='r475365b' 29 30def build_env_vars(): 31 """Environment variables for building with code coverage instrumentation. 32 33 Returns: 34 A dict with the environment variables to set. 35 """ 36 env_vars = { 37 'CLANG_COVERAGE': 'true', 38 'NATIVE_COVERAGE_PATHS': '*', 39 'EMMA_INSTRUMENT': 'true', 40 } 41 return env_vars 42 43 44def tf_args(*value): 45 """TradeFed command line arguments needed to collect code coverage. 46 47 Returns: 48 A list of the command line arguments to append. 49 """ 50 del value 51 build_top = Path(os.environ.get(constants.ANDROID_BUILD_TOP)) 52 llvm_profdata = build_top.joinpath( 53 f'prebuilts/clang/host/linux-x86/clang-{CLANG_VERSION}') 54 return ('--coverage', 55 '--coverage-toolchain', 'JACOCO', 56 '--coverage-toolchain', 'CLANG', 57 '--auto-collect', 'JAVA_COVERAGE', 58 '--auto-collect', 'CLANG_COVERAGE', 59 '--llvm-profdata-path', str(llvm_profdata)) 60 61 62def generate_coverage_report(results_dir: str, 63 test_infos: List[test_info.TestInfo], 64 mod_info: module_info.ModuleInfo): 65 """Generates HTML code coverage reports based on the test info.""" 66 67 soong_intermediates = Path( 68 atest_utils.get_build_out_dir()).joinpath('soong/.intermediates') 69 70 # Collect dependency and source file information for the tests and any 71 # Mainline modules. 72 dep_modules = _get_test_deps(test_infos, mod_info) 73 src_paths = _get_all_src_paths(dep_modules, mod_info) 74 75 # Collect JaCoCo class jars from the build for coverage report generation. 76 jacoco_report_jars = {} 77 unstripped_native_binaries = set() 78 for module in dep_modules: 79 for path in mod_info.get_paths(module): 80 module_dir = soong_intermediates.joinpath(path, module) 81 # Check for uninstrumented Java class files to report coverage. 82 classfiles = list( 83 module_dir.rglob('jacoco-report-classes/*.jar')) 84 if classfiles: 85 jacoco_report_jars[module] = classfiles 86 87 # Check for unstripped native binaries to report coverage. 88 unstripped_native_binaries.update( 89 module_dir.glob('*cov*/unstripped/*')) 90 91 if jacoco_report_jars: 92 _generate_java_coverage_report(jacoco_report_jars, src_paths, 93 results_dir, mod_info) 94 95 if unstripped_native_binaries: 96 _generate_native_coverage_report(unstripped_native_binaries, 97 results_dir) 98 99 100def _get_test_deps(test_infos, mod_info): 101 """Gets all dependencies of the TestInfo, including Mainline modules.""" 102 deps = set() 103 104 for info in test_infos: 105 deps.add(info.raw_test_name) 106 deps |= _get_transitive_module_deps( 107 mod_info.get_module_info(info.raw_test_name), mod_info, deps) 108 109 # Include dependencies of any Mainline modules specified as well. 110 if not info.mainline_modules: 111 continue 112 113 for mainline_module in info.mainline_modules: 114 deps.add(mainline_module) 115 deps |= _get_transitive_module_deps( 116 mod_info.get_module_info(mainline_module), mod_info, deps) 117 118 return deps 119 120 121def _get_transitive_module_deps(info, 122 mod_info: module_info.ModuleInfo, 123 seen: Set[str]) -> Set[str]: 124 """Gets all dependencies of the module, including .impl versions.""" 125 deps = set() 126 127 for dep in info.get(constants.MODULE_DEPENDENCIES, []): 128 if dep in seen: 129 continue 130 131 seen.add(dep) 132 133 dep_info = mod_info.get_module_info(dep) 134 135 # Mainline modules sometimes depend on `java_sdk_library` modules that 136 # generate synthetic build modules ending in `.impl` which do not appear 137 # in the ModuleInfo. Strip this suffix to prevent incomplete dependency 138 # information when generating coverage reports. 139 # TODO(olivernguyen): Reconcile this with 140 # ModuleInfo.get_module_dependency(...). 141 if not dep_info: 142 dep = dep.removesuffix('.impl') 143 dep_info = mod_info.get_module_info(dep) 144 145 if not dep_info: 146 continue 147 148 deps.add(dep) 149 deps |= _get_transitive_module_deps(dep_info, mod_info, seen) 150 151 return deps 152 153 154def _get_all_src_paths(modules, mod_info): 155 """Gets the set of directories containing any source files from the modules. 156 """ 157 src_paths = set() 158 159 for module in modules: 160 info = mod_info.get_module_info(module) 161 if not info: 162 continue 163 164 # Do not report coverage for test modules. 165 if mod_info.is_testable_module(info): 166 continue 167 168 src_paths.update( 169 os.path.dirname(f) for f in info.get(constants.MODULE_SRCS, [])) 170 171 src_paths = {p for p in src_paths if not _is_generated_code(p)} 172 return src_paths 173 174 175def _is_generated_code(path): 176 return 'soong/.intermediates' in path 177 178 179def _generate_java_coverage_report(report_jars, src_paths, results_dir, 180 mod_info): 181 build_top = os.environ.get(constants.ANDROID_BUILD_TOP) 182 out_dir = os.path.join(results_dir, 'java_coverage') 183 jacoco_files = atest_utils.find_files(results_dir, '*.ec') 184 185 os.mkdir(out_dir) 186 jacoco_lcov = mod_info.get_module_info('jacoco_to_lcov_converter') 187 jacoco_lcov = os.path.join(build_top, jacoco_lcov['installed'][0]) 188 lcov_reports = [] 189 190 for name, classfiles in report_jars.items(): 191 dest = f'{out_dir}/{name}.info' 192 cmd = [jacoco_lcov, '-o', dest] 193 for classfile in classfiles: 194 cmd.append('-classfiles') 195 cmd.append(str(classfile)) 196 for src_path in src_paths: 197 cmd.append('-sourcepath') 198 cmd.append(src_path) 199 cmd.extend(jacoco_files) 200 try: 201 subprocess.run(cmd, check=True, 202 stdout=subprocess.PIPE, 203 stderr=subprocess.STDOUT) 204 except subprocess.CalledProcessError as err: 205 atest_utils.colorful_print( 206 f'Failed to generate coverage for {name}:', constants.RED) 207 logging.exception(err.stdout) 208 atest_utils.colorful_print(f'Coverage for {name} written to {dest}.', 209 constants.GREEN) 210 lcov_reports.append(dest) 211 212 _generate_lcov_report(out_dir, lcov_reports, build_top) 213 214 215def _generate_native_coverage_report(unstripped_native_binaries, results_dir): 216 build_top = os.environ.get(constants.ANDROID_BUILD_TOP) 217 out_dir = os.path.join(results_dir, 'native_coverage') 218 profdata_files = atest_utils.find_files(results_dir, '*.profdata') 219 220 os.mkdir(out_dir) 221 cmd = ['llvm-cov', 222 'show', 223 '-format=html', 224 f'-output-dir={out_dir}', 225 f'-path-equivalence=/proc/self/cwd,{build_top}'] 226 for profdata in profdata_files: 227 cmd.append('--instr-profile') 228 cmd.append(profdata) 229 for binary in unstripped_native_binaries: 230 # Exclude .rsp files. These are files containing the command line used 231 # to generate the unstripped binaries, but are stored in the same 232 # directory as the actual output binary. 233 if not binary.match('*.rsp'): 234 cmd.append(f'--object={str(binary)}') 235 236 try: 237 subprocess.run(cmd, check=True, 238 stdout=subprocess.PIPE, 239 stderr=subprocess.STDOUT) 240 atest_utils.colorful_print(f'Native coverage written to {out_dir}.', 241 constants.GREEN) 242 except subprocess.CalledProcessError as err: 243 atest_utils.colorful_print('Failed to generate native code coverage.', 244 constants.RED) 245 logging.exception(err.stdout) 246 247 248def _generate_lcov_report(out_dir, reports, root_dir=None): 249 cmd = ['genhtml', '-q', '-o', out_dir] 250 if root_dir: 251 cmd.extend(['-p', root_dir]) 252 cmd.extend(reports) 253 try: 254 subprocess.run(cmd, check=True, 255 stdout=subprocess.PIPE, 256 stderr=subprocess.STDOUT) 257 atest_utils.colorful_print( 258 f'Code coverage report written to {out_dir}.', 259 constants.GREEN) 260 atest_utils.colorful_print( 261 f'To open, Ctrl+Click on file://{out_dir}/index.html', 262 constants.GREEN) 263 except subprocess.CalledProcessError as err: 264 atest_utils.colorful_print('Failed to generate HTML coverage report.', 265 constants.RED) 266 logging.exception(err.stdout) 267 except FileNotFoundError: 268 atest_utils.colorful_print('genhtml is not on the $PATH.', 269 constants.RED) 270 atest_utils.colorful_print( 271 'Run `sudo apt-get install lcov -y` to install this tool.', 272 constants.RED) 273