723 lines
22 KiB
Python
723 lines
22 KiB
Python
|
#!/usr/bin/env python
|
||
|
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
||
|
# Use of this source code is governed by a BSD-style license that can be
|
||
|
# found in the LICENSE file.
|
||
|
|
||
|
"""Crocodile - compute coverage numbers for Chrome coverage dashboard."""
|
||
|
|
||
|
import optparse
|
||
|
import os
|
||
|
import platform
|
||
|
import re
|
||
|
import sys
|
||
|
import croc_html
|
||
|
import croc_scan
|
||
|
|
||
|
|
||
|
class CrocError(Exception):
|
||
|
"""Coverage error."""
|
||
|
|
||
|
|
||
|
class CrocStatError(CrocError):
|
||
|
"""Error evaluating coverage stat."""
|
||
|
|
||
|
#------------------------------------------------------------------------------
|
||
|
|
||
|
|
||
|
class CoverageStats(dict):
|
||
|
"""Coverage statistics."""
|
||
|
|
||
|
# Default dictionary values for this stat.
|
||
|
DEFAULTS = { 'files_covered': 0,
|
||
|
'files_instrumented': 0,
|
||
|
'files_executable': 0,
|
||
|
'lines_covered': 0,
|
||
|
'lines_instrumented': 0,
|
||
|
'lines_executable': 0 }
|
||
|
|
||
|
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] += v
|
||
|
else:
|
||
|
self[k] = v
|
||
|
|
||
|
def AddDefaults(self):
|
||
|
"""Add some default stats which might be assumed present.
|
||
|
|
||
|
Do not clobber if already present. Adds resilience when evaling a
|
||
|
croc file which expects certain stats to exist."""
|
||
|
for k, v in self.DEFAULTS.iteritems():
|
||
|
if not k in self:
|
||
|
self[k] = v
|
||
|
|
||
|
#------------------------------------------------------------------------------
|
||
|
|
||
|
|
||
|
class CoveredFile(object):
|
||
|
"""Information about a single covered file."""
|
||
|
|
||
|
def __init__(self, filename, **kwargs):
|
||
|
"""Constructor.
|
||
|
|
||
|
Args:
|
||
|
filename: Full path to file, '/'-delimited.
|
||
|
kwargs: Keyword args are attributes for file.
|
||
|
"""
|
||
|
self.filename = filename
|
||
|
self.attrs = dict(kwargs)
|
||
|
|
||
|
# Move these to attrs?
|
||
|
self.local_path = None # Local path to file
|
||
|
self.in_lcov = False # Is file instrumented?
|
||
|
|
||
|
# 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 or self.in_lcov:
|
||
|
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,
|
||
|
os.path.split(self.dirpath)[1] + '/',
|
||
|
' '.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, dict of RHS attrs)
|
||
|
self.tree = CoveredDir('')
|
||
|
self.print_stats = [] # Dicts of args to PrintStat()
|
||
|
|
||
|
# Functions which need to be replaced for unit testing
|
||
|
self.add_files_walk = os.walk # Walk function for AddFiles()
|
||
|
self.scan_file = croc_scan.ScanFile # Source scanner for AddFiles()
|
||
|
|
||
|
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('\\', '/')
|
||
|
|
||
|
# Windows doesn't care about case sensitivity.
|
||
|
if platform.system() in ['Windows', 'Microsoft']:
|
||
|
filename = filename.lower()
|
||
|
|
||
|
# 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:
|
||
|
# Windows doesn't care about case sensitivity.
|
||
|
if platform.system() in ['Windows', 'Microsoft']:
|
||
|
root = root.lower()
|
||
|
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:
|
||
|
A dict of attributes for the file, accumulated from the right hand sides
|
||
|
of rules which fired.
|
||
|
"""
|
||
|
attrs = {}
|
||
|
|
||
|
# Process all rules
|
||
|
for regexp, rhs_dict in self.rules:
|
||
|
if regexp.match(filename):
|
||
|
attrs.update(rhs_dict)
|
||
|
|
||
|
return attrs
|
||
|
# TODO: Files can belong to multiple groups?
|
||
|
# (test/source)
|
||
|
# (mac/pc/win)
|
||
|
# (media_test/all_tests)
|
||
|
# (small/med/large)
|
||
|
# How to handle that?
|
||
|
|
||
|
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. Otherwise, defaults to '_'.
|
||
|
|
||
|
Raises:
|
||
|
ValueError: alt_name was blank.
|
||
|
"""
|
||
|
# Alt name must not be blank. If it were, there wouldn't be a way to
|
||
|
# reverse-resolve from a root-replaced path back to the local path, since
|
||
|
# '' would always match the beginning of the candidate filename, resulting
|
||
|
# in an infinite loop.
|
||
|
if not alt_name:
|
||
|
raise ValueError('AddRoot alt_name must not be blank.')
|
||
|
|
||
|
# Clean up root path based on existing rules
|
||
|
self.root_dirs.append([self.CleanupFilename(root_path), alt_name])
|
||
|
|
||
|
def AddRule(self, path_regexp, **kwargs):
|
||
|
"""Adds a rule.
|
||
|
|
||
|
Args:
|
||
|
path_regexp: Regular expression to match for filenames. These are
|
||
|
matched after root directory replacement.
|
||
|
kwargs: Keyword arguments are attributes to set if the rule applies.
|
||
|
|
||
|
Keyword arguments currently supported:
|
||
|
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), dict(kwargs)])
|
||
|
|
||
|
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. Files must be included and
|
||
|
# have a group and language.
|
||
|
attrs = self.ClassifyFile(filename)
|
||
|
if not (attrs.get('include')
|
||
|
and attrs.get('group')
|
||
|
and attrs.get('language')):
|
||
|
return None
|
||
|
|
||
|
# Add the file
|
||
|
f = CoveredFile(filename, **attrs)
|
||
|
self.files[filename] = f
|
||
|
|
||
|
# Return the newly covered file
|
||
|
return f
|
||
|
|
||
|
def RemoveCoveredFile(self, cov_file):
|
||
|
"""Removes the file from the covered file list.
|
||
|
|
||
|
Args:
|
||
|
cov_file: A file object returned by GetCoveredFile().
|
||
|
"""
|
||
|
self.files.pop(cov_file.filename)
|
||
|
|
||
|
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
|
||
|
cov_file.in_lcov = True # File was instrumented
|
||
|
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
|
||
|
CrocStatError.
|
||
|
|
||
|
Returns:
|
||
|
The evaluated stat, or None if error.
|
||
|
|
||
|
Raises:
|
||
|
CrocStatError: 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 CrocStatError('Group %r not found.' % group)
|
||
|
else:
|
||
|
return default
|
||
|
|
||
|
stats = self.tree.stats_by_group[group]
|
||
|
# Unit tests use real dicts, not CoverageStats objects,
|
||
|
# so we can't AddDefaults() on them.
|
||
|
if group == 'all' and hasattr(stats, 'AddDefaults'):
|
||
|
stats.AddDefaults()
|
||
|
try:
|
||
|
return eval(stat, {'__builtins__': {'S': self.GetStat}}, stats)
|
||
|
except Exception, e:
|
||
|
if default is None:
|
||
|
raise CrocStatError('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) + '/'
|
||
|
attrs = self.ClassifyFile(dpath)
|
||
|
if not attrs.get('include'):
|
||
|
# 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:
|
||
|
local_path = dirpath + '/' + f
|
||
|
|
||
|
covf = self.GetCoveredFile(local_path, add=True)
|
||
|
if not covf:
|
||
|
continue
|
||
|
|
||
|
# Save where we found the file, for generating line-by-line HTML output
|
||
|
covf.local_path = local_path
|
||
|
|
||
|
if covf.in_lcov:
|
||
|
# File already instrumented and doesn't need to be scanned
|
||
|
continue
|
||
|
|
||
|
if not covf.attrs.get('add_if_missing', 1):
|
||
|
# Not allowed to add the file
|
||
|
self.RemoveCoveredFile(covf)
|
||
|
continue
|
||
|
|
||
|
# Scan file to find potentially-executable lines
|
||
|
lines = self.scan_file(covf.local_path, covf.attrs.get('language'))
|
||
|
if lines:
|
||
|
for l in lines:
|
||
|
covf.lines[l] = None
|
||
|
covf.UpdateCoverage()
|
||
|
else:
|
||
|
# File has no executable lines, so don't count it
|
||
|
self.RemoveCoveredFile(covf)
|
||
|
|
||
|
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', []):
|
||
|
regexp = ruledict.pop('regexp')
|
||
|
self.AddRule(regexp, **ruledict)
|
||
|
|
||
|
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.
|
||
|
kwargs: Additional parameters to pass to AddConfig().
|
||
|
"""
|
||
|
# 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.
|
||
|
#
|
||
|
# Or perhaps it could be done after eval. In that case, there'd be an
|
||
|
# 'include' section with a list of files to include. Those would be
|
||
|
# eval()'d and recursively pre- or post-merged with the including file.
|
||
|
#
|
||
|
# Or maybe just don't worry about it, since multiple configs can be
|
||
|
# specified on the command line.
|
||
|
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
|
||
|
fdirs = cov_file.filename.split('/')
|
||
|
parent = self.tree
|
||
|
ancestors = [parent]
|
||
|
for d in fdirs[:-1]:
|
||
|
if d not in parent.subdirs:
|
||
|
if parent.dirpath:
|
||
|
parent.subdirs[d] = CoveredDir(parent.dirpath + '/' + d)
|
||
|
else:
|
||
|
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
|
||
|
group = cov_file.attrs.get('group')
|
||
|
if group not in a.stats_by_group:
|
||
|
a.stats_by_group[group] = CoverageStats()
|
||
|
cbyg = a.stats_by_group[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 = optparse.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.add_option(
|
||
|
'-m', '--html', dest='html_out', type='string', metavar='PATH',
|
||
|
help='write HTML output to PATH')
|
||
|
parser.add_option(
|
||
|
'-b', '--base_url', dest='base_url', type='string', metavar='URL',
|
||
|
help='include URL in base tag of HTML output')
|
||
|
|
||
|
parser.set_defaults(
|
||
|
inputs=[],
|
||
|
roots=[],
|
||
|
configs=[],
|
||
|
addfiles=[],
|
||
|
tree=False,
|
||
|
html_out=None,
|
||
|
)
|
||
|
|
||
|
options = parser.parse_args(args=argv)[0]
|
||
|
|
||
|
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.in_lcov:
|
||
|
print ' %-6s %-6s %s' % (covf.attrs.get('group'),
|
||
|
covf.attrs.get('language'), f)
|
||
|
|
||
|
# Print tree stats
|
||
|
if options.tree:
|
||
|
cov.PrintTree()
|
||
|
|
||
|
# Print stats
|
||
|
for ps_args in cov.print_stats:
|
||
|
cov.PrintStat(**ps_args)
|
||
|
|
||
|
# Generate HTML
|
||
|
if options.html_out:
|
||
|
html = croc_html.CrocHtml(cov, options.html_out, options.base_url)
|
||
|
html.Write()
|
||
|
|
||
|
# Normal exit
|
||
|
return 0
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
sys.exit(Main(sys.argv))
|