1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
|
#!/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 android_mk
import coverage_target
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 = "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("target", "common",
"obj")
def __init__(self, adb_interface):
self._root_path = android_build.GetTop()
self._out_path = android_build.GetOut()
self._output_root_path = os.path.join(self._out_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_name,
target,
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_name: name of TestSuite to generate coverage data for
target: the CoverageTarget to use as basis for coverage calculation
device_coverage_path: location of coverage file on device
output_path: path to place output files in. If None will use
<android_out_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_name
if test_qualifier:
report_name = report_name + "-" + test_qualifier
output_path = os.path.join(self._out_path,
self._COVERAGE_REPORT_PATH,
target.GetName(),
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)
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._out_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 GetCoverageTarget(self, name):
"""Find the CoverageTarget for given name"""
target = self._targets_manifest.GetTarget(name)
if target is None:
msg = ["Error: test references undefined target %s." % name]
msg.append(" Ensure target is defined in %s" % self._TARGET_DEF_FILE)
raise errors.AbortError(msg)
return target
def GetCoverageTargetForPath(self, path):
"""Find the CoverageTarget for given file system path"""
android_mk_path = os.path.join(path, "Android.mk")
if os.path.exists(android_mk_path):
android_mk_parser = android_mk.CreateAndroidMK(path)
target = coverage_target.CoverageTarget()
target.SetBuildPath(os.path.join(path, "src"))
target.SetName(android_mk_parser.GetVariable(android_mk_parser.PACKAGE_NAME))
target.SetType("APPS")
return target
else:
msg = "No Android.mk found at %s" % path
raise errors.AbortError(msg)
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()
|