| Index: tools/code_coverage/croc.py
|
| ===================================================================
|
| --- tools/code_coverage/croc.py (revision 0)
|
| +++ tools/code_coverage/croc.py (revision 0)
|
| @@ -0,0 +1,669 @@
|
| +#!/usr/bin/python2.4
|
| +#
|
| +# Copyright 2009, Google Inc.
|
| +# All rights reserved.
|
| +#
|
| +# Redistribution and use in source and binary forms, with or without
|
| +# modification, are permitted provided that the following conditions are
|
| +# met:
|
| +#
|
| +# * Redistributions of source code must retain the above copyright
|
| +# notice, this list of conditions and the following disclaimer.
|
| +# * Redistributions in binary form must reproduce the above
|
| +# copyright notice, this list of conditions and the following disclaimer
|
| +# in the documentation and/or other materials provided with the
|
| +# distribution.
|
| +# * Neither the name of Google Inc. nor the names of its
|
| +# contributors may be used to endorse or promote products derived from
|
| +# this software without specific prior written permission.
|
| +#
|
| +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
| +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
| +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
| +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
| +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
| +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| +
|
| +"""Crocodile - compute coverage numbers for Chrome coverage dashboard."""
|
| +
|
| +import os
|
| +import re
|
| +import sys
|
| +from optparse import OptionParser
|
| +
|
| +
|
| +class CoverageError(Exception):
|
| + """Coverage error."""
|
| +
|
| +class CoverageStatError(CoverageError):
|
| + """Error evaluating coverage stat."""
|
| +
|
| +#------------------------------------------------------------------------------
|
| +
|
| +
|
| +class CoverageStats(dict):
|
| + """Coverage statistics."""
|
| +
|
| + def Add(self, coverage_stats):
|
| + """Adds a contribution from another coverage stats dict.
|
| +
|
| + Args:
|
| + coverage_stats: Statistics to add to this one.
|
| + """
|
| + for k, v in coverage_stats.iteritems():
|
| + if k in self:
|
| + self[k] = self[k] + v
|
| + else:
|
| + self[k] = v
|
| +
|
| +#------------------------------------------------------------------------------
|
| +
|
| +
|
| +class CoveredFile(object):
|
| + """Information about a single covered file."""
|
| +
|
| + def __init__(self, filename, group, language):
|
| + """Constructor.
|
| +
|
| + Args:
|
| + filename: Full path to file, '/'-delimited.
|
| + group: Group file belongs to.
|
| + language: Language for file.
|
| + """
|
| + self.filename = filename
|
| + self.group = group
|
| + self.language = language
|
| +
|
| + # No coverage data for file yet
|
| + self.lines = {} # line_no -> None=executable, 0=instrumented, 1=covered
|
| + self.stats = CoverageStats()
|
| +
|
| + def UpdateCoverage(self):
|
| + """Updates the coverage summary based on covered lines."""
|
| + exe = instr = cov = 0
|
| + for l in self.lines.itervalues():
|
| + exe += 1
|
| + if l is not None:
|
| + instr += 1
|
| + if l == 1:
|
| + cov += 1
|
| +
|
| + # Add stats that always exist
|
| + self.stats = CoverageStats(lines_executable=exe,
|
| + lines_instrumented=instr,
|
| + lines_covered=cov,
|
| + files_executable=1)
|
| +
|
| + # Add conditional stats
|
| + if cov:
|
| + self.stats['files_covered'] = 1
|
| + if instr:
|
| + self.stats['files_instrumented'] = 1
|
| +
|
| +
|
| +#------------------------------------------------------------------------------
|
| +
|
| +
|
| +class CoveredDir(object):
|
| + """Information about a directory containing covered files."""
|
| +
|
| + def __init__(self, dirpath):
|
| + """Constructor.
|
| +
|
| + Args:
|
| + dirpath: Full path of directory, '/'-delimited.
|
| + """
|
| + self.dirpath = dirpath
|
| +
|
| + # List of covered files directly in this dir, indexed by filename (not
|
| + # full path)
|
| + self.files = {}
|
| +
|
| + # List of subdirs, indexed by filename (not full path)
|
| + self.subdirs = {}
|
| +
|
| + # Dict of CoverageStats objects summarizing all children, indexed by group
|
| + self.stats_by_group = {'all':CoverageStats()}
|
| + # TODO: by language
|
| +
|
| + def GetTree(self, indent=''):
|
| + """Recursively gets stats for the directory and its children.
|
| +
|
| + Args:
|
| + indent: indent prefix string.
|
| +
|
| + Returns:
|
| + The tree as a string.
|
| + """
|
| + dest = []
|
| +
|
| + # Compile all groupstats
|
| + groupstats = []
|
| + for group in sorted(self.stats_by_group):
|
| + s = self.stats_by_group[group]
|
| + if not s.get('lines_executable'):
|
| + continue # Skip groups with no executable lines
|
| + groupstats.append('%s:%d/%d/%d' % (
|
| + group, s.get('lines_covered', 0),
|
| + s.get('lines_instrumented', 0),
|
| + s.get('lines_executable', 0)))
|
| +
|
| + outline = '%s%-30s %s' % (indent,
|
| + self.dirpath + '/', ' '.join(groupstats))
|
| + dest.append(outline.rstrip())
|
| +
|
| + for d in sorted(self.subdirs):
|
| + dest.append(self.subdirs[d].GetTree(indent=indent + ' '))
|
| +
|
| + return '\n'.join(dest)
|
| +
|
| +#------------------------------------------------------------------------------
|
| +
|
| +
|
| +class Coverage(object):
|
| + """Code coverage for a group of files."""
|
| +
|
| + def __init__(self):
|
| + """Constructor."""
|
| + self.files = {} # Map filename --> CoverageFile
|
| + self.root_dirs = [] # (root, altname)
|
| + self.rules = [] # (regexp, include, group, language)
|
| + self.tree = CoveredDir('')
|
| + self.print_stats = [] # Dicts of args to PrintStat()
|
| +
|
| + self.add_files_walk = os.walk # Walk function for AddFiles()
|
| +
|
| + # Must specify subdir rule, or AddFiles() won't find any files because it
|
| + # will prune out all the subdirs. Since subdirs never match any code,
|
| + # they won't be reported in other stats, so this is ok.
|
| + self.AddRule('.*/$', language='subdir')
|
| +
|
| +
|
| + def CleanupFilename(self, filename):
|
| + """Cleans up a filename.
|
| +
|
| + Args:
|
| + filename: Input filename.
|
| +
|
| + Returns:
|
| + The cleaned up filename.
|
| +
|
| + Changes all path separators to '/'.
|
| + Makes relative paths (those starting with '../' or './' absolute.
|
| + Replaces all instances of root dirs with alternate names.
|
| + """
|
| + # Change path separators
|
| + filename = filename.replace('\\', '/')
|
| +
|
| + # If path is relative, make it absolute
|
| + # TODO: Perhaps we should default to relative instead, and only understand
|
| + # absolute to be files starting with '\', '/', or '[A-Za-z]:'?
|
| + if filename.split('/')[0] in ('.', '..'):
|
| + filename = os.path.abspath(filename).replace('\\', '/')
|
| +
|
| + # Replace alternate roots
|
| + for root, alt_name in self.root_dirs:
|
| + filename = re.sub('^' + re.escape(root) + '(?=(/|$))',
|
| + alt_name, filename)
|
| + return filename
|
| +
|
| + def ClassifyFile(self, filename):
|
| + """Applies rules to a filename, to see if we care about it.
|
| +
|
| + Args:
|
| + filename: Input filename.
|
| +
|
| + Returns:
|
| + (None, None) if the file is not included or has no group or has no
|
| + language. Otherwise, a 2-tuple containing:
|
| + The group for the file (for example, 'source' or 'test').
|
| + The language of the file.
|
| + """
|
| + include = False
|
| + group = None
|
| + language = None
|
| +
|
| + # Process all rules
|
| + for regexp, rule_include, rule_group, rule_language in self.rules:
|
| + if regexp.match(filename):
|
| + # include/exclude source
|
| + if rule_include is not None:
|
| + include = rule_include
|
| + if rule_group is not None:
|
| + group = rule_group
|
| + if rule_language is not None:
|
| + language = rule_language
|
| +
|
| + # TODO: Should have a debug mode which prints files which aren't excluded
|
| + # and why (explicitly excluded, no type, no language, etc.)
|
| +
|
| + # TODO: Files can belong to multiple groups?
|
| + # (test/source)
|
| + # (mac/pc/win)
|
| + # (media_test/all_tests)
|
| + # (small/med/large)
|
| + # How to handle that?
|
| +
|
| + # Return classification if the file is included and has a group and
|
| + # language
|
| + if include and group and language:
|
| + return group, language
|
| + else:
|
| + return None, None
|
| +
|
| + def AddRoot(self, root_path, alt_name='#'):
|
| + """Adds a root directory.
|
| +
|
| + Args:
|
| + root_path: Root directory to add.
|
| + alt_name: If specified, name of root dir
|
| + """
|
| + # Clean up root path based on existing rules
|
| + self.root_dirs.append([self.CleanupFilename(root_path), alt_name])
|
| +
|
| + def AddRule(self, path_regexp, include=None, group=None, language=None):
|
| + """Adds a rule.
|
| +
|
| + Args:
|
| + path_regexp: Regular expression to match for filenames. These are
|
| + matched after root directory replacement.
|
| + include: If True, includes matches; if False, excludes matches. Ignored
|
| + if None.
|
| + group: If not None, sets group to apply to matches.
|
| + language: If not None, sets file language to apply to matches.
|
| + """
|
| + # Compile regexp ahead of time
|
| + self.rules.append([re.compile(path_regexp), include, group, language])
|
| +
|
| + def GetCoveredFile(self, filename, add=False):
|
| + """Gets the CoveredFile object for the filename.
|
| +
|
| + Args:
|
| + filename: Name of file to find.
|
| + add: If True, will add the file if it's not present. This applies the
|
| + transformations from AddRoot() and AddRule(), and only adds the file
|
| + if a rule includes it, and it has a group and language.
|
| +
|
| + Returns:
|
| + The matching CoveredFile object, or None if not present.
|
| + """
|
| + # Clean filename
|
| + filename = self.CleanupFilename(filename)
|
| +
|
| + # Check for existing match
|
| + if filename in self.files:
|
| + return self.files[filename]
|
| +
|
| + # File isn't one we know about. If we can't add it, give up.
|
| + if not add:
|
| + return None
|
| +
|
| + # Check rules to see if file can be added
|
| + group, language = self.ClassifyFile(filename)
|
| + if not group:
|
| + return None
|
| +
|
| + # Add the file
|
| + f = CoveredFile(filename, group, language)
|
| + self.files[filename] = f
|
| +
|
| + # Return the newly covered file
|
| + return f
|
| +
|
| + def ParseLcovData(self, lcov_data):
|
| + """Adds coverage from LCOV-formatted data.
|
| +
|
| + Args:
|
| + lcov_data: An iterable returning lines of data in LCOV format. For
|
| + example, a file or list of strings.
|
| + """
|
| + cov_file = None
|
| + cov_lines = None
|
| + for line in lcov_data:
|
| + line = line.strip()
|
| + if line.startswith('SF:'):
|
| + # Start of data for a new file; payload is filename
|
| + cov_file = self.GetCoveredFile(line[3:], add=True)
|
| + if cov_file:
|
| + cov_lines = cov_file.lines
|
| + elif not cov_file:
|
| + # Inside data for a file we don't care about - so skip it
|
| + pass
|
| + elif line.startswith('DA:'):
|
| + # Data point - that is, an executable line in current file
|
| + line_no, is_covered = map(int, line[3:].split(','))
|
| + if is_covered:
|
| + # Line is covered
|
| + cov_lines[line_no] = 1
|
| + elif cov_lines.get(line_no) != 1:
|
| + # Line is not covered, so track it as uncovered
|
| + cov_lines[line_no] = 0
|
| + elif line == 'end_of_record':
|
| + cov_file.UpdateCoverage()
|
| + cov_file = None
|
| + # (else ignore other line types)
|
| +
|
| + def ParseLcovFile(self, input_filename):
|
| + """Adds coverage data from a .lcov file.
|
| +
|
| + Args:
|
| + input_filename: Input filename.
|
| + """
|
| + # TODO: All manner of error checking
|
| + lcov_file = None
|
| + try:
|
| + lcov_file = open(input_filename, 'rt')
|
| + self.ParseLcovData(lcov_file)
|
| + finally:
|
| + if lcov_file:
|
| + lcov_file.close()
|
| +
|
| + def GetStat(self, stat, group='all', default=None):
|
| + """Gets a statistic from the coverage object.
|
| +
|
| + Args:
|
| + stat: Statistic to get. May also be an evaluatable python expression,
|
| + using the stats. For example, 'stat1 - stat2'.
|
| + group: File group to match; if 'all', matches all groups.
|
| + default: Value to return if there was an error evaluating the stat. For
|
| + example, if the stat does not exist. If None, raises
|
| + CoverageStatError.
|
| +
|
| + Returns:
|
| + The evaluated stat, or None if error.
|
| +
|
| + Raises:
|
| + CoverageStatError: Error evaluating stat.
|
| + """
|
| + # TODO: specify a subdir to get the stat from, then walk the tree to
|
| + # print the stats from just that subdir
|
| +
|
| + # Make sure the group exists
|
| + if group not in self.tree.stats_by_group:
|
| + if default is None:
|
| + raise CoverageStatError('Group %r not found.' % group)
|
| + else:
|
| + return default
|
| +
|
| + stats = self.tree.stats_by_group[group]
|
| + try:
|
| + return eval(stat, {'__builtins__':{'S':self.GetStat}}, stats)
|
| + except Exception, e:
|
| + if default is None:
|
| + raise CoverageStatError('Error evaluating stat %r: %s' % (stat, e))
|
| + else:
|
| + return default
|
| +
|
| + def PrintStat(self, stat, format=None, outfile=sys.stdout, **kwargs):
|
| + """Prints a statistic from the coverage object.
|
| +
|
| + Args:
|
| + stat: Statistic to get. May also be an evaluatable python expression,
|
| + using the stats. For example, 'stat1 - stat2'.
|
| + format: Format string to use when printing stat. If None, prints the
|
| + stat and its evaluation.
|
| + outfile: File stream to output stat to; defaults to stdout.
|
| + kwargs: Additional args to pass to GetStat().
|
| + """
|
| + s = self.GetStat(stat, **kwargs)
|
| + if format is None:
|
| + outfile.write('GetStat(%r) = %s\n' % (stat, s))
|
| + else:
|
| + outfile.write(format % s + '\n')
|
| +
|
| + def AddFiles(self, src_dir):
|
| + """Adds files to coverage information.
|
| +
|
| + LCOV files only contains files which are compiled and instrumented as part
|
| + of running coverage. This function finds missing files and adds them.
|
| +
|
| + Args:
|
| + src_dir: Directory on disk at which to start search. May be a relative
|
| + path on disk starting with '.' or '..', or an absolute path, or a
|
| + path relative to an alt_name for one of the roots
|
| + (for example, '#/src'). If the alt_name matches more than one root,
|
| + all matches will be attempted.
|
| +
|
| + Note that dirs not underneath one of the root dirs and covered by an
|
| + inclusion rule will be ignored.
|
| + """
|
| + # Check for root dir alt_names in the path and replace with the actual
|
| + # root dirs, then recurse.
|
| + found_root = False
|
| + for root, alt_name in self.root_dirs:
|
| + replaced_root = re.sub('^' + re.escape(alt_name) + '(?=(/|$))', root,
|
| + src_dir)
|
| + if replaced_root != src_dir:
|
| + found_root = True
|
| + self.AddFiles(replaced_root)
|
| + if found_root:
|
| + return # Replaced an alt_name with a root_dir, so already recursed.
|
| +
|
| + for (dirpath, dirnames, filenames) in self.add_files_walk(src_dir):
|
| + # Make a copy of the dirnames list so we can modify the original to
|
| + # prune subdirs we don't need to walk.
|
| + for d in list(dirnames):
|
| + # Add trailing '/' to directory names so dir-based regexps can match
|
| + # '/' instead of needing to specify '(/|$)'.
|
| + dpath = self.CleanupFilename(dirpath + '/' + d) + '/'
|
| + group, language = self.ClassifyFile(dpath)
|
| + if not group:
|
| + # Directory has been excluded, so don't traverse it
|
| + # TODO: Document the slight weirdness caused by this: If you
|
| + # AddFiles('./A'), and the rules include 'A/B/C/D' but not 'A/B',
|
| + # then it won't recurse into './A/B' so won't find './A/B/C/D'.
|
| + # Workarounds are to AddFiles('./A/B/C/D') or AddFiles('./A/B/C').
|
| + # The latter works because it explicitly walks the contents of the
|
| + # path passed to AddFiles(), so it finds './A/B/C/D'.
|
| + dirnames.remove(d)
|
| +
|
| + for f in filenames:
|
| + covf = self.GetCoveredFile(dirpath + '/' + f, add=True)
|
| + # TODO: scan files for executable lines. Add these to the file as
|
| + # 'executable', but not 'instrumented' or 'covered'.
|
| + # TODO: if a file has no executable lines, don't add it.
|
| + if covf:
|
| + covf.UpdateCoverage()
|
| +
|
| + def AddConfig(self, config_data, lcov_queue=None, addfiles_queue=None):
|
| + """Adds JSON-ish config data.
|
| +
|
| + Args:
|
| + config_data: Config data string.
|
| + lcov_queue: If not None, object to append lcov_files to instead of
|
| + parsing them immediately.
|
| + addfiles_queue: If not None, object to append add_files to instead of
|
| + processing them immediately.
|
| + """
|
| + # TODO: All manner of error checking
|
| + cfg = eval(config_data, {'__builtins__':{}}, {})
|
| +
|
| + for rootdict in cfg.get('roots', []):
|
| + self.AddRoot(rootdict['root'], alt_name=rootdict.get('altname', '#'))
|
| +
|
| + for ruledict in cfg.get('rules', []):
|
| + self.AddRule(ruledict['regexp'],
|
| + include=ruledict.get('include'),
|
| + group=ruledict.get('group'),
|
| + language=ruledict.get('language'))
|
| +
|
| + for add_lcov in cfg.get('lcov_files', []):
|
| + if lcov_queue is not None:
|
| + lcov_queue.append(add_lcov)
|
| + else:
|
| + self.ParseLcovFile(add_lcov)
|
| +
|
| + for add_path in cfg.get('add_files', []):
|
| + if addfiles_queue is not None:
|
| + addfiles_queue.append(add_path)
|
| + else:
|
| + self.AddFiles(add_path)
|
| +
|
| + self.print_stats += cfg.get('print_stats', [])
|
| +
|
| + def ParseConfig(self, filename, **kwargs):
|
| + """Parses a configuration file.
|
| +
|
| + Args:
|
| + filename: Config filename.
|
| + """
|
| + # TODO: All manner of error checking
|
| + f = None
|
| + try:
|
| + f = open(filename, 'rt')
|
| + # Need to strip CR's from CRLF-terminated lines or posix systems can't
|
| + # eval the data.
|
| + config_data = f.read().replace('\r\n', '\n')
|
| + # TODO: some sort of include syntax. Needs to be done at string-time
|
| + # rather than at eval()-time, so that it's possible to include parts of
|
| + # dicts. Path from a file to its include should be relative to the dir
|
| + # containing the file.
|
| + self.AddConfig(config_data, **kwargs)
|
| + finally:
|
| + if f:
|
| + f.close()
|
| +
|
| + def UpdateTreeStats(self):
|
| + """Recalculates the tree stats from the currently covered files.
|
| +
|
| + Also calculates coverage summary for files."""
|
| + self.tree = CoveredDir('')
|
| + for cov_file in self.files.itervalues():
|
| + # Add the file to the tree
|
| + # TODO: Don't really need to create the tree unless we're creating HTML
|
| + fdirs = cov_file.filename.split('/')
|
| + parent = self.tree
|
| + ancestors = [parent]
|
| + for d in fdirs[:-1]:
|
| + if d not in parent.subdirs:
|
| + parent.subdirs[d] = CoveredDir(d)
|
| + parent = parent.subdirs[d]
|
| + ancestors.append(parent)
|
| + # Final subdir actually contains the file
|
| + parent.files[fdirs[-1]] = cov_file
|
| +
|
| + # Now add file's contribution to coverage by dir
|
| + for a in ancestors:
|
| + # Add to 'all' group
|
| + a.stats_by_group['all'].Add(cov_file.stats)
|
| +
|
| + # Add to group file belongs to
|
| + if cov_file.group not in a.stats_by_group:
|
| + a.stats_by_group[cov_file.group] = CoverageStats()
|
| + cbyg = a.stats_by_group[cov_file.group]
|
| + cbyg.Add(cov_file.stats)
|
| +
|
| + def PrintTree(self):
|
| + """Prints the tree stats."""
|
| + # Print the tree
|
| + print 'Lines of code coverage by directory:'
|
| + print self.tree.GetTree()
|
| +
|
| +#------------------------------------------------------------------------------
|
| +
|
| +
|
| +def Main(argv):
|
| + """Main routine.
|
| +
|
| + Args:
|
| + argv: list of arguments
|
| +
|
| + Returns:
|
| + exit code, 0 for normal exit.
|
| + """
|
| + # Parse args
|
| + parser = OptionParser()
|
| + parser.add_option(
|
| + '-i', '--input', dest='inputs', type='string', action='append',
|
| + metavar='FILE',
|
| + help='read LCOV input from FILE')
|
| + parser.add_option(
|
| + '-r', '--root', dest='roots', type='string', action='append',
|
| + metavar='ROOT[=ALTNAME]',
|
| + help='add ROOT directory, optionally map in coverage results as ALTNAME')
|
| + parser.add_option(
|
| + '-c', '--config', dest='configs', type='string', action='append',
|
| + metavar='FILE',
|
| + help='read settings from configuration FILE')
|
| + parser.add_option(
|
| + '-a', '--addfiles', dest='addfiles', type='string', action='append',
|
| + metavar='PATH',
|
| + help='add files from PATH to coverage data')
|
| + parser.add_option(
|
| + '-t', '--tree', dest='tree', action='store_true',
|
| + help='print tree of code coverage by group')
|
| + parser.add_option(
|
| + '-u', '--uninstrumented', dest='uninstrumented', action='store_true',
|
| + help='list uninstrumented files')
|
| +
|
| + parser.set_defaults(
|
| + inputs=[],
|
| + roots=[],
|
| + configs=[],
|
| + addfiles=[],
|
| + tree=False,
|
| + )
|
| +
|
| + (options, args) = parser.parse_args()
|
| +
|
| + cov = Coverage()
|
| +
|
| + # Set root directories for coverage
|
| + for root_opt in options.roots:
|
| + if '=' in root_opt:
|
| + cov.AddRoot(*root_opt.split('='))
|
| + else:
|
| + cov.AddRoot(root_opt)
|
| +
|
| + # Read config files
|
| + for config_file in options.configs:
|
| + cov.ParseConfig(config_file, lcov_queue=options.inputs,
|
| + addfiles_queue=options.addfiles)
|
| +
|
| + # Parse lcov files
|
| + for input_filename in options.inputs:
|
| + cov.ParseLcovFile(input_filename)
|
| +
|
| + # Add missing files
|
| + for add_path in options.addfiles:
|
| + cov.AddFiles(add_path)
|
| +
|
| + # Print help if no files specified
|
| + if not cov.files:
|
| + print 'No covered files found.'
|
| + parser.print_help()
|
| + return 1
|
| +
|
| + # Update tree stats
|
| + cov.UpdateTreeStats()
|
| +
|
| + # Print uninstrumented filenames
|
| + if options.uninstrumented:
|
| + print 'Uninstrumented files:'
|
| + for f in sorted(cov.files):
|
| + covf = cov.files[f]
|
| + if not covf.stats.get('lines_instrumented'):
|
| + print ' %-6s %-6s %s' % (covf.group, covf.language, f)
|
| +
|
| +
|
| + # Print tree stats
|
| + if options.tree:
|
| + cov.PrintTree()
|
| +
|
| + # Print stats
|
| + for ps_args in cov.print_stats:
|
| + cov.PrintStat(**ps_args)
|
| +
|
| + # Normal exit
|
| + return 0
|
| +
|
| +
|
| +#------------------------------------------------------------------------------
|
| +
|
| +if __name__ == '__main__':
|
| + sys.exit(Main(sys.argv))
|
|
|