You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
273 lines
9.7 KiB
273 lines
9.7 KiB
# Copyright 2022, The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""Code coverage instrumentation and collection functionality."""
|
|
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
|
|
from pathlib import Path
|
|
from typing import List, Set
|
|
|
|
from atest import atest_utils
|
|
from atest import constants
|
|
from atest import module_info
|
|
from atest.test_finders import test_info
|
|
|
|
CLANG_VERSION='r475365b'
|
|
|
|
def build_env_vars():
|
|
"""Environment variables for building with code coverage instrumentation.
|
|
|
|
Returns:
|
|
A dict with the environment variables to set.
|
|
"""
|
|
env_vars = {
|
|
'CLANG_COVERAGE': 'true',
|
|
'NATIVE_COVERAGE_PATHS': '*',
|
|
'EMMA_INSTRUMENT': 'true',
|
|
}
|
|
return env_vars
|
|
|
|
|
|
def tf_args(*value):
|
|
"""TradeFed command line arguments needed to collect code coverage.
|
|
|
|
Returns:
|
|
A list of the command line arguments to append.
|
|
"""
|
|
del value
|
|
build_top = Path(os.environ.get(constants.ANDROID_BUILD_TOP))
|
|
llvm_profdata = build_top.joinpath(
|
|
f'prebuilts/clang/host/linux-x86/clang-{CLANG_VERSION}')
|
|
return ('--coverage',
|
|
'--coverage-toolchain', 'JACOCO',
|
|
'--coverage-toolchain', 'CLANG',
|
|
'--auto-collect', 'JAVA_COVERAGE',
|
|
'--auto-collect', 'CLANG_COVERAGE',
|
|
'--llvm-profdata-path', str(llvm_profdata))
|
|
|
|
|
|
def generate_coverage_report(results_dir: str,
|
|
test_infos: List[test_info.TestInfo],
|
|
mod_info: module_info.ModuleInfo):
|
|
"""Generates HTML code coverage reports based on the test info."""
|
|
|
|
soong_intermediates = Path(
|
|
atest_utils.get_build_out_dir()).joinpath('soong/.intermediates')
|
|
|
|
# Collect dependency and source file information for the tests and any
|
|
# Mainline modules.
|
|
dep_modules = _get_test_deps(test_infos, mod_info)
|
|
src_paths = _get_all_src_paths(dep_modules, mod_info)
|
|
|
|
# Collect JaCoCo class jars from the build for coverage report generation.
|
|
jacoco_report_jars = {}
|
|
unstripped_native_binaries = set()
|
|
for module in dep_modules:
|
|
for path in mod_info.get_paths(module):
|
|
module_dir = soong_intermediates.joinpath(path, module)
|
|
# Check for uninstrumented Java class files to report coverage.
|
|
classfiles = list(
|
|
module_dir.rglob('jacoco-report-classes/*.jar'))
|
|
if classfiles:
|
|
jacoco_report_jars[module] = classfiles
|
|
|
|
# Check for unstripped native binaries to report coverage.
|
|
unstripped_native_binaries.update(
|
|
module_dir.glob('*cov*/unstripped/*'))
|
|
|
|
if jacoco_report_jars:
|
|
_generate_java_coverage_report(jacoco_report_jars, src_paths,
|
|
results_dir, mod_info)
|
|
|
|
if unstripped_native_binaries:
|
|
_generate_native_coverage_report(unstripped_native_binaries,
|
|
results_dir)
|
|
|
|
|
|
def _get_test_deps(test_infos, mod_info):
|
|
"""Gets all dependencies of the TestInfo, including Mainline modules."""
|
|
deps = set()
|
|
|
|
for info in test_infos:
|
|
deps.add(info.raw_test_name)
|
|
deps |= _get_transitive_module_deps(
|
|
mod_info.get_module_info(info.raw_test_name), mod_info, deps)
|
|
|
|
# Include dependencies of any Mainline modules specified as well.
|
|
if not info.mainline_modules:
|
|
continue
|
|
|
|
for mainline_module in info.mainline_modules:
|
|
deps.add(mainline_module)
|
|
deps |= _get_transitive_module_deps(
|
|
mod_info.get_module_info(mainline_module), mod_info, deps)
|
|
|
|
return deps
|
|
|
|
|
|
def _get_transitive_module_deps(info,
|
|
mod_info: module_info.ModuleInfo,
|
|
seen: Set[str]) -> Set[str]:
|
|
"""Gets all dependencies of the module, including .impl versions."""
|
|
deps = set()
|
|
|
|
for dep in info.get(constants.MODULE_DEPENDENCIES, []):
|
|
if dep in seen:
|
|
continue
|
|
|
|
seen.add(dep)
|
|
|
|
dep_info = mod_info.get_module_info(dep)
|
|
|
|
# Mainline modules sometimes depend on `java_sdk_library` modules that
|
|
# generate synthetic build modules ending in `.impl` which do not appear
|
|
# in the ModuleInfo. Strip this suffix to prevent incomplete dependency
|
|
# information when generating coverage reports.
|
|
# TODO(olivernguyen): Reconcile this with
|
|
# ModuleInfo.get_module_dependency(...).
|
|
if not dep_info:
|
|
dep = dep.removesuffix('.impl')
|
|
dep_info = mod_info.get_module_info(dep)
|
|
|
|
if not dep_info:
|
|
continue
|
|
|
|
deps.add(dep)
|
|
deps |= _get_transitive_module_deps(dep_info, mod_info, seen)
|
|
|
|
return deps
|
|
|
|
|
|
def _get_all_src_paths(modules, mod_info):
|
|
"""Gets the set of directories containing any source files from the modules.
|
|
"""
|
|
src_paths = set()
|
|
|
|
for module in modules:
|
|
info = mod_info.get_module_info(module)
|
|
if not info:
|
|
continue
|
|
|
|
# Do not report coverage for test modules.
|
|
if mod_info.is_testable_module(info):
|
|
continue
|
|
|
|
src_paths.update(
|
|
os.path.dirname(f) for f in info.get(constants.MODULE_SRCS, []))
|
|
|
|
src_paths = {p for p in src_paths if not _is_generated_code(p)}
|
|
return src_paths
|
|
|
|
|
|
def _is_generated_code(path):
|
|
return 'soong/.intermediates' in path
|
|
|
|
|
|
def _generate_java_coverage_report(report_jars, src_paths, results_dir,
|
|
mod_info):
|
|
build_top = os.environ.get(constants.ANDROID_BUILD_TOP)
|
|
out_dir = os.path.join(results_dir, 'java_coverage')
|
|
jacoco_files = atest_utils.find_files(results_dir, '*.ec')
|
|
|
|
os.mkdir(out_dir)
|
|
jacoco_lcov = mod_info.get_module_info('jacoco_to_lcov_converter')
|
|
jacoco_lcov = os.path.join(build_top, jacoco_lcov['installed'][0])
|
|
lcov_reports = []
|
|
|
|
for name, classfiles in report_jars.items():
|
|
dest = f'{out_dir}/{name}.info'
|
|
cmd = [jacoco_lcov, '-o', dest]
|
|
for classfile in classfiles:
|
|
cmd.append('-classfiles')
|
|
cmd.append(str(classfile))
|
|
for src_path in src_paths:
|
|
cmd.append('-sourcepath')
|
|
cmd.append(src_path)
|
|
cmd.extend(jacoco_files)
|
|
try:
|
|
subprocess.run(cmd, check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
except subprocess.CalledProcessError as err:
|
|
atest_utils.colorful_print(
|
|
f'Failed to generate coverage for {name}:', constants.RED)
|
|
logging.exception(err.stdout)
|
|
atest_utils.colorful_print(f'Coverage for {name} written to {dest}.',
|
|
constants.GREEN)
|
|
lcov_reports.append(dest)
|
|
|
|
_generate_lcov_report(out_dir, lcov_reports, build_top)
|
|
|
|
|
|
def _generate_native_coverage_report(unstripped_native_binaries, results_dir):
|
|
build_top = os.environ.get(constants.ANDROID_BUILD_TOP)
|
|
out_dir = os.path.join(results_dir, 'native_coverage')
|
|
profdata_files = atest_utils.find_files(results_dir, '*.profdata')
|
|
|
|
os.mkdir(out_dir)
|
|
cmd = ['llvm-cov',
|
|
'show',
|
|
'-format=html',
|
|
f'-output-dir={out_dir}',
|
|
f'-path-equivalence=/proc/self/cwd,{build_top}']
|
|
for profdata in profdata_files:
|
|
cmd.append('--instr-profile')
|
|
cmd.append(profdata)
|
|
for binary in unstripped_native_binaries:
|
|
# Exclude .rsp files. These are files containing the command line used
|
|
# to generate the unstripped binaries, but are stored in the same
|
|
# directory as the actual output binary.
|
|
if not binary.match('*.rsp'):
|
|
cmd.append(f'--object={str(binary)}')
|
|
|
|
try:
|
|
subprocess.run(cmd, check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
atest_utils.colorful_print(f'Native coverage written to {out_dir}.',
|
|
constants.GREEN)
|
|
except subprocess.CalledProcessError as err:
|
|
atest_utils.colorful_print('Failed to generate native code coverage.',
|
|
constants.RED)
|
|
logging.exception(err.stdout)
|
|
|
|
|
|
def _generate_lcov_report(out_dir, reports, root_dir=None):
|
|
cmd = ['genhtml', '-q', '-o', out_dir]
|
|
if root_dir:
|
|
cmd.extend(['-p', root_dir])
|
|
cmd.extend(reports)
|
|
try:
|
|
subprocess.run(cmd, check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
atest_utils.colorful_print(
|
|
f'Code coverage report written to {out_dir}.',
|
|
constants.GREEN)
|
|
atest_utils.colorful_print(
|
|
f'To open, Ctrl+Click on file://{out_dir}/index.html',
|
|
constants.GREEN)
|
|
except subprocess.CalledProcessError as err:
|
|
atest_utils.colorful_print('Failed to generate HTML coverage report.',
|
|
constants.RED)
|
|
logging.exception(err.stdout)
|
|
except FileNotFoundError:
|
|
atest_utils.colorful_print('genhtml is not on the $PATH.',
|
|
constants.RED)
|
|
atest_utils.colorful_print(
|
|
'Run `sudo apt-get install lcov -y` to install this tool.',
|
|
constants.RED)
|