OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python2.4 |
| 2 # |
| 3 # Copyright 2009, Google Inc. |
| 4 # All rights reserved. |
| 5 # |
| 6 # Redistribution and use in source and binary forms, with or without |
| 7 # modification, are permitted provided that the following conditions are |
| 8 # met: |
| 9 # |
| 10 # * Redistributions of source code must retain the above copyright |
| 11 # notice, this list of conditions and the following disclaimer. |
| 12 # * Redistributions in binary form must reproduce the above |
| 13 # copyright notice, this list of conditions and the following disclaimer |
| 14 # in the documentation and/or other materials provided with the |
| 15 # distribution. |
| 16 # * Neither the name of Google Inc. nor the names of its |
| 17 # contributors may be used to endorse or promote products derived from |
| 18 # this software without specific prior written permission. |
| 19 # |
| 20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 24 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 25 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 26 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 27 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 28 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 30 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 31 |
| 32 """Crocodile - compute coverage numbers for Chrome coverage dashboard.""" |
| 33 |
| 34 import os |
| 35 import re |
| 36 import sys |
| 37 from optparse import OptionParser |
| 38 |
| 39 |
| 40 class CoverageError(Exception): |
| 41 """Coverage error.""" |
| 42 |
| 43 class CoverageStatError(CoverageError): |
| 44 """Error evaluating coverage stat.""" |
| 45 |
| 46 #------------------------------------------------------------------------------ |
| 47 |
| 48 |
| 49 class CoverageStats(dict): |
| 50 """Coverage statistics.""" |
| 51 |
| 52 def Add(self, coverage_stats): |
| 53 """Adds a contribution from another coverage stats dict. |
| 54 |
| 55 Args: |
| 56 coverage_stats: Statistics to add to this one. |
| 57 """ |
| 58 for k, v in coverage_stats.iteritems(): |
| 59 if k in self: |
| 60 self[k] = self[k] + v |
| 61 else: |
| 62 self[k] = v |
| 63 |
| 64 #------------------------------------------------------------------------------ |
| 65 |
| 66 |
| 67 class CoveredFile(object): |
| 68 """Information about a single covered file.""" |
| 69 |
| 70 def __init__(self, filename, group, language): |
| 71 """Constructor. |
| 72 |
| 73 Args: |
| 74 filename: Full path to file, '/'-delimited. |
| 75 group: Group file belongs to. |
| 76 language: Language for file. |
| 77 """ |
| 78 self.filename = filename |
| 79 self.group = group |
| 80 self.language = language |
| 81 |
| 82 # No coverage data for file yet |
| 83 self.lines = {} # line_no -> None=executable, 0=instrumented, 1=covered |
| 84 self.stats = CoverageStats() |
| 85 |
| 86 def UpdateCoverage(self): |
| 87 """Updates the coverage summary based on covered lines.""" |
| 88 exe = instr = cov = 0 |
| 89 for l in self.lines.itervalues(): |
| 90 exe += 1 |
| 91 if l is not None: |
| 92 instr += 1 |
| 93 if l == 1: |
| 94 cov += 1 |
| 95 |
| 96 # Add stats that always exist |
| 97 self.stats = CoverageStats(lines_executable=exe, |
| 98 lines_instrumented=instr, |
| 99 lines_covered=cov, |
| 100 files_executable=1) |
| 101 |
| 102 # Add conditional stats |
| 103 if cov: |
| 104 self.stats['files_covered'] = 1 |
| 105 if instr: |
| 106 self.stats['files_instrumented'] = 1 |
| 107 |
| 108 |
| 109 #------------------------------------------------------------------------------ |
| 110 |
| 111 |
| 112 class CoveredDir(object): |
| 113 """Information about a directory containing covered files.""" |
| 114 |
| 115 def __init__(self, dirpath): |
| 116 """Constructor. |
| 117 |
| 118 Args: |
| 119 dirpath: Full path of directory, '/'-delimited. |
| 120 """ |
| 121 self.dirpath = dirpath |
| 122 |
| 123 # List of covered files directly in this dir, indexed by filename (not |
| 124 # full path) |
| 125 self.files = {} |
| 126 |
| 127 # List of subdirs, indexed by filename (not full path) |
| 128 self.subdirs = {} |
| 129 |
| 130 # Dict of CoverageStats objects summarizing all children, indexed by group |
| 131 self.stats_by_group = {'all':CoverageStats()} |
| 132 # TODO: by language |
| 133 |
| 134 def GetTree(self, indent=''): |
| 135 """Recursively gets stats for the directory and its children. |
| 136 |
| 137 Args: |
| 138 indent: indent prefix string. |
| 139 |
| 140 Returns: |
| 141 The tree as a string. |
| 142 """ |
| 143 dest = [] |
| 144 |
| 145 # Compile all groupstats |
| 146 groupstats = [] |
| 147 for group in sorted(self.stats_by_group): |
| 148 s = self.stats_by_group[group] |
| 149 if not s.get('lines_executable'): |
| 150 continue # Skip groups with no executable lines |
| 151 groupstats.append('%s:%d/%d/%d' % ( |
| 152 group, s.get('lines_covered', 0), |
| 153 s.get('lines_instrumented', 0), |
| 154 s.get('lines_executable', 0))) |
| 155 |
| 156 outline = '%s%-30s %s' % (indent, |
| 157 self.dirpath + '/', ' '.join(groupstats)) |
| 158 dest.append(outline.rstrip()) |
| 159 |
| 160 for d in sorted(self.subdirs): |
| 161 dest.append(self.subdirs[d].GetTree(indent=indent + ' ')) |
| 162 |
| 163 return '\n'.join(dest) |
| 164 |
| 165 #------------------------------------------------------------------------------ |
| 166 |
| 167 |
| 168 class Coverage(object): |
| 169 """Code coverage for a group of files.""" |
| 170 |
| 171 def __init__(self): |
| 172 """Constructor.""" |
| 173 self.files = {} # Map filename --> CoverageFile |
| 174 self.root_dirs = [] # (root, altname) |
| 175 self.rules = [] # (regexp, include, group, language) |
| 176 self.tree = CoveredDir('') |
| 177 self.print_stats = [] # Dicts of args to PrintStat() |
| 178 |
| 179 self.add_files_walk = os.walk # Walk function for AddFiles() |
| 180 |
| 181 # Must specify subdir rule, or AddFiles() won't find any files because it |
| 182 # will prune out all the subdirs. Since subdirs never match any code, |
| 183 # they won't be reported in other stats, so this is ok. |
| 184 self.AddRule('.*/$', language='subdir') |
| 185 |
| 186 |
| 187 def CleanupFilename(self, filename): |
| 188 """Cleans up a filename. |
| 189 |
| 190 Args: |
| 191 filename: Input filename. |
| 192 |
| 193 Returns: |
| 194 The cleaned up filename. |
| 195 |
| 196 Changes all path separators to '/'. |
| 197 Makes relative paths (those starting with '../' or './' absolute. |
| 198 Replaces all instances of root dirs with alternate names. |
| 199 """ |
| 200 # Change path separators |
| 201 filename = filename.replace('\\', '/') |
| 202 |
| 203 # If path is relative, make it absolute |
| 204 # TODO: Perhaps we should default to relative instead, and only understand |
| 205 # absolute to be files starting with '\', '/', or '[A-Za-z]:'? |
| 206 if filename.split('/')[0] in ('.', '..'): |
| 207 filename = os.path.abspath(filename).replace('\\', '/') |
| 208 |
| 209 # Replace alternate roots |
| 210 for root, alt_name in self.root_dirs: |
| 211 filename = re.sub('^' + re.escape(root) + '(?=(/|$))', |
| 212 alt_name, filename) |
| 213 return filename |
| 214 |
| 215 def ClassifyFile(self, filename): |
| 216 """Applies rules to a filename, to see if we care about it. |
| 217 |
| 218 Args: |
| 219 filename: Input filename. |
| 220 |
| 221 Returns: |
| 222 (None, None) if the file is not included or has no group or has no |
| 223 language. Otherwise, a 2-tuple containing: |
| 224 The group for the file (for example, 'source' or 'test'). |
| 225 The language of the file. |
| 226 """ |
| 227 include = False |
| 228 group = None |
| 229 language = None |
| 230 |
| 231 # Process all rules |
| 232 for regexp, rule_include, rule_group, rule_language in self.rules: |
| 233 if regexp.match(filename): |
| 234 # include/exclude source |
| 235 if rule_include is not None: |
| 236 include = rule_include |
| 237 if rule_group is not None: |
| 238 group = rule_group |
| 239 if rule_language is not None: |
| 240 language = rule_language |
| 241 |
| 242 # TODO: Should have a debug mode which prints files which aren't excluded |
| 243 # and why (explicitly excluded, no type, no language, etc.) |
| 244 |
| 245 # TODO: Files can belong to multiple groups? |
| 246 # (test/source) |
| 247 # (mac/pc/win) |
| 248 # (media_test/all_tests) |
| 249 # (small/med/large) |
| 250 # How to handle that? |
| 251 |
| 252 # Return classification if the file is included and has a group and |
| 253 # language |
| 254 if include and group and language: |
| 255 return group, language |
| 256 else: |
| 257 return None, None |
| 258 |
| 259 def AddRoot(self, root_path, alt_name='#'): |
| 260 """Adds a root directory. |
| 261 |
| 262 Args: |
| 263 root_path: Root directory to add. |
| 264 alt_name: If specified, name of root dir |
| 265 """ |
| 266 # Clean up root path based on existing rules |
| 267 self.root_dirs.append([self.CleanupFilename(root_path), alt_name]) |
| 268 |
| 269 def AddRule(self, path_regexp, include=None, group=None, language=None): |
| 270 """Adds a rule. |
| 271 |
| 272 Args: |
| 273 path_regexp: Regular expression to match for filenames. These are |
| 274 matched after root directory replacement. |
| 275 include: If True, includes matches; if False, excludes matches. Ignored |
| 276 if None. |
| 277 group: If not None, sets group to apply to matches. |
| 278 language: If not None, sets file language to apply to matches. |
| 279 """ |
| 280 # Compile regexp ahead of time |
| 281 self.rules.append([re.compile(path_regexp), include, group, language]) |
| 282 |
| 283 def GetCoveredFile(self, filename, add=False): |
| 284 """Gets the CoveredFile object for the filename. |
| 285 |
| 286 Args: |
| 287 filename: Name of file to find. |
| 288 add: If True, will add the file if it's not present. This applies the |
| 289 transformations from AddRoot() and AddRule(), and only adds the file |
| 290 if a rule includes it, and it has a group and language. |
| 291 |
| 292 Returns: |
| 293 The matching CoveredFile object, or None if not present. |
| 294 """ |
| 295 # Clean filename |
| 296 filename = self.CleanupFilename(filename) |
| 297 |
| 298 # Check for existing match |
| 299 if filename in self.files: |
| 300 return self.files[filename] |
| 301 |
| 302 # File isn't one we know about. If we can't add it, give up. |
| 303 if not add: |
| 304 return None |
| 305 |
| 306 # Check rules to see if file can be added |
| 307 group, language = self.ClassifyFile(filename) |
| 308 if not group: |
| 309 return None |
| 310 |
| 311 # Add the file |
| 312 f = CoveredFile(filename, group, language) |
| 313 self.files[filename] = f |
| 314 |
| 315 # Return the newly covered file |
| 316 return f |
| 317 |
| 318 def ParseLcovData(self, lcov_data): |
| 319 """Adds coverage from LCOV-formatted data. |
| 320 |
| 321 Args: |
| 322 lcov_data: An iterable returning lines of data in LCOV format. For |
| 323 example, a file or list of strings. |
| 324 """ |
| 325 cov_file = None |
| 326 cov_lines = None |
| 327 for line in lcov_data: |
| 328 line = line.strip() |
| 329 if line.startswith('SF:'): |
| 330 # Start of data for a new file; payload is filename |
| 331 cov_file = self.GetCoveredFile(line[3:], add=True) |
| 332 if cov_file: |
| 333 cov_lines = cov_file.lines |
| 334 elif not cov_file: |
| 335 # Inside data for a file we don't care about - so skip it |
| 336 pass |
| 337 elif line.startswith('DA:'): |
| 338 # Data point - that is, an executable line in current file |
| 339 line_no, is_covered = map(int, line[3:].split(',')) |
| 340 if is_covered: |
| 341 # Line is covered |
| 342 cov_lines[line_no] = 1 |
| 343 elif cov_lines.get(line_no) != 1: |
| 344 # Line is not covered, so track it as uncovered |
| 345 cov_lines[line_no] = 0 |
| 346 elif line == 'end_of_record': |
| 347 cov_file.UpdateCoverage() |
| 348 cov_file = None |
| 349 # (else ignore other line types) |
| 350 |
| 351 def ParseLcovFile(self, input_filename): |
| 352 """Adds coverage data from a .lcov file. |
| 353 |
| 354 Args: |
| 355 input_filename: Input filename. |
| 356 """ |
| 357 # TODO: All manner of error checking |
| 358 lcov_file = None |
| 359 try: |
| 360 lcov_file = open(input_filename, 'rt') |
| 361 self.ParseLcovData(lcov_file) |
| 362 finally: |
| 363 if lcov_file: |
| 364 lcov_file.close() |
| 365 |
| 366 def GetStat(self, stat, group='all', default=None): |
| 367 """Gets a statistic from the coverage object. |
| 368 |
| 369 Args: |
| 370 stat: Statistic to get. May also be an evaluatable python expression, |
| 371 using the stats. For example, 'stat1 - stat2'. |
| 372 group: File group to match; if 'all', matches all groups. |
| 373 default: Value to return if there was an error evaluating the stat. For |
| 374 example, if the stat does not exist. If None, raises |
| 375 CoverageStatError. |
| 376 |
| 377 Returns: |
| 378 The evaluated stat, or None if error. |
| 379 |
| 380 Raises: |
| 381 CoverageStatError: Error evaluating stat. |
| 382 """ |
| 383 # TODO: specify a subdir to get the stat from, then walk the tree to |
| 384 # print the stats from just that subdir |
| 385 |
| 386 # Make sure the group exists |
| 387 if group not in self.tree.stats_by_group: |
| 388 if default is None: |
| 389 raise CoverageStatError('Group %r not found.' % group) |
| 390 else: |
| 391 return default |
| 392 |
| 393 stats = self.tree.stats_by_group[group] |
| 394 try: |
| 395 return eval(stat, {'__builtins__':{'S':self.GetStat}}, stats) |
| 396 except Exception, e: |
| 397 if default is None: |
| 398 raise CoverageStatError('Error evaluating stat %r: %s' % (stat, e)) |
| 399 else: |
| 400 return default |
| 401 |
| 402 def PrintStat(self, stat, format=None, outfile=sys.stdout, **kwargs): |
| 403 """Prints a statistic from the coverage object. |
| 404 |
| 405 Args: |
| 406 stat: Statistic to get. May also be an evaluatable python expression, |
| 407 using the stats. For example, 'stat1 - stat2'. |
| 408 format: Format string to use when printing stat. If None, prints the |
| 409 stat and its evaluation. |
| 410 outfile: File stream to output stat to; defaults to stdout. |
| 411 kwargs: Additional args to pass to GetStat(). |
| 412 """ |
| 413 s = self.GetStat(stat, **kwargs) |
| 414 if format is None: |
| 415 outfile.write('GetStat(%r) = %s\n' % (stat, s)) |
| 416 else: |
| 417 outfile.write(format % s + '\n') |
| 418 |
| 419 def AddFiles(self, src_dir): |
| 420 """Adds files to coverage information. |
| 421 |
| 422 LCOV files only contains files which are compiled and instrumented as part |
| 423 of running coverage. This function finds missing files and adds them. |
| 424 |
| 425 Args: |
| 426 src_dir: Directory on disk at which to start search. May be a relative |
| 427 path on disk starting with '.' or '..', or an absolute path, or a |
| 428 path relative to an alt_name for one of the roots |
| 429 (for example, '#/src'). If the alt_name matches more than one root, |
| 430 all matches will be attempted. |
| 431 |
| 432 Note that dirs not underneath one of the root dirs and covered by an |
| 433 inclusion rule will be ignored. |
| 434 """ |
| 435 # Check for root dir alt_names in the path and replace with the actual |
| 436 # root dirs, then recurse. |
| 437 found_root = False |
| 438 for root, alt_name in self.root_dirs: |
| 439 replaced_root = re.sub('^' + re.escape(alt_name) + '(?=(/|$))', root, |
| 440 src_dir) |
| 441 if replaced_root != src_dir: |
| 442 found_root = True |
| 443 self.AddFiles(replaced_root) |
| 444 if found_root: |
| 445 return # Replaced an alt_name with a root_dir, so already recursed. |
| 446 |
| 447 for (dirpath, dirnames, filenames) in self.add_files_walk(src_dir): |
| 448 # Make a copy of the dirnames list so we can modify the original to |
| 449 # prune subdirs we don't need to walk. |
| 450 for d in list(dirnames): |
| 451 # Add trailing '/' to directory names so dir-based regexps can match |
| 452 # '/' instead of needing to specify '(/|$)'. |
| 453 dpath = self.CleanupFilename(dirpath + '/' + d) + '/' |
| 454 group, language = self.ClassifyFile(dpath) |
| 455 if not group: |
| 456 # Directory has been excluded, so don't traverse it |
| 457 # TODO: Document the slight weirdness caused by this: If you |
| 458 # AddFiles('./A'), and the rules include 'A/B/C/D' but not 'A/B', |
| 459 # then it won't recurse into './A/B' so won't find './A/B/C/D'. |
| 460 # Workarounds are to AddFiles('./A/B/C/D') or AddFiles('./A/B/C'). |
| 461 # The latter works because it explicitly walks the contents of the |
| 462 # path passed to AddFiles(), so it finds './A/B/C/D'. |
| 463 dirnames.remove(d) |
| 464 |
| 465 for f in filenames: |
| 466 covf = self.GetCoveredFile(dirpath + '/' + f, add=True) |
| 467 # TODO: scan files for executable lines. Add these to the file as |
| 468 # 'executable', but not 'instrumented' or 'covered'. |
| 469 # TODO: if a file has no executable lines, don't add it. |
| 470 if covf: |
| 471 covf.UpdateCoverage() |
| 472 |
| 473 def AddConfig(self, config_data, lcov_queue=None, addfiles_queue=None): |
| 474 """Adds JSON-ish config data. |
| 475 |
| 476 Args: |
| 477 config_data: Config data string. |
| 478 lcov_queue: If not None, object to append lcov_files to instead of |
| 479 parsing them immediately. |
| 480 addfiles_queue: If not None, object to append add_files to instead of |
| 481 processing them immediately. |
| 482 """ |
| 483 # TODO: All manner of error checking |
| 484 cfg = eval(config_data, {'__builtins__':{}}, {}) |
| 485 |
| 486 for rootdict in cfg.get('roots', []): |
| 487 self.AddRoot(rootdict['root'], alt_name=rootdict.get('altname', '#')) |
| 488 |
| 489 for ruledict in cfg.get('rules', []): |
| 490 self.AddRule(ruledict['regexp'], |
| 491 include=ruledict.get('include'), |
| 492 group=ruledict.get('group'), |
| 493 language=ruledict.get('language')) |
| 494 |
| 495 for add_lcov in cfg.get('lcov_files', []): |
| 496 if lcov_queue is not None: |
| 497 lcov_queue.append(add_lcov) |
| 498 else: |
| 499 self.ParseLcovFile(add_lcov) |
| 500 |
| 501 for add_path in cfg.get('add_files', []): |
| 502 if addfiles_queue is not None: |
| 503 addfiles_queue.append(add_path) |
| 504 else: |
| 505 self.AddFiles(add_path) |
| 506 |
| 507 self.print_stats += cfg.get('print_stats', []) |
| 508 |
| 509 def ParseConfig(self, filename, **kwargs): |
| 510 """Parses a configuration file. |
| 511 |
| 512 Args: |
| 513 filename: Config filename. |
| 514 """ |
| 515 # TODO: All manner of error checking |
| 516 f = None |
| 517 try: |
| 518 f = open(filename, 'rt') |
| 519 # Need to strip CR's from CRLF-terminated lines or posix systems can't |
| 520 # eval the data. |
| 521 config_data = f.read().replace('\r\n', '\n') |
| 522 # TODO: some sort of include syntax. Needs to be done at string-time |
| 523 # rather than at eval()-time, so that it's possible to include parts of |
| 524 # dicts. Path from a file to its include should be relative to the dir |
| 525 # containing the file. |
| 526 self.AddConfig(config_data, **kwargs) |
| 527 finally: |
| 528 if f: |
| 529 f.close() |
| 530 |
| 531 def UpdateTreeStats(self): |
| 532 """Recalculates the tree stats from the currently covered files. |
| 533 |
| 534 Also calculates coverage summary for files.""" |
| 535 self.tree = CoveredDir('') |
| 536 for cov_file in self.files.itervalues(): |
| 537 # Add the file to the tree |
| 538 # TODO: Don't really need to create the tree unless we're creating HTML |
| 539 fdirs = cov_file.filename.split('/') |
| 540 parent = self.tree |
| 541 ancestors = [parent] |
| 542 for d in fdirs[:-1]: |
| 543 if d not in parent.subdirs: |
| 544 parent.subdirs[d] = CoveredDir(d) |
| 545 parent = parent.subdirs[d] |
| 546 ancestors.append(parent) |
| 547 # Final subdir actually contains the file |
| 548 parent.files[fdirs[-1]] = cov_file |
| 549 |
| 550 # Now add file's contribution to coverage by dir |
| 551 for a in ancestors: |
| 552 # Add to 'all' group |
| 553 a.stats_by_group['all'].Add(cov_file.stats) |
| 554 |
| 555 # Add to group file belongs to |
| 556 if cov_file.group not in a.stats_by_group: |
| 557 a.stats_by_group[cov_file.group] = CoverageStats() |
| 558 cbyg = a.stats_by_group[cov_file.group] |
| 559 cbyg.Add(cov_file.stats) |
| 560 |
| 561 def PrintTree(self): |
| 562 """Prints the tree stats.""" |
| 563 # Print the tree |
| 564 print 'Lines of code coverage by directory:' |
| 565 print self.tree.GetTree() |
| 566 |
| 567 #------------------------------------------------------------------------------ |
| 568 |
| 569 |
| 570 def Main(argv): |
| 571 """Main routine. |
| 572 |
| 573 Args: |
| 574 argv: list of arguments |
| 575 |
| 576 Returns: |
| 577 exit code, 0 for normal exit. |
| 578 """ |
| 579 # Parse args |
| 580 parser = OptionParser() |
| 581 parser.add_option( |
| 582 '-i', '--input', dest='inputs', type='string', action='append', |
| 583 metavar='FILE', |
| 584 help='read LCOV input from FILE') |
| 585 parser.add_option( |
| 586 '-r', '--root', dest='roots', type='string', action='append', |
| 587 metavar='ROOT[=ALTNAME]', |
| 588 help='add ROOT directory, optionally map in coverage results as ALTNAME') |
| 589 parser.add_option( |
| 590 '-c', '--config', dest='configs', type='string', action='append', |
| 591 metavar='FILE', |
| 592 help='read settings from configuration FILE') |
| 593 parser.add_option( |
| 594 '-a', '--addfiles', dest='addfiles', type='string', action='append', |
| 595 metavar='PATH', |
| 596 help='add files from PATH to coverage data') |
| 597 parser.add_option( |
| 598 '-t', '--tree', dest='tree', action='store_true', |
| 599 help='print tree of code coverage by group') |
| 600 parser.add_option( |
| 601 '-u', '--uninstrumented', dest='uninstrumented', action='store_true', |
| 602 help='list uninstrumented files') |
| 603 |
| 604 parser.set_defaults( |
| 605 inputs=[], |
| 606 roots=[], |
| 607 configs=[], |
| 608 addfiles=[], |
| 609 tree=False, |
| 610 ) |
| 611 |
| 612 (options, args) = parser.parse_args() |
| 613 |
| 614 cov = Coverage() |
| 615 |
| 616 # Set root directories for coverage |
| 617 for root_opt in options.roots: |
| 618 if '=' in root_opt: |
| 619 cov.AddRoot(*root_opt.split('=')) |
| 620 else: |
| 621 cov.AddRoot(root_opt) |
| 622 |
| 623 # Read config files |
| 624 for config_file in options.configs: |
| 625 cov.ParseConfig(config_file, lcov_queue=options.inputs, |
| 626 addfiles_queue=options.addfiles) |
| 627 |
| 628 # Parse lcov files |
| 629 for input_filename in options.inputs: |
| 630 cov.ParseLcovFile(input_filename) |
| 631 |
| 632 # Add missing files |
| 633 for add_path in options.addfiles: |
| 634 cov.AddFiles(add_path) |
| 635 |
| 636 # Print help if no files specified |
| 637 if not cov.files: |
| 638 print 'No covered files found.' |
| 639 parser.print_help() |
| 640 return 1 |
| 641 |
| 642 # Update tree stats |
| 643 cov.UpdateTreeStats() |
| 644 |
| 645 # Print uninstrumented filenames |
| 646 if options.uninstrumented: |
| 647 print 'Uninstrumented files:' |
| 648 for f in sorted(cov.files): |
| 649 covf = cov.files[f] |
| 650 if not covf.stats.get('lines_instrumented'): |
| 651 print ' %-6s %-6s %s' % (covf.group, covf.language, f) |
| 652 |
| 653 |
| 654 # Print tree stats |
| 655 if options.tree: |
| 656 cov.PrintTree() |
| 657 |
| 658 # Print stats |
| 659 for ps_args in cov.print_stats: |
| 660 cov.PrintStat(**ps_args) |
| 661 |
| 662 # Normal exit |
| 663 return 0 |
| 664 |
| 665 |
| 666 #------------------------------------------------------------------------------ |
| 667 |
| 668 if __name__ == '__main__': |
| 669 sys.exit(Main(sys.argv)) |
OLD | NEW |