1#!/usr/bin/python2.4 2# 3# 4# Copyright 2008, The Android Open Source Project 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17 18"""Utilities for generating code coverage reports for Android tests.""" 19 20# Python imports 21import glob 22import optparse 23import os 24 25# local imports 26import android_build 27import android_mk 28import coverage_target 29import coverage_targets 30import errors 31import logger 32import run_command 33 34 35class CoverageGenerator(object): 36 """Helper utility for obtaining code coverage results on Android. 37 38 Intended to simplify the process of building,running, and generating code 39 coverage results for a pre-defined set of tests and targets 40 """ 41 42 # path to EMMA host jar, relative to Android build root 43 _EMMA_JAR = os.path.join("external", "emma", "lib", "emma.jar") 44 _TEST_COVERAGE_EXT = "ec" 45 # root path of generated coverage report files, relative to Android build root 46 _COVERAGE_REPORT_PATH = os.path.join("out", "emma") 47 _TARGET_DEF_FILE = "coverage_targets.xml" 48 _CORE_TARGET_PATH = os.path.join("development", "testrunner", 49 _TARGET_DEF_FILE) 50 # vendor glob file path patterns to tests, relative to android 51 # build root 52 _VENDOR_TARGET_PATH = os.path.join("vendor", "*", "tests", "testinfo", 53 _TARGET_DEF_FILE) 54 55 # path to root of target build intermediates 56 _TARGET_INTERMEDIATES_BASE_PATH = os.path.join("out", "target", "common", 57 "obj") 58 59 def __init__(self, adb_interface): 60 self._root_path = android_build.GetTop() 61 self._output_root_path = os.path.join(self._root_path, 62 self._COVERAGE_REPORT_PATH) 63 self._emma_jar_path = os.path.join(self._root_path, self._EMMA_JAR) 64 self._adb = adb_interface 65 self._targets_manifest = self._ReadTargets() 66 67 def ExtractReport(self, 68 test_suite_name, 69 target, 70 device_coverage_path, 71 output_path=None, 72 test_qualifier=None): 73 """Extract runtime coverage data and generate code coverage report. 74 75 Assumes test has just been executed. 76 Args: 77 test_suite_name: name of TestSuite to generate coverage data for 78 target: the CoverageTarget to use as basis for coverage calculation 79 device_coverage_path: location of coverage file on device 80 output_path: path to place output files in. If None will use 81 <android_root_path>/<_COVERAGE_REPORT_PATH>/<target>/<test[-qualifier]> 82 test_qualifier: designates mode test was run with. e.g size=small. 83 If not None, this will be used to customize output_path as shown above. 84 85 Returns: 86 absolute file path string of generated html report file. 87 """ 88 if output_path is None: 89 report_name = test_suite_name 90 if test_qualifier: 91 report_name = report_name + "-" + test_qualifier 92 output_path = os.path.join(self._root_path, 93 self._COVERAGE_REPORT_PATH, 94 target.GetName(), 95 report_name) 96 97 coverage_local_name = "%s.%s" % (report_name, 98 self._TEST_COVERAGE_EXT) 99 coverage_local_path = os.path.join(output_path, 100 coverage_local_name) 101 if self._adb.Pull(device_coverage_path, coverage_local_path): 102 103 report_path = os.path.join(output_path, 104 report_name) 105 return self._GenerateReport(report_path, coverage_local_path, [target], 106 do_src=True) 107 return None 108 109 def _GenerateReport(self, report_path, coverage_file_path, targets, 110 do_src=True): 111 """Generate the code coverage report. 112 113 Args: 114 report_path: absolute file path of output file, without extension 115 coverage_file_path: absolute file path of code coverage result file 116 targets: list of CoverageTargets to use as base for code coverage 117 measurement. 118 do_src: True if generate coverage report with source linked in. 119 Note this will increase size of generated report. 120 121 Returns: 122 absolute file path to generated report file. 123 """ 124 input_metadatas = self._GatherMetadatas(targets) 125 126 if do_src: 127 src_arg = self._GatherSrcs(targets) 128 else: 129 src_arg = "" 130 131 report_file = "%s.html" % report_path 132 cmd1 = ("java -cp %s emma report -r html -in %s %s %s " % 133 (self._emma_jar_path, coverage_file_path, input_metadatas, src_arg)) 134 cmd2 = "-Dreport.html.out.file=%s" % report_file 135 self._RunCmd(cmd1 + cmd2) 136 return report_file 137 138 def _GatherMetadatas(self, targets): 139 """Builds the emma input metadata argument from provided targets. 140 141 Args: 142 targets: list of CoverageTargets 143 144 Returns: 145 input metadata argument string 146 """ 147 input_metadatas = "" 148 for target in targets: 149 input_metadata = os.path.join(self._GetBuildIntermediatePath(target), 150 "coverage.em") 151 input_metadatas += " -in %s" % input_metadata 152 return input_metadatas 153 154 def _GetBuildIntermediatePath(self, target): 155 return os.path.join( 156 self._root_path, self._TARGET_INTERMEDIATES_BASE_PATH, target.GetType(), 157 "%s_intermediates" % target.GetName()) 158 159 def _GatherSrcs(self, targets): 160 """Builds the emma input source path arguments from provided targets. 161 162 Args: 163 targets: list of CoverageTargets 164 Returns: 165 source path arguments string 166 """ 167 src_list = [] 168 for target in targets: 169 target_srcs = target.GetPaths() 170 for path in target_srcs: 171 src_list.append("-sp %s" % os.path.join(self._root_path, path)) 172 return " ".join(src_list) 173 174 def _MergeFiles(self, input_paths, dest_path): 175 """Merges a set of emma coverage files into a consolidated file. 176 177 Args: 178 input_paths: list of string absolute coverage file paths to merge 179 dest_path: absolute file path of destination file 180 """ 181 input_list = [] 182 for input_path in input_paths: 183 input_list.append("-in %s" % input_path) 184 input_args = " ".join(input_list) 185 self._RunCmd("java -cp %s emma merge %s -out %s" % (self._emma_jar_path, 186 input_args, dest_path)) 187 188 def _RunCmd(self, cmd): 189 """Runs and logs the given os command.""" 190 run_command.RunCommand(cmd, return_output=False) 191 192 def _CombineTargetCoverage(self): 193 """Combines all target mode code coverage results. 194 195 Will find all code coverage data files in direct sub-directories of 196 self._output_root_path, and combine them into a single coverage report. 197 Generated report is placed at self._output_root_path/android.html 198 """ 199 coverage_files = self._FindCoverageFiles(self._output_root_path) 200 combined_coverage = os.path.join(self._output_root_path, 201 "android.%s" % self._TEST_COVERAGE_EXT) 202 self._MergeFiles(coverage_files, combined_coverage) 203 report_path = os.path.join(self._output_root_path, "android") 204 # don't link to source, to limit file size 205 self._GenerateReport(report_path, combined_coverage, 206 self._targets_manifest.GetTargets(), do_src=False) 207 208 def _CombineTestCoverage(self): 209 """Consolidates code coverage results for all target result directories.""" 210 target_dirs = os.listdir(self._output_root_path) 211 for target_name in target_dirs: 212 output_path = os.path.join(self._output_root_path, target_name) 213 target = self._targets_manifest.GetTarget(target_name) 214 if os.path.isdir(output_path) and target is not None: 215 coverage_files = self._FindCoverageFiles(output_path) 216 combined_coverage = os.path.join(output_path, "%s.%s" % 217 (target_name, self._TEST_COVERAGE_EXT)) 218 self._MergeFiles(coverage_files, combined_coverage) 219 report_path = os.path.join(output_path, target_name) 220 self._GenerateReport(report_path, combined_coverage, [target]) 221 else: 222 logger.Log("%s is not a valid target directory, skipping" % output_path) 223 224 def _FindCoverageFiles(self, root_path): 225 """Finds all files in <root_path>/*/*.<_TEST_COVERAGE_EXT>. 226 227 Args: 228 root_path: absolute file path string to search from 229 Returns: 230 list of absolute file path strings of coverage files 231 """ 232 file_pattern = os.path.join(root_path, "*", "*.%s" % 233 self._TEST_COVERAGE_EXT) 234 coverage_files = glob.glob(file_pattern) 235 return coverage_files 236 237 def _ReadTargets(self): 238 """Parses the set of coverage target data. 239 240 Returns: 241 a CoverageTargets object that contains set of parsed targets. 242 Raises: 243 AbortError if a fatal error occurred when parsing the target files. 244 """ 245 core_target_path = os.path.join(self._root_path, self._CORE_TARGET_PATH) 246 try: 247 targets = coverage_targets.CoverageTargets() 248 targets.Parse(core_target_path) 249 vendor_targets_pattern = os.path.join(self._root_path, 250 self._VENDOR_TARGET_PATH) 251 target_file_paths = glob.glob(vendor_targets_pattern) 252 for target_file_path in target_file_paths: 253 targets.Parse(target_file_path) 254 return targets 255 except errors.ParseError: 256 raise errors.AbortError 257 258 def TidyOutput(self): 259 """Runs tidy on all generated html files. 260 261 This is needed to the html files can be displayed cleanly on a web server. 262 Assumes tidy is on current PATH. 263 """ 264 logger.Log("Tidying output files") 265 self._TidyDir(self._output_root_path) 266 267 def _TidyDir(self, dir_path): 268 """Recursively tidy all html files in given dir_path.""" 269 html_file_pattern = os.path.join(dir_path, "*.html") 270 html_files_iter = glob.glob(html_file_pattern) 271 for html_file_path in html_files_iter: 272 os.system("tidy -m -errors -quiet %s" % html_file_path) 273 sub_dirs = os.listdir(dir_path) 274 for sub_dir_name in sub_dirs: 275 sub_dir_path = os.path.join(dir_path, sub_dir_name) 276 if os.path.isdir(sub_dir_path): 277 self._TidyDir(sub_dir_path) 278 279 def CombineCoverage(self): 280 """Create combined coverage reports for all targets and tests.""" 281 self._CombineTestCoverage() 282 self._CombineTargetCoverage() 283 284 def GetCoverageTarget(self, name): 285 """Find the CoverageTarget for given name""" 286 target = self._targets_manifest.GetTarget(name) 287 if target is None: 288 msg = ["Error: test references undefined target %s." % name] 289 msg.append(" Ensure target is defined in %s" % self._TARGET_DEF_FILE) 290 raise errors.AbortError(msg) 291 return target 292 293 def GetCoverageTargetForPath(self, path): 294 """Find the CoverageTarget for given file system path""" 295 android_mk_path = os.path.join(path, "Android.mk") 296 if os.path.exists(android_mk_path): 297 android_mk_parser = android_mk.CreateAndroidMK(path) 298 target = coverage_target.CoverageTarget() 299 target.SetBuildPath(os.path.join(path, "src")) 300 target.SetName(android_mk_parser.GetVariable(android_mk_parser.PACKAGE_NAME)) 301 target.SetType("APPS") 302 return target 303 else: 304 msg = "No Android.mk found at %s" % path 305 raise errors.AbortError(msg) 306 307 308def EnableCoverageBuild(): 309 """Enable building an Android target with code coverage instrumentation.""" 310 os.environ["EMMA_INSTRUMENT"] = "true" 311 312def Run(): 313 """Does coverage operations based on command line args.""" 314 # TODO: do we want to support combining coverage for a single target 315 316 try: 317 parser = optparse.OptionParser(usage="usage: %prog --combine-coverage") 318 parser.add_option( 319 "-c", "--combine-coverage", dest="combine_coverage", default=False, 320 action="store_true", help="Combine coverage results stored given " 321 "android root path") 322 parser.add_option( 323 "-t", "--tidy", dest="tidy", default=False, action="store_true", 324 help="Run tidy on all generated html files") 325 326 options, args = parser.parse_args() 327 328 coverage = CoverageGenerator(None) 329 if options.combine_coverage: 330 coverage.CombineCoverage() 331 if options.tidy: 332 coverage.TidyOutput() 333 except errors.AbortError: 334 logger.SilentLog("Exiting due to AbortError") 335 336if __name__ == "__main__": 337 Run() 338