Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright 2015 The Chromium Authors. All rights reserved. | 2 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Generates incremental code coverage reports for Java code in Chromium.""" | 6 """Generates incremental code coverage reports for Java code in Chromium. |
| 7 | 7 |
| 8 Usage: | |
| 9 | |
| 10 build/android/coverage.py -v --out <output file path> --emma-dir | |
| 11 <EMMA file directory> --lines-for-coverage-file | |
| 12 <path to file containing lines for coverage> | |
| 13 | |
| 14 Creates a JSON representation of the overall and file coverage stats and saves | |
| 15 this information to the specified output file. | |
| 16 """ | |
| 17 | |
| 18 import argparse | |
| 8 import collections | 19 import collections |
| 20 import json | |
| 21 import logging | |
| 9 import os | 22 import os |
| 23 import re | |
| 24 import sys | |
| 10 from xml.etree import ElementTree | 25 from xml.etree import ElementTree |
| 11 | 26 |
| 27 from pylib.utils import run_tests_helper | |
| 28 | |
| 12 NOT_EXECUTABLE = -1 | 29 NOT_EXECUTABLE = -1 |
| 13 NOT_COVERED = 0 | 30 NOT_COVERED = 0 |
| 14 COVERED = 1 | 31 COVERED = 1 |
| 15 PARTIALLY_COVERED = 2 | 32 PARTIALLY_COVERED = 2 |
| 16 | 33 |
| 17 # Coverage information about a single line of code. | 34 # Coverage information about a single line of code. |
| 18 LineCoverage = collections.namedtuple( | 35 LineCoverage = collections.namedtuple( |
| 19 'LineCoverage', | 36 'LineCoverage', |
| 20 ['lineno', 'source', 'covered_status', 'fractional_line_coverage']) | 37 ['lineno', 'source', 'covered_status', 'fractional_line_coverage']) |
| 21 | 38 |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 60 1. Line number | 77 1. Line number |
| 61 2. Line of source code | 78 2. Line of source code |
| 62 3. Coverage status (c, z, or p) | 79 3. Coverage status (c, z, or p) |
| 63 4. Fractional coverage value (% out of 100 if PARTIALLY_COVERED) | 80 4. Fractional coverage value (% out of 100 if PARTIALLY_COVERED) |
| 64 """ | 81 """ |
| 65 # Selector to match all <a> elements within the rows that are in the table | 82 # Selector to match all <a> elements within the rows that are in the table |
| 66 # that displays all of the different packages. | 83 # that displays all of the different packages. |
| 67 _XPATH_SELECT_PACKAGE_ELEMENTS = './/BODY/TABLE[4]/TR/TD/A' | 84 _XPATH_SELECT_PACKAGE_ELEMENTS = './/BODY/TABLE[4]/TR/TD/A' |
| 68 | 85 |
| 69 # Selector to match all <a> elements within the rows that are in the table | 86 # Selector to match all <a> elements within the rows that are in the table |
| 70 # that displays all of the different packages within a class. | 87 # that displays all of the different classes within a package. |
| 71 _XPATH_SELECT_CLASS_ELEMENTS = './/BODY/TABLE[3]/TR/TD/A' | 88 _XPATH_SELECT_CLASS_ELEMENTS = './/BODY/TABLE[3]/TR/TD/A' |
| 72 | 89 |
| 73 # Selector to match all <tr> elements within the table containing Java source | 90 # Selector to match all <tr> elements within the table containing Java source |
| 74 # code in an EMMA HTML file. | 91 # code in an EMMA HTML file. |
| 75 _XPATH_SELECT_LOC = './/BODY/TABLE[4]/TR' | 92 _XPATH_SELECT_LOC = './/BODY/TABLE[4]/TR' |
| 76 | 93 |
| 77 # Children of HTML elements are represented as a list in ElementTree. These | 94 # Children of HTML elements are represented as a list in ElementTree. These |
| 78 # constants represent list indices corresponding to relevant child elements. | 95 # constants represent list indices corresponding to relevant child elements. |
| 79 | 96 |
| 80 # Child 1 contains percentage covered for a line. | 97 # Child 1 contains percentage covered for a line. |
| (...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 151 | 168 |
| 152 Returns: | 169 Returns: |
| 153 A dict mapping string representation of Java packages (with class | 170 A dict mapping string representation of Java packages (with class |
| 154 names appended) to the corresponding file paths of EMMA HTML files. | 171 names appended) to the corresponding file paths of EMMA HTML files. |
| 155 """ | 172 """ |
| 156 # These <a> elements contain each package name and the path of the file | 173 # These <a> elements contain each package name and the path of the file |
| 157 # where all classes within said package are listed. | 174 # where all classes within said package are listed. |
| 158 package_link_elements = self._FindElements( | 175 package_link_elements = self._FindElements( |
| 159 self._index_path, self._XPATH_SELECT_PACKAGE_ELEMENTS) | 176 self._index_path, self._XPATH_SELECT_PACKAGE_ELEMENTS) |
| 160 # Maps file path of package directory (EMMA generated) to package name. | 177 # Maps file path of package directory (EMMA generated) to package name. |
| 161 # Ex. emma_dir/f.html: org.chromium.chrome. | 178 # Example: emma_dir/f.html: org.chromium.chrome. |
| 162 package_links = { | 179 package_links = { |
| 163 os.path.join(self._base_dir, link.attrib['HREF']): link.text | 180 os.path.join(self._base_dir, link.attrib['HREF']): link.text |
| 164 for link in package_link_elements if 'HREF' in link.attrib | 181 for link in package_link_elements if 'HREF' in link.attrib |
| 165 } | 182 } |
| 166 | 183 |
| 167 package_to_emma = {} | 184 package_to_emma = {} |
| 168 for package_emma_file_path, package_name in package_links.iteritems(): | 185 for package_emma_file_path, package_name in package_links.iteritems(): |
| 169 # These <a> elements contain each class name in the current package and | 186 # These <a> elements contain each class name in the current package and |
| 170 # the path of the file where the coverage info is stored for each class. | 187 # the path of the file where the coverage info is stored for each class. |
| 171 coverage_file_link_elements = self._FindElements( | 188 coverage_file_link_elements = self._FindElements( |
| 172 package_emma_file_path, self._XPATH_SELECT_CLASS_ELEMENTS) | 189 package_emma_file_path, self._XPATH_SELECT_CLASS_ELEMENTS) |
| 173 | 190 |
| 174 for coverage_file_element in coverage_file_link_elements: | 191 for class_name_element in coverage_file_link_elements: |
| 175 emma_coverage_file_path = os.path.join( | 192 emma_coverage_file_path = os.path.join( |
| 176 self._emma_files_path, coverage_file_element.attrib['HREF']) | 193 self._emma_files_path, class_name_element.attrib['HREF']) |
| 177 full_package_name = '%s.%s' % (package_name, coverage_file_element.text) | 194 full_package_name = '%s.%s' % (package_name, class_name_element.text) |
| 178 package_to_emma[full_package_name] = emma_coverage_file_path | 195 package_to_emma[full_package_name] = emma_coverage_file_path |
| 179 | 196 |
| 180 return package_to_emma | 197 return package_to_emma |
| 181 | 198 |
| 182 def _FindElements(self, file_path, xpath_selector): | 199 def _FindElements(self, file_path, xpath_selector): |
| 183 """Reads a HTML file and performs an XPath match. | 200 """Reads a HTML file and performs an XPath match. |
| 184 | 201 |
| 185 Args: | 202 Args: |
| 186 file_path: String representing the path to the HTML file. | 203 file_path: String representing the path to the HTML file. |
| 187 xpath_selector: String representing xpath search pattern. | 204 xpath_selector: String representing xpath search pattern. |
| 188 | 205 |
| 189 Returns: | 206 Returns: |
| 190 A list of ElementTree.Elements matching the given XPath selector. | 207 A list of ElementTree.Elements matching the given XPath selector. |
| 191 Returns an empty list if there is no match. | 208 Returns an empty list if there is no match. |
| 192 """ | 209 """ |
| 193 with open(file_path) as f: | 210 with open(file_path) as f: |
| 194 file_contents = f.read().decode('ISO-8859-1').encode('UTF-8') | 211 file_contents = f.read().decode('ISO-8859-1').encode('UTF-8') |
| 195 root = ElementTree.fromstring(file_contents) | 212 root = ElementTree.fromstring(file_contents) |
| 196 return root.findall(xpath_selector) | 213 return root.findall(xpath_selector) |
| 214 | |
| 215 | |
| 216 class _EmmaCoverageStats(object): | |
| 217 """Computes code coverage stats for Java code using the coverage tool EMMA. | |
| 218 | |
| 219 This class provides an API that allows users to capture absolute code coverage | |
| 220 and code coverage on a subset of lines for each Java source file. Coverage | |
| 221 reports are generated in JSON format. | |
| 222 """ | |
| 223 # Regular expression to get package name from Java package statement. | |
| 224 RE_PACKAGE_MATCH_GROUP = 'package' | |
| 225 RE_PACKAGE = re.compile(r'package (?P<%s>[\w.]*);' % RE_PACKAGE_MATCH_GROUP) | |
| 226 | |
| 227 def __init__(self, emma_file_base_dir, files_for_coverage): | |
| 228 """Initialize _EmmaCoverageStats. | |
| 229 | |
| 230 Args: | |
| 231 emma_file_base_dir: String representing the path to the base directory | |
| 232 where EMMA HTML coverage files are stored, i.e. parent of index.html. | |
| 233 files_for_coverage: A list of Java source code file paths to get EMMA | |
| 234 coverage for. | |
| 235 """ | |
| 236 self._emma_parser = _EmmaHtmlParser(emma_file_base_dir) | |
| 237 self._source_to_emma = self._GetSourceFileToEmmaFileDict(files_for_coverage) | |
| 238 | |
| 239 def GetCoverageDict(self, lines_for_coverage): | |
| 240 """Returns a dict containing detailed coverage information. | |
| 241 | |
| 242 Gets detailed coverage stats for each file specified in the | |
| 243 |lines_for_coverage| dict and the total incremental number of lines covered | |
| 244 and executable for all files in |lines_for_coverage|. | |
| 245 | |
| 246 Args: | |
| 247 lines_for_coverage: A dict mapping Java source file paths to lists of line | |
| 248 numbers. | |
| 249 | |
| 250 Returns: | |
| 251 A dict containing coverage stats for the given dict of files and lines. | |
| 252 Contains absolute coverage stats for each file, coverage stats for each | |
| 253 file's lines specified in |lines_for_coverage|, line by line coverage | |
| 254 for each file, and overall coverage stats for the lines specified in | |
| 255 |lines_for_coverage|. | |
| 256 """ | |
| 257 file_coverage = {} | |
| 258 for file_path, line_numbers in lines_for_coverage.iteritems(): | |
| 259 file_coverage[file_path] = self.GetCoverageDictForFile( | |
| 260 file_path, line_numbers) | |
| 261 | |
| 262 covered_statuses = [s['incremental'] for s in file_coverage.itervalues()] | |
| 263 num_covered_lines = sum(s['covered'] for s in covered_statuses) | |
| 264 num_total_lines = sum(s['total'] for s in covered_statuses) | |
| 265 return { | |
| 266 'files': file_coverage, | |
| 267 'patch': { | |
| 268 'incremental': { | |
| 269 'covered': num_covered_lines, | |
| 270 'total': num_total_lines | |
| 271 } | |
| 272 } | |
| 273 } | |
| 274 | |
| 275 def GetCoverageDictForFile(self, file_path, line_numbers): | |
| 276 """Returns a dict containing detailed coverage info for the given file. | |
| 277 | |
| 278 Args: | |
| 279 file_path: The path to the Java source file that we want to create the | |
| 280 coverage dict for. | |
| 281 line_numbers: A list of integer line numbers to retrieve additional stats | |
| 282 for. | |
| 283 | |
| 284 Returns: | |
| 285 A dict containing absolute, incremental, and line by line coverage for | |
| 286 a file. | |
| 287 """ | |
| 288 total_line_coverage = self._GetLineCoverageForFile(file_path) | |
| 289 incremental_line_coverage = [line for line in total_line_coverage | |
| 290 if line.lineno in line_numbers] | |
| 291 line_by_line_coverage = [ | |
| 292 { | |
| 293 'line': line.source, | |
| 294 'coverage': line.covered_status, | |
| 295 'changed': line.lineno in line_numbers, | |
| 296 } | |
| 297 for line in total_line_coverage | |
| 298 ] | |
| 299 total_covered_lines, total_lines = ( | |
| 300 self.GetSummaryStatsForLines(total_line_coverage)) | |
| 301 incremental_covered_lines, incremental_total_lines = ( | |
| 302 self.GetSummaryStatsForLines(incremental_line_coverage)) | |
| 303 | |
| 304 file_coverage_stats = { | |
| 305 'absolute': { | |
| 306 'covered': total_covered_lines, | |
| 307 'total': total_lines | |
| 308 }, | |
| 309 'incremental': { | |
| 310 'covered': incremental_covered_lines, | |
| 311 'total': incremental_total_lines | |
| 312 }, | |
| 313 'source': line_by_line_coverage, | |
| 314 } | |
| 315 return file_coverage_stats | |
| 316 | |
| 317 def GetSummaryStatsForLines(self, line_coverage): | |
| 318 """Gets summary stats for a given list of LineCoverage objects. | |
| 319 | |
| 320 Args: | |
| 321 line_coverage: A list of LineCoverage objects. | |
| 322 | |
| 323 Returns: | |
| 324 A tuple containing the number of lines that are covered and the total | |
| 325 number of lines that are executable, respectively | |
| 326 """ | |
| 327 partially_covered_sum = 0 | |
| 328 covered_status_totals = {COVERED: 0, NOT_COVERED: 0, PARTIALLY_COVERED: 0} | |
| 329 for line in line_coverage: | |
| 330 status = line.covered_status | |
| 331 if status == NOT_EXECUTABLE: | |
| 332 continue | |
| 333 covered_status_totals[status] += 1 | |
| 334 if status == PARTIALLY_COVERED: | |
| 335 partially_covered_sum += line.fractional_line_coverage | |
| 336 | |
| 337 total_covered = covered_status_totals[COVERED] + partially_covered_sum | |
| 338 total_lines = sum(covered_status_totals.values()) | |
| 339 return total_covered, total_lines | |
| 340 | |
| 341 def _GetLineCoverageForFile(self, file_path): | |
| 342 """Gets a list of LineCoverage objects corresponding to the given file path. | |
| 343 | |
| 344 Args: | |
| 345 file_path: String representing the path to the Java source file. | |
| 346 | |
| 347 Returns: | |
| 348 A list of LineCoverage objects, or None if there is no EMMA file | |
| 349 for the given Java source file. | |
| 350 """ | |
| 351 if file_path in self._source_to_emma: | |
| 352 emma_file = self._source_to_emma[file_path] | |
| 353 return self._emma_parser.GetLineCoverage(emma_file) | |
| 354 else: | |
| 355 logging.warning( | |
| 356 'No code coverage data for %s, skipping.', file_path) | |
| 357 return None | |
| 358 | |
| 359 def _GetSourceFileToEmmaFileDict(self, files): | |
| 360 """Gets a dict used to correlate Java source files with EMMA HTML files. | |
| 361 | |
| 362 This method gathers the information needed to correlate EMMA HTML | |
| 363 files with Java source files. EMMA XML and plain text reports do not provide | |
| 364 line by line coverage data, so HTML reports must be used instead. | |
| 365 Unfortunately, the HTML files that are created are given garbage names | |
| 366 (i.e 1.html) so we need to manually correlate EMMA HTML files | |
| 367 with the original Java source files. | |
| 368 | |
| 369 Args: | |
| 370 files: A list of file names for which coverage information is desired. | |
| 371 | |
| 372 Returns: | |
| 373 A dict mapping Java source file paths to EMMA HTML file paths. | |
| 374 """ | |
| 375 # Maps Java source file paths to package names. | |
| 376 # Example: /usr/code/file.java -> org.chromium.file.java. | |
| 377 source_to_package = {} | |
| 378 for file_path in files: | |
| 379 package = self.GetPackageNameFromFile(file_path) | |
| 380 if package: | |
| 381 source_to_package[file_path] = package | |
| 382 else: | |
| 383 logging.warning('Skipping %s because it doesn\'t have a package ' | |
|
jbudorick
2015/07/31 20:21:14
nit: use double quotes for strings containing sing
estevenson1
2015/07/31 21:52:16
Done.
| |
| 384 'statement.', file_path) | |
| 385 | |
| 386 # Maps package names to EMMA report HTML files. | |
| 387 # Example: org.chromium.file.java -> out/coverage/1a.html. | |
| 388 package_to_emma = self._emma_parser.GetPackageNameToEmmaFileDict() | |
| 389 # Finally, we have a dict mapping Java file paths to EMMA report files. | |
| 390 # Example: /usr/code/file.java -> out/coverage/1a.html. | |
| 391 source_to_emma = {source: package_to_emma.get(package) | |
| 392 for source, package in source_to_package.iteritems()} | |
| 393 return source_to_emma | |
| 394 | |
| 395 @staticmethod | |
| 396 def NeedsCoverage(file_path): | |
| 397 """Checks to see if the file needs to be analyzed for code coverage. | |
| 398 | |
| 399 Args: | |
| 400 file_path: A string representing path to the file. | |
| 401 | |
| 402 Returns: | |
| 403 True for Java files that exist, False for all others. | |
| 404 """ | |
| 405 if os.path.splitext(file_path)[1] == '.java' and os.path.exists(file_path): | |
| 406 return True | |
| 407 else: | |
| 408 logging.debug( | |
| 409 'Skipping file %s, cannot compute code coverage.', file_path) | |
| 410 return False | |
| 411 | |
| 412 @staticmethod | |
| 413 def GetPackageNameFromFile(file_path): | |
| 414 """Gets the full package name including the file name for a given file path. | |
| 415 | |
| 416 Args: | |
| 417 file_path: String representing the path to the Java source file. | |
| 418 | |
| 419 Returns: | |
| 420 A string representing the full package name with file name appended or | |
| 421 None if there is no package statement in the file. | |
| 422 """ | |
| 423 with open(file_path) as f: | |
| 424 file_content = f.read() | |
| 425 package_match = re.search(_EmmaCoverageStats.RE_PACKAGE, file_content) | |
| 426 if package_match: | |
| 427 package = package_match.group(_EmmaCoverageStats.RE_PACKAGE_MATCH_GROUP) | |
| 428 file_name = os.path.basename(file_path) | |
| 429 return '%s.%s' % (package, file_name) | |
| 430 else: | |
| 431 return None | |
| 432 | |
| 433 | |
| 434 def GenerateCoverageReport(line_coverage_file, out_file_path, coverage_dir): | |
| 435 """Generates a coverage report for a given set of lines. | |
| 436 | |
| 437 Writes the results of the coverage analysis to the file specified by | |
| 438 |out_file_path|. | |
| 439 | |
| 440 Args: | |
| 441 line_coverage_file: The path to a file which contains a dict mapping file | |
| 442 names to lists of line numbers. Example: {file1: [1, 2, 3], ...} means | |
| 443 that we should compute coverage information on lines 1 - 3 for file1. | |
| 444 out_file_path: A string representing the location to write the JSON report. | |
| 445 coverage_dir: A string representing the file path where the EMMA | |
| 446 HTML coverage files are located (i.e. folder where index.html is located). | |
| 447 """ | |
| 448 with open(line_coverage_file) as f: | |
| 449 potential_files_for_coverage = json.load(f) | |
| 450 files_for_coverage = {f: lines | |
| 451 for f, lines in potential_files_for_coverage.iteritems() | |
| 452 if _EmmaCoverageStats.NeedsCoverage(f)} | |
| 453 if not files_for_coverage: | |
| 454 logging.info('No Java files requiring coverage were included in %s.', | |
| 455 line_coverage_file) | |
| 456 return | |
| 457 | |
| 458 code_coverage = _EmmaCoverageStats(coverage_dir, files_for_coverage.keys()) | |
| 459 coverage_results = code_coverage.GetCoverageDictForAllFiles( | |
|
jbudorick
2015/07/31 20:21:14
This function doesn't seem to be in this CL...?
estevenson1
2015/07/31 21:52:16
Oops, was supposed to be GetCoverageDict.
| |
| 460 files_for_coverage) | |
| 461 | |
| 462 with open(out_file_path, 'w+') as out_status_file: | |
| 463 json.dump(coverage_results, out_status_file) | |
| 464 | |
| 465 | |
| 466 def main(): | |
| 467 argparser = argparse.ArgumentParser() | |
| 468 argparser.add_argument('--out', required=True, type=str, | |
| 469 help='Report output file path.') | |
| 470 argparser.add_argument('--emma-dir', required=True, type=str, | |
| 471 help='EMMA HTML report directory.') | |
| 472 argparser.add_argument('--lines-for-coverage-file', required=True, type=str, | |
| 473 help='File containing a JSON object. Should contain a ' | |
| 474 'dict mapping file names to lists of line numbers of ' | |
| 475 'code for which coverage information is desired.') | |
| 476 argparser.add_argument('-v', '--verbose', action='count', | |
| 477 help='Print verbose log information.') | |
| 478 args = argparser.parse_args() | |
| 479 run_tests_helper.SetLogLevel(args.verbose) | |
| 480 GenerateCoverageReport(args.lines_for_coverage, args.out, args.emma_dir) | |
| 481 | |
| 482 | |
| 483 if __name__ == '__main__': | |
| 484 sys.exit(main()) | |
| OLD | NEW |