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