Major refactoring of Croc.
Add support for scanning missing source files for executable lines. Add support for HTML output. Now reports percent coverage. BUG=none TEST=by hand on experimental buildbot Review URL: http://codereview.chromium.org/113980 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@17141 0039d316-1c4b-4281-b951-d872f2087c98
Esse commit está contido em:
@@ -0,0 +1,100 @@
|
||||
# -*- python -*-
|
||||
# Crocodile config file for Chromium - settings common to all platforms
|
||||
#
|
||||
# This should be speicified before the platform-specific config, for example:
|
||||
# croc -c chrome_common.croc -c linux/chrome_linux.croc
|
||||
|
||||
{
|
||||
# List of root directories, applied in order
|
||||
'roots' : [
|
||||
# Sub-paths we specifically care about and want to call out
|
||||
{
|
||||
'root' : '_/src',
|
||||
'altname' : 'CHROMIUM',
|
||||
},
|
||||
],
|
||||
|
||||
# List of rules, applied in order
|
||||
# Note that any 'include':0 rules here will be overridden by the 'include':1
|
||||
# rules in the platform-specific configs.
|
||||
'rules' : [
|
||||
# Don't scan for executable lines in uninstrumented C++ header files
|
||||
{
|
||||
'regexp' : '.*\\.(h|hpp)$',
|
||||
'add_if_missing' : 0,
|
||||
},
|
||||
|
||||
# Groups
|
||||
{
|
||||
'regexp' : '',
|
||||
'group' : 'source',
|
||||
},
|
||||
{
|
||||
'regexp' : '.*_(test|unittest)\\.',
|
||||
'group' : 'test',
|
||||
},
|
||||
|
||||
# Languages
|
||||
{
|
||||
'regexp' : '.*\\.(c|h)$',
|
||||
'language' : 'C',
|
||||
},
|
||||
{
|
||||
'regexp' : '.*\\.(cc|cpp|hpp)$',
|
||||
'language' : 'C++',
|
||||
},
|
||||
],
|
||||
|
||||
# Paths to add source from
|
||||
'add_files' : [
|
||||
'CHROMIUM'
|
||||
],
|
||||
|
||||
# Statistics to print
|
||||
'print_stats' : [
|
||||
{
|
||||
'stat' : 'files_executable',
|
||||
'format' : '*RESULT FilesKnown: files_executable= %d files',
|
||||
},
|
||||
{
|
||||
'stat' : 'files_instrumented',
|
||||
'format' : '*RESULT FilesInstrumented: files_instrumented= %d files',
|
||||
},
|
||||
{
|
||||
'stat' : '100.0 * files_instrumented / files_executable',
|
||||
'format' : '*RESULT FilesInstrumentedPercent: files_instrumented_percent= %g',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_executable',
|
||||
'format' : '*RESULT LinesKnown: lines_known= %d lines',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_instrumented',
|
||||
'format' : '*RESULT LinesInstrumented: lines_instrumented= %d lines',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_covered',
|
||||
'format' : '*RESULT LinesCoveredSource: lines_covered_source= %d lines',
|
||||
'group' : 'source',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_covered',
|
||||
'format' : '*RESULT LinesCoveredTest: lines_covered_test= %d lines',
|
||||
'group' : 'test',
|
||||
},
|
||||
{
|
||||
'stat' : '100.0 * lines_covered / lines_executable',
|
||||
'format' : '*RESULT PercentCovered: percent_covered= %g',
|
||||
},
|
||||
{
|
||||
'stat' : '100.0 * lines_covered / lines_executable',
|
||||
'format' : '*RESULT PercentCoveredSource: percent_covered_source= %g',
|
||||
'group' : 'source',
|
||||
},
|
||||
{
|
||||
'stat' : '100.0 * lines_covered / lines_executable',
|
||||
'format' : '*RESULT PercentCoveredTest: percent_covered_test= %g',
|
||||
'group' : 'test',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
# Files/paths to include. Specify these before the excludes, since rules
|
||||
# are in order.
|
||||
{
|
||||
'regexp' : '^#/src/(base|media|net|printing)/',
|
||||
'regexp' : '^CHROMIUM/(base|media|net|printing)/',
|
||||
'include' : 1,
|
||||
},
|
||||
# Don't include subversion or mercurial SCM dirs
|
||||
@@ -33,56 +33,7 @@
|
||||
|
||||
# Groups
|
||||
{
|
||||
'regexp' : '',
|
||||
'group' : 'source',
|
||||
},
|
||||
{
|
||||
'regexp' : '.*_(test|test_linux|unittest)\\.',
|
||||
'group' : 'test',
|
||||
},
|
||||
|
||||
# Languages
|
||||
{
|
||||
'regexp' : '.*\\.(c|h)$',
|
||||
'language' : 'C',
|
||||
},
|
||||
{
|
||||
'regexp' : '.*\\.(cc|cpp|hpp)$',
|
||||
'language' : 'C++',
|
||||
},
|
||||
],
|
||||
|
||||
# Paths to add source from
|
||||
'add_files' : [
|
||||
'#/src'
|
||||
],
|
||||
|
||||
# Statistics to print
|
||||
'print_stats' : [
|
||||
{
|
||||
'stat' : 'files_executable',
|
||||
'format' : '*RESULT FilesKnown: files_executable= %d files',
|
||||
},
|
||||
{
|
||||
'stat' : 'files_instrumented',
|
||||
'format' : '*RESULT FilesInstrumented: files_instrumented= %d files',
|
||||
},
|
||||
{
|
||||
'stat' : '100.0 * files_instrumented / files_executable',
|
||||
'format' : '*RESULT FilesInstrumentedPercent: files_instrumented_percent= %g',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_instrumented',
|
||||
'format' : '*RESULT LinesInstrumented: lines_instrumented= %d lines',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_covered',
|
||||
'format' : '*RESULT LinesCoveredSource: lines_covered_source= %d lines',
|
||||
'group' : 'source',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_covered',
|
||||
'format' : '*RESULT LinesCoveredTest: lines_covered_test= %d lines',
|
||||
'regexp' : '.*_test_linux\\.',
|
||||
'group' : 'test',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- python -*-
|
||||
# Crocodile config file for Chromium mac
|
||||
|
||||
{
|
||||
@@ -6,7 +7,7 @@
|
||||
# Files/paths to include. Specify these before the excludes, since rules
|
||||
# are in order.
|
||||
{
|
||||
'regexp' : '^#/src/(base|media|net|printing)/',
|
||||
'regexp' : '^CHROMIUM/(base|media|net|printing)/',
|
||||
'include' : 1,
|
||||
},
|
||||
# Don't include subversion or mercurial SCM dirs
|
||||
@@ -32,23 +33,11 @@
|
||||
|
||||
# Groups
|
||||
{
|
||||
'regexp' : '',
|
||||
'group' : 'source',
|
||||
},
|
||||
{
|
||||
'regexp' : '.*_(test|test_mac|unittest)\\.',
|
||||
'regexp' : '.*_test_mac\\.',
|
||||
'group' : 'test',
|
||||
},
|
||||
|
||||
# Languages
|
||||
{
|
||||
'regexp' : '.*\\.(c|h)$',
|
||||
'language' : 'C',
|
||||
},
|
||||
{
|
||||
'regexp' : '.*\\.(cc|cpp|hpp)$',
|
||||
'language' : 'C++',
|
||||
},
|
||||
{
|
||||
'regexp' : '.*\\.m$',
|
||||
'language' : 'ObjC',
|
||||
@@ -58,39 +47,4 @@
|
||||
'language' : 'ObjC++',
|
||||
},
|
||||
],
|
||||
|
||||
# Paths to add source from
|
||||
'add_files' : [
|
||||
'#/src'
|
||||
],
|
||||
|
||||
# Statistics to print
|
||||
'print_stats' : [
|
||||
{
|
||||
'stat' : 'files_executable',
|
||||
'format' : '*RESULT FilesKnown: files_executable= %d files',
|
||||
},
|
||||
{
|
||||
'stat' : 'files_instrumented',
|
||||
'format' : '*RESULT FilesInstrumented: files_instrumented= %d files',
|
||||
},
|
||||
{
|
||||
'stat' : '100.0 * files_instrumented / files_executable',
|
||||
'format' : '*RESULT FilesInstrumentedPercent: files_instrumented_percent= %g',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_instrumented',
|
||||
'format' : '*RESULT LinesInstrumented: lines_instrumented= %d lines',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_covered',
|
||||
'format' : '*RESULT LinesCoveredSource: lines_covered_source= %d lines',
|
||||
'group' : 'source',
|
||||
},
|
||||
{
|
||||
'stat' : 'lines_covered',
|
||||
'format' : '*RESULT LinesCoveredTest: lines_covered_test= %d lines',
|
||||
'group' : 'test',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
+139
-90
@@ -31,16 +31,19 @@
|
||||
|
||||
"""Crocodile - compute coverage numbers for Chrome coverage dashboard."""
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
import croc_html
|
||||
import croc_scan
|
||||
|
||||
|
||||
class CoverageError(Exception):
|
||||
class CrocError(Exception):
|
||||
"""Coverage error."""
|
||||
|
||||
class CoverageStatError(CoverageError):
|
||||
|
||||
class CrocStatError(CrocError):
|
||||
"""Error evaluating coverage stat."""
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
@@ -57,7 +60,7 @@ class CoverageStats(dict):
|
||||
"""
|
||||
for k, v in coverage_stats.iteritems():
|
||||
if k in self:
|
||||
self[k] = self[k] + v
|
||||
self[k] += v
|
||||
else:
|
||||
self[k] = v
|
||||
|
||||
@@ -67,17 +70,19 @@ class CoverageStats(dict):
|
||||
class CoveredFile(object):
|
||||
"""Information about a single covered file."""
|
||||
|
||||
def __init__(self, filename, group, language):
|
||||
def __init__(self, filename, **kwargs):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
filename: Full path to file, '/'-delimited.
|
||||
group: Group file belongs to.
|
||||
language: Language for file.
|
||||
kwargs: Keyword args are attributes for file.
|
||||
"""
|
||||
self.filename = filename
|
||||
self.group = group
|
||||
self.language = language
|
||||
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
|
||||
@@ -102,10 +107,9 @@ class CoveredFile(object):
|
||||
# Add conditional stats
|
||||
if cov:
|
||||
self.stats['files_covered'] = 1
|
||||
if instr:
|
||||
if instr or self.in_lcov:
|
||||
self.stats['files_instrumented'] = 1
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -128,7 +132,7 @@ class CoveredDir(object):
|
||||
self.subdirs = {}
|
||||
|
||||
# Dict of CoverageStats objects summarizing all children, indexed by group
|
||||
self.stats_by_group = {'all':CoverageStats()}
|
||||
self.stats_by_group = {'all': CoverageStats()}
|
||||
# TODO: by language
|
||||
|
||||
def GetTree(self, indent=''):
|
||||
@@ -154,7 +158,8 @@ class CoveredDir(object):
|
||||
s.get('lines_executable', 0)))
|
||||
|
||||
outline = '%s%-30s %s' % (indent,
|
||||
self.dirpath + '/', ' '.join(groupstats))
|
||||
os.path.split(self.dirpath)[1] + '/',
|
||||
' '.join(groupstats))
|
||||
dest.append(outline.rstrip())
|
||||
|
||||
for d in sorted(self.subdirs):
|
||||
@@ -172,17 +177,13 @@ class Coverage(object):
|
||||
"""Constructor."""
|
||||
self.files = {} # Map filename --> CoverageFile
|
||||
self.root_dirs = [] # (root, altname)
|
||||
self.rules = [] # (regexp, include, group, language)
|
||||
self.rules = [] # (regexp, dict of RHS attrs)
|
||||
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')
|
||||
|
||||
# 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.
|
||||
@@ -208,8 +209,8 @@ class Coverage(object):
|
||||
|
||||
# Replace alternate roots
|
||||
for root, alt_name in self.root_dirs:
|
||||
filename = re.sub('^' + re.escape(root) + '(?=(/|$))',
|
||||
alt_name, filename)
|
||||
filename = re.sub('^' + re.escape(root) + '(?=(/|$))',
|
||||
alt_name, filename)
|
||||
return filename
|
||||
|
||||
def ClassifyFile(self, filename):
|
||||
@@ -219,29 +220,17 @@ class Coverage(object):
|
||||
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.
|
||||
A dict of attributes for the file, accumulated from the right hand sides
|
||||
of rules which fired.
|
||||
"""
|
||||
include = False
|
||||
group = None
|
||||
language = None
|
||||
attrs = {}
|
||||
|
||||
# Process all rules
|
||||
for regexp, rule_include, rule_group, rule_language in self.rules:
|
||||
for regexp, rhs_dict 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.)
|
||||
attrs.update(rhs_dict)
|
||||
|
||||
return attrs
|
||||
# TODO: Files can belong to multiple groups?
|
||||
# (test/source)
|
||||
# (mac/pc/win)
|
||||
@@ -249,36 +238,43 @@ class Coverage(object):
|
||||
# (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='#'):
|
||||
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
|
||||
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, include=None, group=None, language=None):
|
||||
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), include, group, language])
|
||||
self.rules.append([re.compile(path_regexp), dict(kwargs)])
|
||||
|
||||
def GetCoveredFile(self, filename, add=False):
|
||||
"""Gets the CoveredFile object for the filename.
|
||||
@@ -303,18 +299,29 @@ class Coverage(object):
|
||||
if not add:
|
||||
return None
|
||||
|
||||
# Check rules to see if file can be added
|
||||
group, language = self.ClassifyFile(filename)
|
||||
if not group:
|
||||
# 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, group, language)
|
||||
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.
|
||||
|
||||
@@ -331,6 +338,7 @@ class Coverage(object):
|
||||
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
|
||||
@@ -372,13 +380,13 @@ class Coverage(object):
|
||||
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.
|
||||
CrocStatError.
|
||||
|
||||
Returns:
|
||||
The evaluated stat, or None if error.
|
||||
|
||||
Raises:
|
||||
CoverageStatError: Error evaluating stat.
|
||||
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
|
||||
@@ -386,16 +394,16 @@ class Coverage(object):
|
||||
# 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)
|
||||
raise CrocStatError('Group %r not found.' % group)
|
||||
else:
|
||||
return default
|
||||
|
||||
stats = self.tree.stats_by_group[group]
|
||||
try:
|
||||
return eval(stat, {'__builtins__':{'S':self.GetStat}}, stats)
|
||||
return eval(stat, {'__builtins__': {'S': self.GetStat}}, stats)
|
||||
except Exception, e:
|
||||
if default is None:
|
||||
raise CoverageStatError('Error evaluating stat %r: %s' % (stat, e))
|
||||
raise CrocStatError('Error evaluating stat %r: %s' % (stat, e))
|
||||
else:
|
||||
return default
|
||||
|
||||
@@ -426,7 +434,7 @@ class Coverage(object):
|
||||
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,
|
||||
(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
|
||||
@@ -451,8 +459,8 @@ class Coverage(object):
|
||||
# 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:
|
||||
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',
|
||||
@@ -463,12 +471,33 @@ class Coverage(object):
|
||||
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:
|
||||
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.
|
||||
@@ -481,16 +510,14 @@ class Coverage(object):
|
||||
processing them immediately.
|
||||
"""
|
||||
# TODO: All manner of error checking
|
||||
cfg = eval(config_data, {'__builtins__':{}}, {})
|
||||
cfg = eval(config_data, {'__builtins__': {}}, {})
|
||||
|
||||
for rootdict in cfg.get('roots', []):
|
||||
self.AddRoot(rootdict['root'], alt_name=rootdict.get('altname', '#'))
|
||||
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'))
|
||||
regexp = ruledict.pop('regexp')
|
||||
self.AddRule(regexp, **ruledict)
|
||||
|
||||
for add_lcov in cfg.get('lcov_files', []):
|
||||
if lcov_queue is not None:
|
||||
@@ -511,6 +538,7 @@ class Coverage(object):
|
||||
|
||||
Args:
|
||||
filename: Config filename.
|
||||
kwargs: Additional parameters to pass to AddConfig().
|
||||
"""
|
||||
# TODO: All manner of error checking
|
||||
f = None
|
||||
@@ -519,10 +547,18 @@ class Coverage(object):
|
||||
# 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.
|
||||
# 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:
|
||||
@@ -531,17 +567,20 @@ class Coverage(object):
|
||||
def UpdateTreeStats(self):
|
||||
"""Recalculates the tree stats from the currently covered files.
|
||||
|
||||
Also calculates coverage summary for 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)
|
||||
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
|
||||
@@ -553,9 +592,10 @@ class Coverage(object):
|
||||
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]
|
||||
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):
|
||||
@@ -577,7 +617,7 @@ def Main(argv):
|
||||
exit code, 0 for normal exit.
|
||||
"""
|
||||
# Parse args
|
||||
parser = OptionParser()
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option(
|
||||
'-i', '--input', dest='inputs', type='string', action='append',
|
||||
metavar='FILE',
|
||||
@@ -600,6 +640,9 @@ def Main(argv):
|
||||
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.set_defaults(
|
||||
inputs=[],
|
||||
@@ -607,9 +650,10 @@ def Main(argv):
|
||||
configs=[],
|
||||
addfiles=[],
|
||||
tree=False,
|
||||
html_out=None,
|
||||
)
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
options = parser.parse_args(args=argv)[0]
|
||||
|
||||
cov = Coverage()
|
||||
|
||||
@@ -647,9 +691,9 @@ def Main(argv):
|
||||
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)
|
||||
|
||||
if not covf.in_lcov:
|
||||
print ' %-6s %-6s %s' % (covf.attrs.get('group'),
|
||||
covf.attrs.get('language'), f)
|
||||
|
||||
# Print tree stats
|
||||
if options.tree:
|
||||
@@ -659,6 +703,11 @@ def Main(argv):
|
||||
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)
|
||||
html.Write()
|
||||
|
||||
# Normal exit
|
||||
return 0
|
||||
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
#!/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 HTML output."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
import xml.dom
|
||||
|
||||
|
||||
class CrocHtmlError(Exception):
|
||||
"""Coverage HTML error."""
|
||||
|
||||
|
||||
class HtmlElement(object):
|
||||
"""Node in a HTML file."""
|
||||
|
||||
def __init__(self, doc, element):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
doc: XML document object.
|
||||
element: XML element.
|
||||
"""
|
||||
self.doc = doc
|
||||
self.element = element
|
||||
|
||||
def E(self, name, **kwargs):
|
||||
"""Adds a child element.
|
||||
|
||||
Args:
|
||||
name: Name of element.
|
||||
kwargs: Attributes for element. To use an attribute which is a python
|
||||
reserved word (i.e. 'class'), prefix the attribute name with 'e_'.
|
||||
|
||||
Returns:
|
||||
The child element.
|
||||
"""
|
||||
he = HtmlElement(self.doc, self.doc.createElement(name))
|
||||
element = he.element
|
||||
self.element.appendChild(element)
|
||||
|
||||
for k, v in kwargs.iteritems():
|
||||
if k.startswith('e_'):
|
||||
# Remove prefix
|
||||
element.setAttribute(k[2:], str(v))
|
||||
else:
|
||||
element.setAttribute(k, str(v))
|
||||
|
||||
return he
|
||||
|
||||
def Text(self, text):
|
||||
"""Adds a text node.
|
||||
|
||||
Args:
|
||||
text: Text to add.
|
||||
|
||||
Returns:
|
||||
self.
|
||||
"""
|
||||
t = self.doc.createTextNode(str(text))
|
||||
self.element.appendChild(t)
|
||||
return self
|
||||
|
||||
|
||||
class HtmlFile(object):
|
||||
"""HTML file."""
|
||||
|
||||
def __init__(self, xml_impl, filename):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
xml_impl: DOMImplementation to use to create document.
|
||||
filename: Path to file.
|
||||
"""
|
||||
self.xml_impl = xml_impl
|
||||
doctype = xml_impl.createDocumentType(
|
||||
'HTML', '-//W3C//DTD HTML 4.01//EN',
|
||||
'http://www.w3.org/TR/html4/strict.dtd')
|
||||
self.doc = xml_impl.createDocument(None, 'html', doctype)
|
||||
self.filename = filename
|
||||
|
||||
# Create head and body elements
|
||||
root = HtmlElement(self.doc, self.doc.documentElement)
|
||||
self.head = root.E('head')
|
||||
self.body = root.E('body')
|
||||
|
||||
def Write(self, cleanup=True):
|
||||
"""Writes the file.
|
||||
|
||||
Args:
|
||||
cleanup: If True, calls unlink() on the internal xml document. This
|
||||
frees up memory, but means that you can't use this file for anything
|
||||
else.
|
||||
"""
|
||||
f = open(self.filename, 'wt')
|
||||
self.doc.writexml(f, encoding='UTF-8')
|
||||
f.close()
|
||||
|
||||
if cleanup:
|
||||
self.doc.unlink()
|
||||
# Prevent future uses of the doc now that we've unlinked it
|
||||
self.doc = None
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
COV_TYPE_STRING = {None: 'm', 0: 'i', 1: 'E', 2: ' '}
|
||||
COV_TYPE_CLASS = {None: 'missing', 0: 'instr', 1: 'covered', 2: ''}
|
||||
|
||||
|
||||
class CrocHtml(object):
|
||||
"""Crocodile HTML output class."""
|
||||
|
||||
def __init__(self, cov, output_root):
|
||||
"""Constructor."""
|
||||
self.cov = cov
|
||||
self.output_root = output_root
|
||||
self.xml_impl = xml.dom.getDOMImplementation()
|
||||
self.time_string = 'Coverage information generated %s.' % time.asctime()
|
||||
|
||||
def CreateHtmlDoc(self, filename, title):
|
||||
"""Creates a new HTML document.
|
||||
|
||||
Args:
|
||||
filename: Filename to write to, relative to self.output_root.
|
||||
title: Title of page
|
||||
|
||||
Returns:
|
||||
The document.
|
||||
"""
|
||||
f = HtmlFile(self.xml_impl, self.output_root + '/' + filename)
|
||||
|
||||
f.head.E('title').Text(title)
|
||||
f.head.E(
|
||||
'link', rel='stylesheet', type='text/css',
|
||||
href='../' * (len(filename.split('/')) - 1) + 'croc.css')
|
||||
|
||||
return f
|
||||
|
||||
def AddCaptionForFile(self, body, path):
|
||||
"""Adds a caption for the file, with links to each parent dir.
|
||||
|
||||
Args:
|
||||
body: Body elemement.
|
||||
path: Path to file.
|
||||
"""
|
||||
# This is slightly different that for subdir, because it needs to have a
|
||||
# link to the current directory's index.html.
|
||||
hdr = body.E('h2')
|
||||
hdr.Text('Coverage for ')
|
||||
dirs = [''] + path.split('/')
|
||||
num_dirs = len(dirs)
|
||||
for i in range(num_dirs - 1):
|
||||
hdr.E('a', href=(
|
||||
'../' * (num_dirs - i - 2) + 'index.html')).Text(dirs[i] + '/')
|
||||
hdr.Text(dirs[-1])
|
||||
|
||||
def AddCaptionForSubdir(self, body, path):
|
||||
"""Adds a caption for the subdir, with links to each parent dir.
|
||||
|
||||
Args:
|
||||
body: Body elemement.
|
||||
path: Path to subdir.
|
||||
"""
|
||||
# Link to parent dirs
|
||||
hdr = body.E('h2')
|
||||
hdr.Text('Coverage for ')
|
||||
dirs = [''] + path.split('/')
|
||||
num_dirs = len(dirs)
|
||||
for i in range(num_dirs - 1):
|
||||
hdr.E('a', href=(
|
||||
'../' * (num_dirs - i - 1) + 'index.html')).Text(dirs[i] + '/')
|
||||
hdr.Text(dirs[-1] + '/')
|
||||
|
||||
def AddSectionHeader(self, table, caption, itemtype, is_file=False):
|
||||
"""Adds a section header to the coverage table.
|
||||
|
||||
Args:
|
||||
table: Table to add rows to.
|
||||
caption: Caption for section, if not None.
|
||||
itemtype: Type of items in this section, if not None.
|
||||
is_file: Are items in this section files?
|
||||
"""
|
||||
|
||||
if caption is not None:
|
||||
table.E('tr').E('td', e_class='secdesc', colspan=8).Text(caption)
|
||||
|
||||
sec_hdr = table.E('tr')
|
||||
|
||||
if itemtype is not None:
|
||||
sec_hdr.E('td', e_class='section').Text(itemtype)
|
||||
|
||||
sec_hdr.E('td', e_class='section').Text('Coverage')
|
||||
sec_hdr.E('td', e_class='section', colspan=3).Text(
|
||||
'Lines executed / instrumented / missing')
|
||||
|
||||
graph = sec_hdr.E('td', e_class='section')
|
||||
graph.E('span', style='color:#00FF00').Text('exe')
|
||||
graph.Text(' / ')
|
||||
graph.E('span', style='color:#FFFF00').Text('inst')
|
||||
graph.Text(' / ')
|
||||
graph.E('span', style='color:#FF0000').Text('miss')
|
||||
|
||||
if is_file:
|
||||
sec_hdr.E('td', e_class='section').Text('Language')
|
||||
sec_hdr.E('td', e_class='section').Text('Group')
|
||||
else:
|
||||
sec_hdr.E('td', e_class='section', colspan=2)
|
||||
|
||||
def AddItem(self, table, itemname, stats, attrs, link=None):
|
||||
"""Adds a bar graph to the element. This is a series of <td> elements.
|
||||
|
||||
Args:
|
||||
table: Table to add item to.
|
||||
itemname: Name of item.
|
||||
stats: Stats object.
|
||||
attrs: Attributes dictionary; if None, no attributes will be printed.
|
||||
link: Destination for itemname hyperlink, if not None.
|
||||
"""
|
||||
row = table.E('tr')
|
||||
|
||||
# Add item name
|
||||
if itemname is not None:
|
||||
item_elem = row.E('td')
|
||||
if link is not None:
|
||||
item_elem = item_elem.E('a', href=link)
|
||||
item_elem.Text(itemname)
|
||||
|
||||
# Get stats
|
||||
stat_exe = stats.get('lines_executable', 0)
|
||||
stat_ins = stats.get('lines_instrumented', 0)
|
||||
stat_cov = stats.get('lines_covered', 0)
|
||||
|
||||
percent = row.E('td')
|
||||
|
||||
# Add text
|
||||
row.E('td', e_class='number').Text(stat_cov)
|
||||
row.E('td', e_class='number').Text(stat_ins)
|
||||
row.E('td', e_class='number').Text(stat_exe - stat_ins)
|
||||
|
||||
# Add percent and graph; only fill in if there's something in there
|
||||
graph = row.E('td', e_class='graph', width=100)
|
||||
if stat_exe:
|
||||
percent_cov = 100.0 * stat_cov / stat_exe
|
||||
percent_ins = 100.0 * stat_ins / stat_exe
|
||||
|
||||
# Color percent based on thresholds
|
||||
percent.Text('%.1f%%' % percent_cov)
|
||||
if percent_cov >= 80:
|
||||
percent.element.setAttribute('class', 'high_pct')
|
||||
elif percent_cov >= 60:
|
||||
percent.element.setAttribute('class', 'mid_pct')
|
||||
else:
|
||||
percent.element.setAttribute('class', 'low_pct')
|
||||
|
||||
# Graphs use integer values
|
||||
percent_cov = int(percent_cov)
|
||||
percent_ins = int(percent_ins)
|
||||
|
||||
graph.Text('.')
|
||||
graph.E('span', style='padding-left:%dpx' % percent_cov,
|
||||
e_class='g_covered')
|
||||
graph.E('span', style='padding-left:%dpx' % (percent_ins - percent_cov),
|
||||
e_class='g_instr')
|
||||
graph.E('span', style='padding-left:%dpx' % (100 - percent_ins),
|
||||
e_class='g_missing')
|
||||
|
||||
if attrs:
|
||||
row.E('td', e_class='stat').Text(attrs.get('language'))
|
||||
row.E('td', e_class='stat').Text(attrs.get('group'))
|
||||
else:
|
||||
row.E('td', colspan=2)
|
||||
|
||||
def WriteFile(self, cov_file):
|
||||
"""Writes the HTML for a file.
|
||||
|
||||
Args:
|
||||
cov_file: croc.CoveredFile to write.
|
||||
"""
|
||||
print ' ' + cov_file.filename
|
||||
title = 'Coverage for ' + cov_file.filename
|
||||
|
||||
f = self.CreateHtmlDoc(cov_file.filename + '.html', title)
|
||||
body = f.body
|
||||
|
||||
# Write header section
|
||||
self.AddCaptionForFile(body, cov_file.filename)
|
||||
|
||||
# Summary for this file
|
||||
table = body.E('table')
|
||||
self.AddSectionHeader(table, None, None, is_file=True)
|
||||
self.AddItem(table, None, cov_file.stats, cov_file.attrs)
|
||||
|
||||
body.E('h2').Text('Line-by-line coverage:')
|
||||
|
||||
# Print line-by-line coverage
|
||||
if cov_file.local_path:
|
||||
code_table = body.E('table').E('tr').E('td').E('pre')
|
||||
|
||||
flines = open(cov_file.local_path, 'rt')
|
||||
lineno = 0
|
||||
|
||||
for line in flines:
|
||||
lineno += 1
|
||||
line_cov = cov_file.lines.get(lineno, 2)
|
||||
e_class = COV_TYPE_CLASS.get(line_cov)
|
||||
|
||||
code_table.E('span', e_class=e_class).Text('%4d %s : %s\n' % (
|
||||
lineno,
|
||||
COV_TYPE_STRING.get(line_cov),
|
||||
line.rstrip()
|
||||
))
|
||||
|
||||
else:
|
||||
body.Text('Line-by-line coverage not available. Make sure the directory'
|
||||
' containing this file has been scanned via ')
|
||||
body.E('B').Text('add_files')
|
||||
body.Text(' in a configuration file, or the ')
|
||||
body.E('B').Text('--addfiles')
|
||||
body.Text(' command line option.')
|
||||
|
||||
# TODO: if file doesn't have a local path, try to find it by
|
||||
# reverse-mapping roots and searching for the file.
|
||||
|
||||
body.E('p', e_class='time').Text(self.time_string)
|
||||
f.Write()
|
||||
|
||||
def WriteSubdir(self, cov_dir):
|
||||
"""Writes the index.html for a subdirectory.
|
||||
|
||||
Args:
|
||||
cov_dir: croc.CoveredDir to write.
|
||||
"""
|
||||
print ' ' + cov_dir.dirpath + '/'
|
||||
|
||||
# Create the subdir if it doesn't already exist
|
||||
subdir = self.output_root + '/' + cov_dir.dirpath
|
||||
if not os.path.exists(subdir):
|
||||
os.mkdir(subdir)
|
||||
|
||||
if cov_dir.dirpath:
|
||||
title = 'Coverage for ' + cov_dir.dirpath + '/'
|
||||
f = self.CreateHtmlDoc(cov_dir.dirpath + '/index.html', title)
|
||||
else:
|
||||
title = 'Coverage summary'
|
||||
f = self.CreateHtmlDoc('index.html', title)
|
||||
|
||||
body = f.body
|
||||
|
||||
# Write header section
|
||||
if cov_dir.dirpath:
|
||||
self.AddCaptionForSubdir(body, cov_dir.dirpath)
|
||||
else:
|
||||
body.E('h2').Text(title)
|
||||
|
||||
table = body.E('table')
|
||||
|
||||
# Coverage by group
|
||||
self.AddSectionHeader(table, 'Coverage by Group', 'Group')
|
||||
|
||||
for group in sorted(cov_dir.stats_by_group):
|
||||
self.AddItem(table, group, cov_dir.stats_by_group[group], None)
|
||||
|
||||
# List subdirs
|
||||
if cov_dir.subdirs:
|
||||
self.AddSectionHeader(table, 'Subdirectories', 'Subdirectory')
|
||||
|
||||
for d in sorted(cov_dir.subdirs):
|
||||
self.AddItem(table, d + '/', cov_dir.subdirs[d].stats_by_group['all'],
|
||||
None, link=d + '/index.html')
|
||||
|
||||
# List files
|
||||
if cov_dir.files:
|
||||
self.AddSectionHeader(table, 'Files in This Directory', 'Filename',
|
||||
is_file=True)
|
||||
|
||||
for filename in sorted(cov_dir.files):
|
||||
cov_file = cov_dir.files[filename]
|
||||
self.AddItem(table, filename, cov_file.stats, cov_file.attrs,
|
||||
link=filename + '.html')
|
||||
|
||||
body.E('p', e_class='time').Text(self.time_string)
|
||||
f.Write()
|
||||
|
||||
def WriteRoot(self):
|
||||
"""Writes the files in the output root."""
|
||||
# Find ourselves
|
||||
src_dir = os.path.split(self.WriteRoot.func_code.co_filename)[0]
|
||||
|
||||
# Files to copy into output root
|
||||
copy_files = [
|
||||
'croc.css',
|
||||
]
|
||||
|
||||
# Copy files from our directory into the output directory
|
||||
for copy_file in copy_files:
|
||||
print ' Copying %s' % copy_file
|
||||
shutil.copyfile(os.path.join(src_dir, copy_file),
|
||||
os.path.join(self.output_root, copy_file))
|
||||
|
||||
def Write(self):
|
||||
"""Writes HTML output."""
|
||||
|
||||
print 'Writing HTML to %s...' % self.output_root
|
||||
|
||||
# Loop through the tree and write subdirs, breadth-first
|
||||
# TODO: switch to depth-first and sort values - makes nicer output?
|
||||
todo = [self.cov.tree]
|
||||
while todo:
|
||||
cov_dir = todo.pop(0)
|
||||
|
||||
# Append subdirs to todo list
|
||||
todo += cov_dir.subdirs.values()
|
||||
|
||||
# Write this subdir
|
||||
self.WriteSubdir(cov_dir)
|
||||
|
||||
# Write files in this subdir
|
||||
for cov_file in cov_dir.files.itervalues():
|
||||
self.WriteFile(cov_file)
|
||||
|
||||
# Write files in root directory
|
||||
self.WriteRoot()
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/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 source scanners."""
|
||||
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class Scanner(object):
|
||||
"""Generic source scanner."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
|
||||
self.re_token = re.compile('#')
|
||||
self.comment_to_eol = ['#']
|
||||
self.comment_start = None
|
||||
self.comment_end = None
|
||||
|
||||
def ScanLines(self, lines):
|
||||
"""Scans the lines for executable statements.
|
||||
|
||||
Args:
|
||||
lines: Iterator returning source lines.
|
||||
|
||||
Returns:
|
||||
An array of line numbers which are executable.
|
||||
"""
|
||||
exe_lines = []
|
||||
lineno = 0
|
||||
|
||||
in_string = None
|
||||
in_comment = None
|
||||
comment_index = None
|
||||
|
||||
for line in lines:
|
||||
lineno += 1
|
||||
in_string_at_start = in_string
|
||||
|
||||
for t in self.re_token.finditer(line):
|
||||
tokenstr = t.groups()[0]
|
||||
|
||||
if in_comment:
|
||||
# Inside a multi-line comment, so look for end token
|
||||
if tokenstr == in_comment:
|
||||
in_comment = None
|
||||
# Replace comment with spaces
|
||||
line = (line[:comment_index]
|
||||
+ ' ' * (t.end(0) - comment_index)
|
||||
+ line[t.end(0):])
|
||||
|
||||
elif in_string:
|
||||
# Inside a string, so look for end token
|
||||
if tokenstr == in_string:
|
||||
in_string = None
|
||||
|
||||
elif tokenstr in self.comment_to_eol:
|
||||
# Single-line comment, so truncate line at start of token
|
||||
line = line[:t.start(0)]
|
||||
break
|
||||
|
||||
elif tokenstr == self.comment_start:
|
||||
# Multi-line comment start - end token is comment_end
|
||||
in_comment = self.comment_end
|
||||
comment_index = t.start(0)
|
||||
|
||||
else:
|
||||
# Starting a string - end token is same as start
|
||||
in_string = tokenstr
|
||||
|
||||
# If still in comment at end of line, remove comment
|
||||
if in_comment:
|
||||
line = line[:comment_index]
|
||||
# Next line, delete from the beginnine
|
||||
comment_index = 0
|
||||
|
||||
# If line-sans-comments is not empty, claim it may be executable
|
||||
if line.strip() or in_string_at_start:
|
||||
exe_lines.append(lineno)
|
||||
|
||||
# Return executable lines
|
||||
return exe_lines
|
||||
|
||||
def Scan(self, filename):
|
||||
"""Reads the file and scans its lines.
|
||||
|
||||
Args:
|
||||
filename: Path to file to scan.
|
||||
|
||||
Returns:
|
||||
An array of line numbers which are executable.
|
||||
"""
|
||||
|
||||
# TODO: All manner of error checking
|
||||
f = None
|
||||
try:
|
||||
f = open(filename, 'rt')
|
||||
return self.ScanLines(f)
|
||||
finally:
|
||||
if f:
|
||||
f.close()
|
||||
|
||||
|
||||
class PythonScanner(Scanner):
|
||||
"""Python source scanner."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
Scanner.__init__(self)
|
||||
|
||||
# TODO: This breaks for strings ending in more than 2 backslashes. Need
|
||||
# a pattern which counts only an odd number of backslashes, so the last
|
||||
# one thus escapes the quote.
|
||||
self.re_token = re.compile(r'(#|\'\'\'|"""|(?<!(?<!\\)\\)["\'])')
|
||||
self.comment_to_eol = ['#']
|
||||
self.comment_start = None
|
||||
self.comment_end = None
|
||||
|
||||
|
||||
class CppScanner(Scanner):
|
||||
"""C / C++ / ObjC / ObjC++ source scanner."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
Scanner.__init__(self)
|
||||
|
||||
# TODO: This breaks for strings ending in more than 2 backslashes. Need
|
||||
# a pattern which counts only an odd number of backslashes, so the last
|
||||
# one thus escapes the quote.
|
||||
self.re_token = re.compile(r'(^\s*#|//|/\*|\*/|(?<!(?<!\\)\\)["\'])')
|
||||
|
||||
# TODO: Treat '\' at EOL as a token, and handle it as continuing the
|
||||
# previous line. That is, if in a comment-to-eol, this line is a comment
|
||||
# too.
|
||||
|
||||
# Note that we treat # at beginning of line as a comment, so that we ignore
|
||||
# preprocessor definitions
|
||||
self.comment_to_eol = ['//', '#']
|
||||
|
||||
self.comment_start = '/*'
|
||||
self.comment_end = '*/'
|
||||
|
||||
|
||||
def ScanFile(filename, language):
|
||||
"""Scans a file for executable lines.
|
||||
|
||||
Args:
|
||||
filename: Path to file to scan.
|
||||
language: Language for file ('C', 'C++', 'python', 'ObjC', 'ObjC++')
|
||||
|
||||
Returns:
|
||||
A list of executable lines, or an empty list if the file was not a handled
|
||||
language.
|
||||
"""
|
||||
|
||||
if language == 'python':
|
||||
return PythonScanner().Scan(filename)
|
||||
elif language in ['C', 'C++', 'ObjC', 'ObjC++']:
|
||||
return CppScanner().Scan(filename)
|
||||
|
||||
# Something we don't handle
|
||||
return []
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/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.
|
||||
|
||||
"""Unit tests for croc_scan.py."""
|
||||
|
||||
#import os
|
||||
import re
|
||||
#import sys
|
||||
#import StringIO
|
||||
import unittest
|
||||
import croc_scan
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestScanner(unittest.TestCase):
|
||||
"""Tests for croc_scan.Scanner."""
|
||||
|
||||
def testInit(self):
|
||||
"""Test __init()__."""
|
||||
s = croc_scan.Scanner()
|
||||
|
||||
self.assertEqual(s.re_token.pattern, '#')
|
||||
self.assertEqual(s.comment_to_eol, ['#'])
|
||||
self.assertEqual(s.comment_start, None)
|
||||
self.assertEqual(s.comment_end, None)
|
||||
|
||||
def testScanLines(self):
|
||||
"""Test ScanLines()."""
|
||||
s = croc_scan.Scanner()
|
||||
# Set up imaginary language:
|
||||
# ':' = comment to EOL
|
||||
# '"' = string start/end
|
||||
# '(' = comment start
|
||||
# ')' = comment end
|
||||
s.re_token = re.compile(r'([\:\"\(\)])')
|
||||
s.comment_to_eol = [':']
|
||||
s.comment_start = '('
|
||||
s.comment_end = ')'
|
||||
|
||||
# No input file = no output lines
|
||||
self.assertEqual(s.ScanLines([]), [])
|
||||
|
||||
# Empty lines and lines with only whitespace are ignored
|
||||
self.assertEqual(s.ScanLines([
|
||||
'', # 1
|
||||
'line', # 2 exe
|
||||
' \t ', # 3
|
||||
]), [2])
|
||||
|
||||
# Comments to EOL are stripped, but not inside strings
|
||||
self.assertEqual(s.ScanLines([
|
||||
'test', # 1 exe
|
||||
' : A comment', # 2
|
||||
'"a : in a string"', # 3 exe
|
||||
'test2 : with comment to EOL', # 4 exe
|
||||
'foo = "a multiline string with an empty line', # 5 exe
|
||||
'', # 6 exe
|
||||
': and a comment-to-EOL character"', # 7 exe
|
||||
': done', # 8
|
||||
]), [1, 3, 4, 5, 6, 7])
|
||||
|
||||
# Test Comment start/stop detection
|
||||
self.assertEqual(s.ScanLines([
|
||||
'( a comment on one line)', # 1
|
||||
'text (with a comment)', # 2 exe
|
||||
'( a comment with a : in the middle)', # 3
|
||||
'( a multi-line', # 4
|
||||
' comment)', # 5
|
||||
'a string "with a ( in it"', # 6 exe
|
||||
'not in a multi-line comment', # 7 exe
|
||||
'(a comment with a " in it)', # 8
|
||||
': not in a string, so this gets stripped', # 9
|
||||
'more text "with an uninteresting string"', # 10 exe
|
||||
]), [2, 6, 7, 10])
|
||||
|
||||
# TODO: Test Scan(). Low priority, since it just wraps ScanLines().
|
||||
|
||||
|
||||
class TestPythonScanner(unittest.TestCase):
|
||||
"""Tests for croc_scan.PythonScanner."""
|
||||
|
||||
def testScanLines(self):
|
||||
"""Test ScanLines()."""
|
||||
s = croc_scan.PythonScanner()
|
||||
|
||||
# No input file = no output lines
|
||||
self.assertEqual(s.ScanLines([]), [])
|
||||
|
||||
self.assertEqual(s.ScanLines([
|
||||
'# a comment', # 1
|
||||
'', # 2
|
||||
'"""multi-line string', # 3 exe
|
||||
'# not a comment', # 4 exe
|
||||
'end of multi-line string"""', # 5 exe
|
||||
' ', # 6
|
||||
'"single string with #comment"', # 7 exe
|
||||
'', # 8
|
||||
'\'\'\'multi-line string, single-quote', # 9 exe
|
||||
'# not a comment', # 10 exe
|
||||
'end of multi-line string\'\'\'', # 11 exe
|
||||
'', # 12
|
||||
'"string with embedded \\" is handled"', # 13 exe
|
||||
'# quoted "', # 14
|
||||
'"\\""', # 15 exe
|
||||
'# quoted backslash', # 16
|
||||
'"\\\\"', # 17 exe
|
||||
'main()', # 18 exe
|
||||
'# end', # 19
|
||||
]), [3, 4, 5, 7, 9, 10, 11, 13, 15, 17, 18])
|
||||
|
||||
|
||||
class TestCppScanner(unittest.TestCase):
|
||||
"""Tests for croc_scan.CppScanner."""
|
||||
|
||||
def testScanLines(self):
|
||||
"""Test ScanLines()."""
|
||||
s = croc_scan.CppScanner()
|
||||
|
||||
# No input file = no output lines
|
||||
self.assertEqual(s.ScanLines([]), [])
|
||||
|
||||
self.assertEqual(s.ScanLines([
|
||||
'// a comment', # 1
|
||||
'# a preprocessor define', # 2
|
||||
'', # 3
|
||||
'\'#\', \'"\'', # 4 exe
|
||||
'', # 5
|
||||
'/* a multi-line comment', # 6
|
||||
'with a " in it', # 7
|
||||
'*/', # 8
|
||||
'', # 9
|
||||
'"a string with /* and \' in it"', # 10 exe
|
||||
'', # 11
|
||||
'"a multi-line string\\', # 12 exe
|
||||
'// not a comment\\', # 13 exe
|
||||
'ending here"', # 14 exe
|
||||
'', # 15
|
||||
'"string with embedded \\" is handled"', # 16 exe
|
||||
'', # 17
|
||||
'main()', # 18 exe
|
||||
'// end', # 19
|
||||
]), [4, 10, 12, 13, 14, 16, 18])
|
||||
|
||||
|
||||
class TestScanFile(unittest.TestCase):
|
||||
"""Tests for croc_scan.ScanFile()."""
|
||||
|
||||
class MockScanner(object):
|
||||
"""Mock scanner."""
|
||||
|
||||
def __init__(self, language):
|
||||
"""Constructor."""
|
||||
self.language = language
|
||||
|
||||
def Scan(self, filename):
|
||||
"""Mock Scan() method."""
|
||||
return 'scan %s %s' % (self.language, filename)
|
||||
|
||||
def MockPythonScanner(self):
|
||||
return self.MockScanner('py')
|
||||
|
||||
def MockCppScanner(self):
|
||||
return self.MockScanner('cpp')
|
||||
|
||||
def setUp(self):
|
||||
"""Per-test setup."""
|
||||
# Hook scanners
|
||||
self.old_python_scanner = croc_scan.PythonScanner
|
||||
self.old_cpp_scanner = croc_scan.CppScanner
|
||||
croc_scan.PythonScanner = self.MockPythonScanner
|
||||
croc_scan.CppScanner = self.MockCppScanner
|
||||
|
||||
def tearDown(self):
|
||||
"""Per-test cleanup."""
|
||||
croc_scan.PythonScanner = self.old_python_scanner
|
||||
croc_scan.CppScanner = self.old_cpp_scanner
|
||||
|
||||
def testScanFile(self):
|
||||
"""Test ScanFile()."""
|
||||
self.assertEqual(croc_scan.ScanFile('foo', 'python'), 'scan py foo')
|
||||
self.assertEqual(croc_scan.ScanFile('bar1', 'C'), 'scan cpp bar1')
|
||||
self.assertEqual(croc_scan.ScanFile('bar2', 'C++'), 'scan cpp bar2')
|
||||
self.assertEqual(croc_scan.ScanFile('bar3', 'ObjC'), 'scan cpp bar3')
|
||||
self.assertEqual(croc_scan.ScanFile('bar4', 'ObjC++'), 'scan cpp bar4')
|
||||
self.assertEqual(croc_scan.ScanFile('bar', 'fortran'), [])
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+213
-130
@@ -32,14 +32,13 @@
|
||||
"""Unit tests for Crocodile."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import StringIO
|
||||
import unittest
|
||||
import croc
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCoverageStats(unittest.TestCase):
|
||||
"""Tests for croc.CoverageStats."""
|
||||
|
||||
@@ -53,23 +52,24 @@ class TestCoverageStats(unittest.TestCase):
|
||||
# Add items
|
||||
c['a'] = 1
|
||||
c['b'] = 0
|
||||
self.assertEqual(c, {'a':1, 'b':0})
|
||||
self.assertEqual(c, {'a': 1, 'b': 0})
|
||||
|
||||
# Add dict with non-overlapping items
|
||||
c.Add({'c':5})
|
||||
self.assertEqual(c, {'a':1, 'b':0, 'c':5})
|
||||
c.Add({'c': 5})
|
||||
self.assertEqual(c, {'a': 1, 'b': 0, 'c': 5})
|
||||
|
||||
# Add dict with overlapping items
|
||||
c.Add({'a':4, 'd':3})
|
||||
self.assertEqual(c, {'a':5, 'b':0, 'c':5, 'd':3})
|
||||
c.Add({'a': 4, 'd': 3})
|
||||
self.assertEqual(c, {'a': 5, 'b': 0, 'c': 5, 'd': 3})
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCoveredFile(unittest.TestCase):
|
||||
"""Tests for croc.CoveredFile."""
|
||||
|
||||
def setUp(self):
|
||||
self.cov_file = croc.CoveredFile('bob.cc', 'source', 'C++')
|
||||
self.cov_file = croc.CoveredFile('bob.cc', group='source', language='C++')
|
||||
|
||||
def testInit(self):
|
||||
"""Test init."""
|
||||
@@ -77,63 +77,76 @@ class TestCoveredFile(unittest.TestCase):
|
||||
|
||||
# Check initial values
|
||||
self.assertEqual(f.filename, 'bob.cc')
|
||||
self.assertEqual(f.group, 'source')
|
||||
self.assertEqual(f.language, 'C++')
|
||||
self.assertEqual(f.attrs, {'group': 'source', 'language': 'C++'})
|
||||
self.assertEqual(f.lines, {})
|
||||
self.assertEqual(f.stats, {})
|
||||
self.assertEqual(f.local_path, None)
|
||||
self.assertEqual(f.in_lcov, False)
|
||||
|
||||
def testUpdateCoverageEmpty(self):
|
||||
"""Test updating coverage when empty."""
|
||||
f = self.cov_file
|
||||
f.UpdateCoverage()
|
||||
self.assertEqual(f.stats, {
|
||||
'lines_executable':0,
|
||||
'lines_instrumented':0,
|
||||
'lines_covered':0,
|
||||
'files_executable':1,
|
||||
'lines_executable': 0,
|
||||
'lines_instrumented': 0,
|
||||
'lines_covered': 0,
|
||||
'files_executable': 1,
|
||||
})
|
||||
|
||||
def testUpdateCoverageExeOnly(self):
|
||||
"""Test updating coverage when no lines are instrumented."""
|
||||
f = self.cov_file
|
||||
f.lines = {1:None, 2:None, 4:None}
|
||||
f.lines = {1: None, 2: None, 4: None}
|
||||
f.UpdateCoverage()
|
||||
self.assertEqual(f.stats, {
|
||||
'lines_executable':3,
|
||||
'lines_instrumented':0,
|
||||
'lines_covered':0,
|
||||
'files_executable':1,
|
||||
'lines_executable': 3,
|
||||
'lines_instrumented': 0,
|
||||
'lines_covered': 0,
|
||||
'files_executable': 1,
|
||||
})
|
||||
|
||||
# Now mark the file instrumented via in_lcov
|
||||
f.in_lcov = True
|
||||
f.UpdateCoverage()
|
||||
self.assertEqual(f.stats, {
|
||||
'lines_executable': 3,
|
||||
'lines_instrumented': 0,
|
||||
'lines_covered': 0,
|
||||
'files_executable': 1,
|
||||
'files_instrumented': 1,
|
||||
})
|
||||
|
||||
def testUpdateCoverageExeAndInstr(self):
|
||||
"""Test updating coverage when no lines are covered."""
|
||||
f = self.cov_file
|
||||
f.lines = {1:None, 2:None, 4:0, 5:0, 7:None}
|
||||
f.lines = {1: None, 2: None, 4: 0, 5: 0, 7: None}
|
||||
f.UpdateCoverage()
|
||||
self.assertEqual(f.stats, {
|
||||
'lines_executable':5,
|
||||
'lines_instrumented':2,
|
||||
'lines_covered':0,
|
||||
'files_executable':1,
|
||||
'files_instrumented':1,
|
||||
'lines_executable': 5,
|
||||
'lines_instrumented': 2,
|
||||
'lines_covered': 0,
|
||||
'files_executable': 1,
|
||||
'files_instrumented': 1,
|
||||
})
|
||||
|
||||
def testUpdateCoverageWhenCovered(self):
|
||||
"""Test updating coverage when lines are covered."""
|
||||
f = self.cov_file
|
||||
f.lines = {1:None, 2:None, 3:1, 4:0, 5:0, 6:1, 7:None}
|
||||
f.lines = {1: None, 2: None, 3: 1, 4: 0, 5: 0, 6: 1, 7: None}
|
||||
f.UpdateCoverage()
|
||||
self.assertEqual(f.stats, {
|
||||
'lines_executable':7,
|
||||
'lines_instrumented':4,
|
||||
'lines_covered':2,
|
||||
'files_executable':1,
|
||||
'files_instrumented':1,
|
||||
'files_covered':1,
|
||||
'lines_executable': 7,
|
||||
'lines_instrumented': 4,
|
||||
'lines_covered': 2,
|
||||
'files_executable': 1,
|
||||
'files_instrumented': 1,
|
||||
'files_covered': 1,
|
||||
})
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCoveredDir(unittest.TestCase):
|
||||
"""Tests for croc.CoveredDir."""
|
||||
|
||||
@@ -148,12 +161,12 @@ class TestCoveredDir(unittest.TestCase):
|
||||
self.assertEqual(d.dirpath, '/a/b/c')
|
||||
self.assertEqual(d.files, {})
|
||||
self.assertEqual(d.subdirs, {})
|
||||
self.assertEqual(d.stats_by_group, {'all':{}})
|
||||
self.assertEqual(d.stats_by_group, {'all': {}})
|
||||
|
||||
def testGetTreeEmpty(self):
|
||||
"""Test getting empty tree."""
|
||||
d = self.cov_dir
|
||||
self.assertEqual(d.GetTree(), '/a/b/c/')
|
||||
self.assertEqual(d.GetTree(), 'c/')
|
||||
|
||||
def testGetTreeStats(self):
|
||||
"""Test getting tree with stats."""
|
||||
@@ -165,8 +178,9 @@ class TestCoveredDir(unittest.TestCase):
|
||||
d.stats_by_group['foo'] = croc.CoverageStats(
|
||||
lines_executable=33, lines_instrumented=22, lines_covered=11)
|
||||
# 'bar' group is skipped because it has no executable lines
|
||||
self.assertEqual(d.GetTree(),
|
||||
'/a/b/c/ all:20/30/50 foo:11/22/33')
|
||||
self.assertEqual(
|
||||
d.GetTree(),
|
||||
'c/ all:20/30/50 foo:11/22/33')
|
||||
|
||||
def testGetTreeSubdir(self):
|
||||
"""Test getting tree with subdirs."""
|
||||
@@ -175,13 +189,13 @@ class TestCoveredDir(unittest.TestCase):
|
||||
d3 = self.cov_dir = croc.CoveredDir('/a/c')
|
||||
d4 = self.cov_dir = croc.CoveredDir('/a/b/d')
|
||||
d5 = self.cov_dir = croc.CoveredDir('/a/b/e')
|
||||
d1.subdirs = {'/a/b':d2, '/a/c':d3}
|
||||
d2.subdirs = {'/a/b/d':d4, '/a/b/e':d5}
|
||||
self.assertEqual(d1.GetTree(),
|
||||
'/a/\n /a/b/\n /a/b/d/\n /a/b/e/\n /a/c/')
|
||||
d1.subdirs = {'/a/b': d2, '/a/c': d3}
|
||||
d2.subdirs = {'/a/b/d': d4, '/a/b/e': d5}
|
||||
self.assertEqual(d1.GetTree(), 'a/\n b/\n d/\n e/\n c/')
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCoverage(unittest.TestCase):
|
||||
"""Tests for croc.Coverage."""
|
||||
|
||||
@@ -197,8 +211,24 @@ class TestCoverage(unittest.TestCase):
|
||||
self.mock_walk_calls.append(src_dir)
|
||||
return self.mock_walk_return
|
||||
|
||||
def MockScanFile(self, filename, language):
|
||||
"""Mock for croc_scan.ScanFile().
|
||||
|
||||
Args:
|
||||
filename: Path to file to scan.
|
||||
language: Language for file.
|
||||
|
||||
Returns:
|
||||
A list of executable lines.
|
||||
"""
|
||||
self.mock_scan_calls.append([filename, language])
|
||||
if filename in self.mock_scan_return:
|
||||
return self.mock_scan_return[filename]
|
||||
else:
|
||||
return self.mock_scan_return['default']
|
||||
|
||||
def setUp(self):
|
||||
"""Per-test setup"""
|
||||
"""Per-test setup."""
|
||||
|
||||
# Empty coverage object
|
||||
self.cov = croc.Coverage()
|
||||
@@ -207,26 +237,25 @@ class TestCoverage(unittest.TestCase):
|
||||
self.cov_minimal = croc.Coverage()
|
||||
self.cov_minimal.AddRoot('/src')
|
||||
self.cov_minimal.AddRoot('c:\\source')
|
||||
self.cov_minimal.AddRule('^#/', include=1, group='my')
|
||||
self.cov_minimal.AddRule('^_/', include=1, group='my')
|
||||
self.cov_minimal.AddRule('.*\\.c$', language='C')
|
||||
self.cov_minimal.AddRule('.*\\.c##$', language='C##') # sharper than thou
|
||||
self.cov_minimal.AddRule('.*\\.c##$', language='C##') # sharper than thou
|
||||
|
||||
# Data for MockWalk()
|
||||
self.mock_walk_calls = []
|
||||
self.mock_walk_return = []
|
||||
|
||||
# Data for MockScanFile()
|
||||
self.mock_scan_calls = []
|
||||
self.mock_scan_return = {'default': [1]}
|
||||
|
||||
def testInit(self):
|
||||
"""Test init."""
|
||||
c = self.cov
|
||||
self.assertEqual(c.files, {})
|
||||
self.assertEqual(c.root_dirs, [])
|
||||
self.assertEqual(c.print_stats, [])
|
||||
|
||||
# Check for the initial subdir rule
|
||||
self.assertEqual(len(c.rules), 1)
|
||||
r0 = c.rules[0]
|
||||
self.assertEqual(r0[0].pattern, '.*/$')
|
||||
self.assertEqual(r0[1:], [None, None, 'subdir'])
|
||||
self.assertEqual(c.rules, [])
|
||||
|
||||
def testAddRoot(self):
|
||||
"""Test AddRoot() and CleanupFilename()."""
|
||||
@@ -255,91 +284,108 @@ class TestCoverage(unittest.TestCase):
|
||||
c.CleanupFilename(os.path.abspath('../../a/b/c')))
|
||||
|
||||
# Replace alt roots
|
||||
c.AddRoot('foo', '#')
|
||||
self.assertEqual(c.CleanupFilename('foo'), '#')
|
||||
self.assertEqual(c.CleanupFilename('foo/bar/baz'), '#/bar/baz')
|
||||
c.AddRoot('foo')
|
||||
self.assertEqual(c.CleanupFilename('foo'), '_')
|
||||
self.assertEqual(c.CleanupFilename('foo/bar/baz'), '_/bar/baz')
|
||||
self.assertEqual(c.CleanupFilename('aaa/foo'), 'aaa/foo')
|
||||
|
||||
# Alt root replacement is applied for all roots
|
||||
c.AddRoot('foo/bar', '#B')
|
||||
self.assertEqual(c.CleanupFilename('foo/bar/baz'), '#B/baz')
|
||||
c.AddRoot('foo/bar', '_B')
|
||||
self.assertEqual(c.CleanupFilename('foo/bar/baz'), '_B/baz')
|
||||
|
||||
# Can use previously defined roots in cleanup
|
||||
c.AddRoot('#/nom/nom/nom', '#CANHAS')
|
||||
c.AddRoot('_/nom/nom/nom', '_CANHAS')
|
||||
self.assertEqual(c.CleanupFilename('foo/nom/nom/nom/cheezburger'),
|
||||
'#CANHAS/cheezburger')
|
||||
'_CANHAS/cheezburger')
|
||||
|
||||
# Verify roots starting with UNC paths or drive letters work, and that
|
||||
# more than one root can point to the same alt_name
|
||||
c.AddRoot('/usr/local/foo', '#FOO')
|
||||
c.AddRoot('D:\\my\\foo', '#FOO')
|
||||
self.assertEqual(c.CleanupFilename('/usr/local/foo/a/b'), '#FOO/a/b')
|
||||
self.assertEqual(c.CleanupFilename('D:\\my\\foo\\c\\d'), '#FOO/c/d')
|
||||
c.AddRoot('/usr/local/foo', '_FOO')
|
||||
c.AddRoot('D:\\my\\foo', '_FOO')
|
||||
self.assertEqual(c.CleanupFilename('/usr/local/foo/a/b'), '_FOO/a/b')
|
||||
self.assertEqual(c.CleanupFilename('D:\\my\\foo\\c\\d'), '_FOO/c/d')
|
||||
|
||||
# Cannot specify a blank alt_name
|
||||
self.assertRaises(ValueError, c.AddRoot, 'some_dir', '')
|
||||
|
||||
def testAddRule(self):
|
||||
"""Test AddRule() and ClassifyFile()."""
|
||||
c = self.cov
|
||||
|
||||
# With only the default rule, nothing gets kept
|
||||
self.assertEqual(c.ClassifyFile('#/src/'), (None, None))
|
||||
self.assertEqual(c.ClassifyFile('#/src/a.c'), (None, None))
|
||||
self.assertEqual(c.ClassifyFile('_/src/'), {})
|
||||
self.assertEqual(c.ClassifyFile('_/src/a.c'), {})
|
||||
|
||||
# Add rules to include a tree and set a default group
|
||||
c.AddRule('^#/src/', include=1, group='source')
|
||||
# Now the subdir matches, but source doesn't, since no languages are
|
||||
# defined yet
|
||||
self.assertEqual(c.ClassifyFile('#/src/'), ('source', 'subdir'))
|
||||
self.assertEqual(c.ClassifyFile('#/notsrc/'), (None, None))
|
||||
self.assertEqual(c.ClassifyFile('#/src/a.c'), (None, None))
|
||||
c.AddRule('^_/src/', include=1, group='source')
|
||||
self.assertEqual(c.ClassifyFile('_/src/'),
|
||||
{'include': 1, 'group': 'source'})
|
||||
self.assertEqual(c.ClassifyFile('_/notsrc/'), {})
|
||||
self.assertEqual(c.ClassifyFile('_/src/a.c'),
|
||||
{'include': 1, 'group': 'source'})
|
||||
|
||||
# Define some languages and groups
|
||||
c.AddRule('.*\\.(c|h)$', language='C')
|
||||
c.AddRule('.*\\.py$', language='Python')
|
||||
c.AddRule('.*_test\\.', group='test')
|
||||
self.assertEqual(c.ClassifyFile('#/src/a.c'), ('source', 'C'))
|
||||
self.assertEqual(c.ClassifyFile('#/src/a.h'), ('source', 'C'))
|
||||
self.assertEqual(c.ClassifyFile('#/src/a.cpp'), (None, None))
|
||||
self.assertEqual(c.ClassifyFile('#/src/a_test.c'), ('test', 'C'))
|
||||
self.assertEqual(c.ClassifyFile('#/src/test_a.c'), ('source', 'C'))
|
||||
self.assertEqual(c.ClassifyFile('#/src/foo/bar.py'), ('source', 'Python'))
|
||||
self.assertEqual(c.ClassifyFile('#/src/test.py'), ('source', 'Python'))
|
||||
self.assertEqual(c.ClassifyFile('_/src/a.c'),
|
||||
{'include': 1, 'group': 'source', 'language': 'C'})
|
||||
self.assertEqual(c.ClassifyFile('_/src/a.h'),
|
||||
{'include': 1, 'group': 'source', 'language': 'C'})
|
||||
self.assertEqual(c.ClassifyFile('_/src/a.cpp'),
|
||||
{'include': 1, 'group': 'source'})
|
||||
self.assertEqual(c.ClassifyFile('_/src/a_test.c'),
|
||||
{'include': 1, 'group': 'test', 'language': 'C'})
|
||||
self.assertEqual(c.ClassifyFile('_/src/test_a.c'),
|
||||
{'include': 1, 'group': 'source', 'language': 'C'})
|
||||
self.assertEqual(c.ClassifyFile('_/src/foo/bar.py'),
|
||||
{'include': 1, 'group': 'source', 'language': 'Python'})
|
||||
self.assertEqual(c.ClassifyFile('_/src/test.py'),
|
||||
{'include': 1, 'group': 'source', 'language': 'Python'})
|
||||
|
||||
# Exclude a path (for example, anything in a build output dir)
|
||||
c.AddRule('.*/build/', include=0)
|
||||
# But add back in a dir which matched the above rule but isn't a build
|
||||
# output dir
|
||||
c.AddRule('#/src/tools/build/', include=1)
|
||||
self.assertEqual(c.ClassifyFile('#/src/build.c'), ('source', 'C'))
|
||||
self.assertEqual(c.ClassifyFile('#/src/build/'), (None, None))
|
||||
self.assertEqual(c.ClassifyFile('#/src/build/a.c'), (None, None))
|
||||
self.assertEqual(c.ClassifyFile('#/src/tools/build/'), ('source', 'subdir'))
|
||||
self.assertEqual(c.ClassifyFile('#/src/tools/build/t.c'), ('source', 'C'))
|
||||
c.AddRule('_/src/tools/build/', include=1)
|
||||
self.assertEqual(c.ClassifyFile('_/src/build.c').get('include'), 1)
|
||||
self.assertEqual(c.ClassifyFile('_/src/build/').get('include'), 0)
|
||||
self.assertEqual(c.ClassifyFile('_/src/build/a.c').get('include'), 0)
|
||||
self.assertEqual(c.ClassifyFile('_/src/tools/build/').get('include'), 1)
|
||||
self.assertEqual(c.ClassifyFile('_/src/tools/build/t.c').get('include'), 1)
|
||||
|
||||
def testGetCoveredFile(self):
|
||||
"""Test GetCoveredFile()."""
|
||||
c = self.cov_minimal
|
||||
|
||||
# Not currently any covered files
|
||||
self.assertEqual(c.GetCoveredFile('#/a.c'), None)
|
||||
self.assertEqual(c.GetCoveredFile('_/a.c'), None)
|
||||
|
||||
# Add some files
|
||||
a_c = c.GetCoveredFile('#/a.c', add=True)
|
||||
b_c = c.GetCoveredFile('#/b.c##', add=True)
|
||||
self.assertEqual(a_c.filename, '#/a.c')
|
||||
self.assertEqual(a_c.group, 'my')
|
||||
self.assertEqual(a_c.language, 'C')
|
||||
self.assertEqual(b_c.filename, '#/b.c##')
|
||||
self.assertEqual(b_c.group, 'my')
|
||||
self.assertEqual(b_c.language, 'C##')
|
||||
a_c = c.GetCoveredFile('_/a.c', add=True)
|
||||
b_c = c.GetCoveredFile('_/b.c##', add=True)
|
||||
self.assertEqual(a_c.filename, '_/a.c')
|
||||
self.assertEqual(a_c.attrs, {'include': 1, 'group': 'my', 'language': 'C'})
|
||||
self.assertEqual(b_c.filename, '_/b.c##')
|
||||
self.assertEqual(b_c.attrs,
|
||||
{'include': 1, 'group': 'my', 'language': 'C##'})
|
||||
|
||||
# Specifying the same filename should return the existing object
|
||||
self.assertEqual(c.GetCoveredFile('#/a.c'), a_c)
|
||||
self.assertEqual(c.GetCoveredFile('#/a.c', add=True), a_c)
|
||||
self.assertEqual(c.GetCoveredFile('_/a.c'), a_c)
|
||||
self.assertEqual(c.GetCoveredFile('_/a.c', add=True), a_c)
|
||||
|
||||
# Filenames get cleaned on the way in, as do root paths
|
||||
self.assertEqual(c.GetCoveredFile('/src/a.c'), a_c)
|
||||
self.assertEqual(c.GetCoveredFile('c:\\source\\a.c'), a_c)
|
||||
|
||||
# TODO: Make sure that covered files require language, group, and include
|
||||
# (since that checking is now done in GetCoveredFile() rather than
|
||||
# ClassifyFile())
|
||||
|
||||
def testRemoveCoveredFile(self):
|
||||
"""Test RemoveCoveredFile()."""
|
||||
# TODO: TEST ME!
|
||||
|
||||
def testParseLcov(self):
|
||||
"""Test ParseLcovData()."""
|
||||
c = self.cov_minimal
|
||||
@@ -350,7 +396,7 @@ class TestCoverage(unittest.TestCase):
|
||||
'SF:/src/a.c',
|
||||
'DA:10,1',
|
||||
'DA:11,0',
|
||||
'DA:12,1 \n', # Trailing whitespace should get stripped
|
||||
'DA:12,1 \n', # Trailing whitespace should get stripped
|
||||
'end_of_record',
|
||||
# File we should ignore
|
||||
'SF:/not_src/a.c',
|
||||
@@ -368,13 +414,16 @@ class TestCoverage(unittest.TestCase):
|
||||
'SF:/src/b.c',
|
||||
'DA:50,0',
|
||||
'end_of_record',
|
||||
# Empty file (instrumented but no executable lines)
|
||||
'SF:c:\\source\\c.c',
|
||||
'end_of_record',
|
||||
])
|
||||
|
||||
# We should know about two files
|
||||
self.assertEqual(sorted(c.files), ['#/a.c', '#/b.c'])
|
||||
# We should know about three files
|
||||
self.assertEqual(sorted(c.files), ['_/a.c', '_/b.c', '_/c.c'])
|
||||
|
||||
# Check expected contents
|
||||
a_c = c.GetCoveredFile('#/a.c')
|
||||
a_c = c.GetCoveredFile('_/a.c')
|
||||
self.assertEqual(a_c.lines, {10: 1, 11: 0, 12: 1, 30: 1})
|
||||
self.assertEqual(a_c.stats, {
|
||||
'files_executable': 1,
|
||||
@@ -384,7 +433,9 @@ class TestCoverage(unittest.TestCase):
|
||||
'lines_executable': 4,
|
||||
'lines_covered': 3,
|
||||
})
|
||||
b_c = c.GetCoveredFile('#/b.c')
|
||||
self.assertEqual(a_c.in_lcov, True)
|
||||
|
||||
b_c = c.GetCoveredFile('_/b.c')
|
||||
self.assertEqual(b_c.lines, {50: 0})
|
||||
self.assertEqual(b_c.stats, {
|
||||
'files_executable': 1,
|
||||
@@ -393,6 +444,23 @@ class TestCoverage(unittest.TestCase):
|
||||
'lines_executable': 1,
|
||||
'lines_covered': 0,
|
||||
})
|
||||
self.assertEqual(b_c.in_lcov, True)
|
||||
|
||||
c_c = c.GetCoveredFile('_/c.c')
|
||||
self.assertEqual(c_c.lines, {})
|
||||
self.assertEqual(c_c.stats, {
|
||||
'files_executable': 1,
|
||||
'files_instrumented': 1,
|
||||
'lines_instrumented': 0,
|
||||
'lines_executable': 0,
|
||||
'lines_covered': 0,
|
||||
})
|
||||
self.assertEqual(c_c.in_lcov, True)
|
||||
|
||||
# TODO: Test that files are marked as instrumented if they come from lcov,
|
||||
# even if they don't have any instrumented lines. (and that in_lcov is set
|
||||
# for those files - probably should set that via some method rather than
|
||||
# directly...)
|
||||
|
||||
def testGetStat(self):
|
||||
"""Test GetStat() and PrintStat()."""
|
||||
@@ -413,10 +481,10 @@ class TestCoverage(unittest.TestCase):
|
||||
}
|
||||
|
||||
# Test missing stats and groups
|
||||
self.assertRaises(croc.CoverageStatError, c.GetStat, 'nosuch')
|
||||
self.assertRaises(croc.CoverageStatError, c.GetStat, 'baz')
|
||||
self.assertRaises(croc.CoverageStatError, c.GetStat, 'foo', group='tests')
|
||||
self.assertRaises(croc.CoverageStatError, c.GetStat, 'foo', group='nosuch')
|
||||
self.assertRaises(croc.CrocStatError, c.GetStat, 'nosuch')
|
||||
self.assertRaises(croc.CrocStatError, c.GetStat, 'baz')
|
||||
self.assertRaises(croc.CrocStatError, c.GetStat, 'foo', group='tests')
|
||||
self.assertRaises(croc.CrocStatError, c.GetStat, 'foo', group='nosuch')
|
||||
|
||||
# Test returning defaults
|
||||
self.assertEqual(c.GetStat('nosuch', default=13), 13)
|
||||
@@ -434,13 +502,13 @@ class TestCoverage(unittest.TestCase):
|
||||
self.assertEqual(c.GetStat('100.0 * count_a / count_b', group='tests'),
|
||||
40.0)
|
||||
# Should catch eval errors
|
||||
self.assertRaises(croc.CoverageStatError, c.GetStat, '100 / 0')
|
||||
self.assertRaises(croc.CoverageStatError, c.GetStat, 'count_a -')
|
||||
self.assertRaises(croc.CrocStatError, c.GetStat, '100 / 0')
|
||||
self.assertRaises(croc.CrocStatError, c.GetStat, 'count_a -')
|
||||
|
||||
# Test nested stats via S()
|
||||
self.assertEqual(c.GetStat('count_a - S("count_a", group="tests")'), 8)
|
||||
self.assertRaises(croc.CoverageStatError, c.GetStat, 'S()')
|
||||
self.assertRaises(croc.CoverageStatError, c.GetStat, 'S("nosuch")')
|
||||
self.assertRaises(croc.CrocStatError, c.GetStat, 'S()')
|
||||
self.assertRaises(croc.CrocStatError, c.GetStat, 'S("nosuch")')
|
||||
|
||||
# Test PrintStat()
|
||||
# We won't see the first print, but at least verify it doesn't assert
|
||||
@@ -476,14 +544,14 @@ GetStat('nosuch') = 42
|
||||
c.AddConfig("""{
|
||||
'roots' : [
|
||||
{'root' : '/foo'},
|
||||
{'root' : '/bar', 'altname' : '#BAR'},
|
||||
{'root' : '/bar', 'altname' : 'BAR'},
|
||||
],
|
||||
'rules' : [
|
||||
{'regexp' : '^#', 'group' : 'apple'},
|
||||
{'regexp' : '^_/', 'group' : 'apple'},
|
||||
{'regexp' : 're2', 'include' : 1, 'language' : 'elvish'},
|
||||
],
|
||||
'lcov_files' : ['a.lcov', 'b.lcov'],
|
||||
'add_files' : ['/src', '#BAR/doo'],
|
||||
'add_files' : ['/src', 'BAR/doo'],
|
||||
'print_stats' : [
|
||||
{'stat' : 'count_a'},
|
||||
{'stat' : 'count_b', 'group' : 'tests'},
|
||||
@@ -492,8 +560,8 @@ GetStat('nosuch') = 42
|
||||
}""", lcov_queue=lcov_queue, addfiles_queue=addfiles_queue)
|
||||
|
||||
self.assertEqual(lcov_queue, ['a.lcov', 'b.lcov'])
|
||||
self.assertEqual(addfiles_queue, ['/src', '#BAR/doo'])
|
||||
self.assertEqual(c.root_dirs, [['/foo', '#'], ['/bar', '#BAR']])
|
||||
self.assertEqual(addfiles_queue, ['/src', 'BAR/doo'])
|
||||
self.assertEqual(c.root_dirs, [['/foo', '_'], ['/bar', 'BAR']])
|
||||
self.assertEqual(c.print_stats, [
|
||||
{'stat': 'count_a'},
|
||||
{'stat': 'count_b', 'group': 'tests'},
|
||||
@@ -501,30 +569,35 @@ GetStat('nosuch') = 42
|
||||
# Convert compiled re's back to patterns for comparison
|
||||
rules = [[r[0].pattern] + r[1:] for r in c.rules]
|
||||
self.assertEqual(rules, [
|
||||
['.*/$', None, None, 'subdir'],
|
||||
['^#', None, 'apple', None],
|
||||
['re2', 1, None, 'elvish'],
|
||||
['^_/', {'group': 'apple'}],
|
||||
['re2', {'include': 1, 'language': 'elvish'}],
|
||||
])
|
||||
|
||||
def testAddFilesSimple(self):
|
||||
"""Test AddFiles() simple call."""
|
||||
c = self.cov_minimal
|
||||
c.add_files_walk = self.MockWalk
|
||||
c.scan_file = self.MockScanFile
|
||||
|
||||
c.AddFiles('/a/b/c')
|
||||
self.assertEqual(self.mock_walk_calls, ['/a/b/c'])
|
||||
self.assertEqual(self.mock_scan_calls, [])
|
||||
self.assertEqual(c.files, {})
|
||||
|
||||
def testAddFilesRootMap(self):
|
||||
"""Test AddFiles() with root mappings."""
|
||||
c = self.cov_minimal
|
||||
c.add_files_walk = self.MockWalk
|
||||
c.AddRoot('#/subdir', '#SUBDIR')
|
||||
c.scan_file = self.MockScanFile
|
||||
|
||||
# AddFiles() should replace the '#SUBDIR' alt_name, then match both
|
||||
# possible roots for the '#' alt_name.
|
||||
c.AddFiles('#SUBDIR/foo')
|
||||
c.AddRoot('_/subdir', 'SUBDIR')
|
||||
|
||||
# AddFiles() should replace the 'SUBDIR' alt_name, then match both
|
||||
# possible roots for the '_' alt_name.
|
||||
c.AddFiles('SUBDIR/foo')
|
||||
self.assertEqual(self.mock_walk_calls,
|
||||
['/src/subdir/foo', 'c:/source/subdir/foo'])
|
||||
self.assertEqual(self.mock_scan_calls, [])
|
||||
self.assertEqual(c.files, {})
|
||||
|
||||
def testAddFilesNonEmpty(self):
|
||||
@@ -532,16 +605,20 @@ GetStat('nosuch') = 42
|
||||
|
||||
c = self.cov_minimal
|
||||
c.add_files_walk = self.MockWalk
|
||||
c.scan_file = self.MockScanFile
|
||||
|
||||
# Add a rule to exclude a subdir
|
||||
c.AddRule('^#/proj1/excluded/', include=0)
|
||||
c.AddRule('^_/proj1/excluded/', include=0)
|
||||
|
||||
# Set data for mock walk
|
||||
# Add a rule to exclude adding some fiels
|
||||
c.AddRule('.*noscan.c$', add_if_missing=0)
|
||||
|
||||
# Set data for mock walk and scan
|
||||
self.mock_walk_return = [
|
||||
[
|
||||
'/src/proj1',
|
||||
['excluded', 'subdir'],
|
||||
['a.c', 'no.f', 'yes.c'],
|
||||
['a.c', 'no.f', 'yes.c', 'noexe.c', 'bob_noscan.c'],
|
||||
],
|
||||
[
|
||||
'/src/proj1/subdir',
|
||||
@@ -550,15 +627,24 @@ GetStat('nosuch') = 42
|
||||
],
|
||||
]
|
||||
|
||||
# Add a file with no executable lines; it should be scanned but not added
|
||||
self.mock_scan_return['/src/proj1/noexe.c'] = []
|
||||
|
||||
c.AddFiles('/src/proj1')
|
||||
|
||||
self.assertEqual(self.mock_walk_calls, ['/src/proj1'])
|
||||
self.assertEqual(self.mock_scan_calls, [
|
||||
['/src/proj1/a.c', 'C'],
|
||||
['/src/proj1/yes.c', 'C'],
|
||||
['/src/proj1/noexe.c', 'C'],
|
||||
['/src/proj1/subdir/cherry.c', 'C'],
|
||||
])
|
||||
|
||||
# Include files from the main dir and subdir
|
||||
self.assertEqual(sorted(c.files), [
|
||||
'#/proj1/a.c',
|
||||
'#/proj1/subdir/cherry.c',
|
||||
'#/proj1/yes.c'])
|
||||
'_/proj1/a.c',
|
||||
'_/proj1/subdir/cherry.c',
|
||||
'_/proj1/yes.c'])
|
||||
|
||||
# Excluded dir should have been pruned from the mock walk data dirnames.
|
||||
# In the real os.walk() call this prunes the walk.
|
||||
@@ -568,9 +654,6 @@ GetStat('nosuch') = 42
|
||||
"""Test UpdateTreeStats()."""
|
||||
|
||||
c = self.cov_minimal
|
||||
|
||||
|
||||
|
||||
c.AddRule('.*_test', group='test')
|
||||
|
||||
# Fill the files list
|
||||
@@ -593,7 +676,7 @@ GetStat('nosuch') = 42
|
||||
t = c.tree
|
||||
self.assertEqual(t.dirpath, '')
|
||||
self.assertEqual(sorted(t.files), [])
|
||||
self.assertEqual(sorted(t.subdirs), ['#'])
|
||||
self.assertEqual(sorted(t.subdirs), ['_'])
|
||||
self.assertEqual(t.stats_by_group, {
|
||||
'all': {
|
||||
'files_covered': 3,
|
||||
@@ -621,8 +704,8 @@ GetStat('nosuch') = 42
|
||||
},
|
||||
})
|
||||
|
||||
t = t.subdirs['#']
|
||||
self.assertEqual(t.dirpath, '#')
|
||||
t = t.subdirs['_']
|
||||
self.assertEqual(t.dirpath, '_')
|
||||
self.assertEqual(sorted(t.files), ['a.c', 'a_test.c'])
|
||||
self.assertEqual(sorted(t.subdirs), ['foo'])
|
||||
self.assertEqual(t.stats_by_group, {
|
||||
@@ -653,7 +736,7 @@ GetStat('nosuch') = 42
|
||||
})
|
||||
|
||||
t = t.subdirs['foo']
|
||||
self.assertEqual(t.dirpath, 'foo')
|
||||
self.assertEqual(t.dirpath, '_/foo')
|
||||
self.assertEqual(sorted(t.files), ['b.c', 'b_test.c'])
|
||||
self.assertEqual(sorted(t.subdirs), [])
|
||||
self.assertEqual(t.stats_by_group, {
|
||||
|
||||
Referência em uma Nova Issue
Bloquear um usuário