Stop using 'adb sync' in runtest. It was unreliable even when working with full platform builds, and doesn't work at all for unbundled apps. Instead, use output from build command to find produced artifacts and use 'adb install' where possible. However, note that this approach won't sync previously built artifacts to device. Also adjust to build system support for code coverage. Its no longer required to build libcore to get code coverage. Change-Id: I9c5d37897c9570d2d29db3ec82f5c53e60a8f485
318 lines
12 KiB
Python
Executable File
318 lines
12 KiB
Python
Executable File
#!/usr/bin/python2.4
|
|
#
|
|
#
|
|
# Copyright 2008, 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.
|
|
|
|
"""Utilities for generating code coverage reports for Android tests."""
|
|
|
|
# Python imports
|
|
import glob
|
|
import optparse
|
|
import os
|
|
|
|
# local imports
|
|
import android_build
|
|
import coverage_targets
|
|
import errors
|
|
import logger
|
|
import run_command
|
|
|
|
|
|
class CoverageGenerator(object):
|
|
"""Helper utility for obtaining code coverage results on Android.
|
|
|
|
Intended to simplify the process of building,running, and generating code
|
|
coverage results for a pre-defined set of tests and targets
|
|
"""
|
|
|
|
# path to EMMA host jar, relative to Android build root
|
|
_EMMA_JAR = os.path.join("external", "emma", "lib", "emma.jar")
|
|
_TEST_COVERAGE_EXT = "ec"
|
|
# root path of generated coverage report files, relative to Android build root
|
|
_COVERAGE_REPORT_PATH = os.path.join("out", "emma")
|
|
_TARGET_DEF_FILE = "coverage_targets.xml"
|
|
_CORE_TARGET_PATH = os.path.join("development", "testrunner",
|
|
_TARGET_DEF_FILE)
|
|
# vendor glob file path patterns to tests, relative to android
|
|
# build root
|
|
_VENDOR_TARGET_PATH = os.path.join("vendor", "*", "tests", "testinfo",
|
|
_TARGET_DEF_FILE)
|
|
|
|
# path to root of target build intermediates
|
|
_TARGET_INTERMEDIATES_BASE_PATH = os.path.join("out", "target", "common",
|
|
"obj")
|
|
|
|
def __init__(self, adb_interface):
|
|
self._root_path = android_build.GetTop()
|
|
self._output_root_path = os.path.join(self._root_path,
|
|
self._COVERAGE_REPORT_PATH)
|
|
self._emma_jar_path = os.path.join(self._root_path, self._EMMA_JAR)
|
|
self._adb = adb_interface
|
|
self._targets_manifest = self._ReadTargets()
|
|
|
|
def ExtractReport(self, test_suite,
|
|
device_coverage_path,
|
|
output_path=None,
|
|
test_qualifier=None):
|
|
"""Extract runtime coverage data and generate code coverage report.
|
|
|
|
Assumes test has just been executed.
|
|
Args:
|
|
test_suite: TestSuite to generate coverage data for
|
|
device_coverage_path: location of coverage file on device
|
|
output_path: path to place output files in. If None will use
|
|
<android_root_path>/<_COVERAGE_REPORT_PATH>/<target>/<test[-qualifier]>
|
|
test_qualifier: designates mode test was run with. e.g size=small.
|
|
If not None, this will be used to customize output_path as shown above.
|
|
|
|
Returns:
|
|
absolute file path string of generated html report file.
|
|
"""
|
|
if output_path is None:
|
|
report_name = test_suite.GetName()
|
|
if test_qualifier:
|
|
report_name = report_name + "-" + test_qualifier
|
|
output_path = os.path.join(self._root_path,
|
|
self._COVERAGE_REPORT_PATH,
|
|
test_suite.GetTargetName(),
|
|
report_name)
|
|
|
|
coverage_local_name = "%s.%s" % (report_name,
|
|
self._TEST_COVERAGE_EXT)
|
|
coverage_local_path = os.path.join(output_path,
|
|
coverage_local_name)
|
|
if self._adb.Pull(device_coverage_path, coverage_local_path):
|
|
|
|
report_path = os.path.join(output_path,
|
|
report_name)
|
|
target = self._targets_manifest.GetTarget(test_suite.GetTargetName())
|
|
if target is None:
|
|
msg = ["Error: test %s references undefined target %s."
|
|
% (test_suite.GetName(), test_suite.GetTargetName())]
|
|
msg.append(" Ensure target is defined in %s" % self._TARGET_DEF_FILE)
|
|
logger.Log("".join(msg))
|
|
else:
|
|
return self._GenerateReport(report_path, coverage_local_path, [target],
|
|
do_src=True)
|
|
return None
|
|
|
|
def _GenerateReport(self, report_path, coverage_file_path, targets,
|
|
do_src=True):
|
|
"""Generate the code coverage report.
|
|
|
|
Args:
|
|
report_path: absolute file path of output file, without extension
|
|
coverage_file_path: absolute file path of code coverage result file
|
|
targets: list of CoverageTargets to use as base for code coverage
|
|
measurement.
|
|
do_src: True if generate coverage report with source linked in.
|
|
Note this will increase size of generated report.
|
|
|
|
Returns:
|
|
absolute file path to generated report file.
|
|
"""
|
|
input_metadatas = self._GatherMetadatas(targets)
|
|
|
|
if do_src:
|
|
src_arg = self._GatherSrcs(targets)
|
|
else:
|
|
src_arg = ""
|
|
|
|
report_file = "%s.html" % report_path
|
|
cmd1 = ("java -cp %s emma report -r html -in %s %s %s " %
|
|
(self._emma_jar_path, coverage_file_path, input_metadatas, src_arg))
|
|
cmd2 = "-Dreport.html.out.file=%s" % report_file
|
|
self._RunCmd(cmd1 + cmd2)
|
|
return report_file
|
|
|
|
def _GatherMetadatas(self, targets):
|
|
"""Builds the emma input metadata argument from provided targets.
|
|
|
|
Args:
|
|
targets: list of CoverageTargets
|
|
|
|
Returns:
|
|
input metadata argument string
|
|
"""
|
|
input_metadatas = ""
|
|
for target in targets:
|
|
input_metadata = os.path.join(self._GetBuildIntermediatePath(target),
|
|
"coverage.em")
|
|
input_metadatas += " -in %s" % input_metadata
|
|
return input_metadatas
|
|
|
|
def _GetBuildIntermediatePath(self, target):
|
|
return os.path.join(
|
|
self._root_path, self._TARGET_INTERMEDIATES_BASE_PATH, target.GetType(),
|
|
"%s_intermediates" % target.GetName())
|
|
|
|
def _GatherSrcs(self, targets):
|
|
"""Builds the emma input source path arguments from provided targets.
|
|
|
|
Args:
|
|
targets: list of CoverageTargets
|
|
Returns:
|
|
source path arguments string
|
|
"""
|
|
src_list = []
|
|
for target in targets:
|
|
target_srcs = target.GetPaths()
|
|
for path in target_srcs:
|
|
src_list.append("-sp %s" % os.path.join(self._root_path, path))
|
|
return " ".join(src_list)
|
|
|
|
def _MergeFiles(self, input_paths, dest_path):
|
|
"""Merges a set of emma coverage files into a consolidated file.
|
|
|
|
Args:
|
|
input_paths: list of string absolute coverage file paths to merge
|
|
dest_path: absolute file path of destination file
|
|
"""
|
|
input_list = []
|
|
for input_path in input_paths:
|
|
input_list.append("-in %s" % input_path)
|
|
input_args = " ".join(input_list)
|
|
self._RunCmd("java -cp %s emma merge %s -out %s" % (self._emma_jar_path,
|
|
input_args, dest_path))
|
|
|
|
def _RunCmd(self, cmd):
|
|
"""Runs and logs the given os command."""
|
|
run_command.RunCommand(cmd, return_output=False)
|
|
|
|
def _CombineTargetCoverage(self):
|
|
"""Combines all target mode code coverage results.
|
|
|
|
Will find all code coverage data files in direct sub-directories of
|
|
self._output_root_path, and combine them into a single coverage report.
|
|
Generated report is placed at self._output_root_path/android.html
|
|
"""
|
|
coverage_files = self._FindCoverageFiles(self._output_root_path)
|
|
combined_coverage = os.path.join(self._output_root_path,
|
|
"android.%s" % self._TEST_COVERAGE_EXT)
|
|
self._MergeFiles(coverage_files, combined_coverage)
|
|
report_path = os.path.join(self._output_root_path, "android")
|
|
# don't link to source, to limit file size
|
|
self._GenerateReport(report_path, combined_coverage,
|
|
self._targets_manifest.GetTargets(), do_src=False)
|
|
|
|
def _CombineTestCoverage(self):
|
|
"""Consolidates code coverage results for all target result directories."""
|
|
target_dirs = os.listdir(self._output_root_path)
|
|
for target_name in target_dirs:
|
|
output_path = os.path.join(self._output_root_path, target_name)
|
|
target = self._targets_manifest.GetTarget(target_name)
|
|
if os.path.isdir(output_path) and target is not None:
|
|
coverage_files = self._FindCoverageFiles(output_path)
|
|
combined_coverage = os.path.join(output_path, "%s.%s" %
|
|
(target_name, self._TEST_COVERAGE_EXT))
|
|
self._MergeFiles(coverage_files, combined_coverage)
|
|
report_path = os.path.join(output_path, target_name)
|
|
self._GenerateReport(report_path, combined_coverage, [target])
|
|
else:
|
|
logger.Log("%s is not a valid target directory, skipping" % output_path)
|
|
|
|
def _FindCoverageFiles(self, root_path):
|
|
"""Finds all files in <root_path>/*/*.<_TEST_COVERAGE_EXT>.
|
|
|
|
Args:
|
|
root_path: absolute file path string to search from
|
|
Returns:
|
|
list of absolute file path strings of coverage files
|
|
"""
|
|
file_pattern = os.path.join(root_path, "*", "*.%s" %
|
|
self._TEST_COVERAGE_EXT)
|
|
coverage_files = glob.glob(file_pattern)
|
|
return coverage_files
|
|
|
|
def _ReadTargets(self):
|
|
"""Parses the set of coverage target data.
|
|
|
|
Returns:
|
|
a CoverageTargets object that contains set of parsed targets.
|
|
Raises:
|
|
AbortError if a fatal error occurred when parsing the target files.
|
|
"""
|
|
core_target_path = os.path.join(self._root_path, self._CORE_TARGET_PATH)
|
|
try:
|
|
targets = coverage_targets.CoverageTargets()
|
|
targets.Parse(core_target_path)
|
|
vendor_targets_pattern = os.path.join(self._root_path,
|
|
self._VENDOR_TARGET_PATH)
|
|
target_file_paths = glob.glob(vendor_targets_pattern)
|
|
for target_file_path in target_file_paths:
|
|
targets.Parse(target_file_path)
|
|
return targets
|
|
except errors.ParseError:
|
|
raise errors.AbortError
|
|
|
|
def TidyOutput(self):
|
|
"""Runs tidy on all generated html files.
|
|
|
|
This is needed to the html files can be displayed cleanly on a web server.
|
|
Assumes tidy is on current PATH.
|
|
"""
|
|
logger.Log("Tidying output files")
|
|
self._TidyDir(self._output_root_path)
|
|
|
|
def _TidyDir(self, dir_path):
|
|
"""Recursively tidy all html files in given dir_path."""
|
|
html_file_pattern = os.path.join(dir_path, "*.html")
|
|
html_files_iter = glob.glob(html_file_pattern)
|
|
for html_file_path in html_files_iter:
|
|
os.system("tidy -m -errors -quiet %s" % html_file_path)
|
|
sub_dirs = os.listdir(dir_path)
|
|
for sub_dir_name in sub_dirs:
|
|
sub_dir_path = os.path.join(dir_path, sub_dir_name)
|
|
if os.path.isdir(sub_dir_path):
|
|
self._TidyDir(sub_dir_path)
|
|
|
|
def CombineCoverage(self):
|
|
"""Create combined coverage reports for all targets and tests."""
|
|
self._CombineTestCoverage()
|
|
self._CombineTargetCoverage()
|
|
|
|
|
|
def EnableCoverageBuild():
|
|
"""Enable building an Android target with code coverage instrumentation."""
|
|
os.environ["EMMA_INSTRUMENT"] = "true"
|
|
|
|
|
|
def Run():
|
|
"""Does coverage operations based on command line args."""
|
|
# TODO: do we want to support combining coverage for a single target
|
|
|
|
try:
|
|
parser = optparse.OptionParser(usage="usage: %prog --combine-coverage")
|
|
parser.add_option(
|
|
"-c", "--combine-coverage", dest="combine_coverage", default=False,
|
|
action="store_true", help="Combine coverage results stored given "
|
|
"android root path")
|
|
parser.add_option(
|
|
"-t", "--tidy", dest="tidy", default=False, action="store_true",
|
|
help="Run tidy on all generated html files")
|
|
|
|
options, args = parser.parse_args()
|
|
|
|
coverage = CoverageGenerator(None)
|
|
if options.combine_coverage:
|
|
coverage.CombineCoverage()
|
|
if options.tidy:
|
|
coverage.TidyOutput()
|
|
except errors.AbortError:
|
|
logger.SilentLog("Exiting due to AbortError")
|
|
|
|
if __name__ == "__main__":
|
|
Run()
|