summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorrspangler@google.com <rspangler@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2009-05-13 18:31:04 +0000
committerrspangler@google.com <rspangler@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2009-05-13 18:31:04 +0000
commit916225c4298662c51f7a3218e283da7816d5844d (patch)
treeb88e01cb8c90289604b5c0363a7b26af78c9da25 /tools
parent616d628496d7940cecfe5b42a73215cbbbff9f1d (diff)
downloadchromium_src-916225c4298662c51f7a3218e283da7816d5844d.zip
chromium_src-916225c4298662c51f7a3218e283da7816d5844d.tar.gz
chromium_src-916225c4298662c51f7a3218e283da7816d5844d.tar.bz2
Add croc code coverage tool. (Same change as yesterday, but now made in the
writable repository) Review URL: http://codereview.chromium.org/113346 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@15974 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools')
-rw-r--r--tools/code_coverage/croc.py669
-rw-r--r--tools/code_coverage/croc_test.py694
2 files changed, 1363 insertions, 0 deletions
diff --git a/tools/code_coverage/croc.py b/tools/code_coverage/croc.py
new file mode 100644
index 0000000..b2bd33b
--- /dev/null
+++ b/tools/code_coverage/croc.py
@@ -0,0 +1,669 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Crocodile - compute coverage numbers for Chrome coverage dashboard."""
+
+import os
+import re
+import sys
+from optparse import OptionParser
+
+
+class CoverageError(Exception):
+ """Coverage error."""
+
+class CoverageStatError(CoverageError):
+ """Error evaluating coverage stat."""
+
+#------------------------------------------------------------------------------
+
+
+class CoverageStats(dict):
+ """Coverage statistics."""
+
+ def Add(self, coverage_stats):
+ """Adds a contribution from another coverage stats dict.
+
+ Args:
+ coverage_stats: Statistics to add to this one.
+ """
+ for k, v in coverage_stats.iteritems():
+ if k in self:
+ self[k] = self[k] + v
+ else:
+ self[k] = v
+
+#------------------------------------------------------------------------------
+
+
+class CoveredFile(object):
+ """Information about a single covered file."""
+
+ def __init__(self, filename, group, language):
+ """Constructor.
+
+ Args:
+ filename: Full path to file, '/'-delimited.
+ group: Group file belongs to.
+ language: Language for file.
+ """
+ self.filename = filename
+ self.group = group
+ self.language = language
+
+ # No coverage data for file yet
+ self.lines = {} # line_no -> None=executable, 0=instrumented, 1=covered
+ self.stats = CoverageStats()
+
+ def UpdateCoverage(self):
+ """Updates the coverage summary based on covered lines."""
+ exe = instr = cov = 0
+ for l in self.lines.itervalues():
+ exe += 1
+ if l is not None:
+ instr += 1
+ if l == 1:
+ cov += 1
+
+ # Add stats that always exist
+ self.stats = CoverageStats(lines_executable=exe,
+ lines_instrumented=instr,
+ lines_covered=cov,
+ files_executable=1)
+
+ # Add conditional stats
+ if cov:
+ self.stats['files_covered'] = 1
+ if instr:
+ self.stats['files_instrumented'] = 1
+
+
+#------------------------------------------------------------------------------
+
+
+class CoveredDir(object):
+ """Information about a directory containing covered files."""
+
+ def __init__(self, dirpath):
+ """Constructor.
+
+ Args:
+ dirpath: Full path of directory, '/'-delimited.
+ """
+ self.dirpath = dirpath
+
+ # List of covered files directly in this dir, indexed by filename (not
+ # full path)
+ self.files = {}
+
+ # List of subdirs, indexed by filename (not full path)
+ self.subdirs = {}
+
+ # Dict of CoverageStats objects summarizing all children, indexed by group
+ self.stats_by_group = {'all':CoverageStats()}
+ # TODO: by language
+
+ def GetTree(self, indent=''):
+ """Recursively gets stats for the directory and its children.
+
+ Args:
+ indent: indent prefix string.
+
+ Returns:
+ The tree as a string.
+ """
+ dest = []
+
+ # Compile all groupstats
+ groupstats = []
+ for group in sorted(self.stats_by_group):
+ s = self.stats_by_group[group]
+ if not s.get('lines_executable'):
+ continue # Skip groups with no executable lines
+ groupstats.append('%s:%d/%d/%d' % (
+ group, s.get('lines_covered', 0),
+ s.get('lines_instrumented', 0),
+ s.get('lines_executable', 0)))
+
+ outline = '%s%-30s %s' % (indent,
+ self.dirpath + '/', ' '.join(groupstats))
+ dest.append(outline.rstrip())
+
+ for d in sorted(self.subdirs):
+ dest.append(self.subdirs[d].GetTree(indent=indent + ' '))
+
+ return '\n'.join(dest)
+
+#------------------------------------------------------------------------------
+
+
+class Coverage(object):
+ """Code coverage for a group of files."""
+
+ def __init__(self):
+ """Constructor."""
+ self.files = {} # Map filename --> CoverageFile
+ self.root_dirs = [] # (root, altname)
+ self.rules = [] # (regexp, include, group, language)
+ self.tree = CoveredDir('')
+ self.print_stats = [] # Dicts of args to PrintStat()
+
+ self.add_files_walk = os.walk # Walk function for AddFiles()
+
+ # Must specify subdir rule, or AddFiles() won't find any files because it
+ # will prune out all the subdirs. Since subdirs never match any code,
+ # they won't be reported in other stats, so this is ok.
+ self.AddRule('.*/$', language='subdir')
+
+
+ def CleanupFilename(self, filename):
+ """Cleans up a filename.
+
+ Args:
+ filename: Input filename.
+
+ Returns:
+ The cleaned up filename.
+
+ Changes all path separators to '/'.
+ Makes relative paths (those starting with '../' or './' absolute.
+ Replaces all instances of root dirs with alternate names.
+ """
+ # Change path separators
+ filename = filename.replace('\\', '/')
+
+ # If path is relative, make it absolute
+ # TODO: Perhaps we should default to relative instead, and only understand
+ # absolute to be files starting with '\', '/', or '[A-Za-z]:'?
+ if filename.split('/')[0] in ('.', '..'):
+ filename = os.path.abspath(filename).replace('\\', '/')
+
+ # Replace alternate roots
+ for root, alt_name in self.root_dirs:
+ filename = re.sub('^' + re.escape(root) + '(?=(/|$))',
+ alt_name, filename)
+ return filename
+
+ def ClassifyFile(self, filename):
+ """Applies rules to a filename, to see if we care about it.
+
+ Args:
+ filename: Input filename.
+
+ Returns:
+ (None, None) if the file is not included or has no group or has no
+ language. Otherwise, a 2-tuple containing:
+ The group for the file (for example, 'source' or 'test').
+ The language of the file.
+ """
+ include = False
+ group = None
+ language = None
+
+ # Process all rules
+ for regexp, rule_include, rule_group, rule_language in self.rules:
+ if regexp.match(filename):
+ # include/exclude source
+ if rule_include is not None:
+ include = rule_include
+ if rule_group is not None:
+ group = rule_group
+ if rule_language is not None:
+ language = rule_language
+
+ # TODO: Should have a debug mode which prints files which aren't excluded
+ # and why (explicitly excluded, no type, no language, etc.)
+
+ # TODO: Files can belong to multiple groups?
+ # (test/source)
+ # (mac/pc/win)
+ # (media_test/all_tests)
+ # (small/med/large)
+ # How to handle that?
+
+ # Return classification if the file is included and has a group and
+ # language
+ if include and group and language:
+ return group, language
+ else:
+ return None, None
+
+ def AddRoot(self, root_path, alt_name='#'):
+ """Adds a root directory.
+
+ Args:
+ root_path: Root directory to add.
+ alt_name: If specified, name of root dir
+ """
+ # Clean up root path based on existing rules
+ self.root_dirs.append([self.CleanupFilename(root_path), alt_name])
+
+ def AddRule(self, path_regexp, include=None, group=None, language=None):
+ """Adds a rule.
+
+ Args:
+ path_regexp: Regular expression to match for filenames. These are
+ matched after root directory replacement.
+ include: If True, includes matches; if False, excludes matches. Ignored
+ if None.
+ group: If not None, sets group to apply to matches.
+ language: If not None, sets file language to apply to matches.
+ """
+ # Compile regexp ahead of time
+ self.rules.append([re.compile(path_regexp), include, group, language])
+
+ def GetCoveredFile(self, filename, add=False):
+ """Gets the CoveredFile object for the filename.
+
+ Args:
+ filename: Name of file to find.
+ add: If True, will add the file if it's not present. This applies the
+ transformations from AddRoot() and AddRule(), and only adds the file
+ if a rule includes it, and it has a group and language.
+
+ Returns:
+ The matching CoveredFile object, or None if not present.
+ """
+ # Clean filename
+ filename = self.CleanupFilename(filename)
+
+ # Check for existing match
+ if filename in self.files:
+ return self.files[filename]
+
+ # File isn't one we know about. If we can't add it, give up.
+ if not add:
+ return None
+
+ # Check rules to see if file can be added
+ group, language = self.ClassifyFile(filename)
+ if not group:
+ return None
+
+ # Add the file
+ f = CoveredFile(filename, group, language)
+ self.files[filename] = f
+
+ # Return the newly covered file
+ return f
+
+ def ParseLcovData(self, lcov_data):
+ """Adds coverage from LCOV-formatted data.
+
+ Args:
+ lcov_data: An iterable returning lines of data in LCOV format. For
+ example, a file or list of strings.
+ """
+ cov_file = None
+ cov_lines = None
+ for line in lcov_data:
+ line = line.strip()
+ if line.startswith('SF:'):
+ # Start of data for a new file; payload is filename
+ cov_file = self.GetCoveredFile(line[3:], add=True)
+ if cov_file:
+ cov_lines = cov_file.lines
+ elif not cov_file:
+ # Inside data for a file we don't care about - so skip it
+ pass
+ elif line.startswith('DA:'):
+ # Data point - that is, an executable line in current file
+ line_no, is_covered = map(int, line[3:].split(','))
+ if is_covered:
+ # Line is covered
+ cov_lines[line_no] = 1
+ elif cov_lines.get(line_no) != 1:
+ # Line is not covered, so track it as uncovered
+ cov_lines[line_no] = 0
+ elif line == 'end_of_record':
+ cov_file.UpdateCoverage()
+ cov_file = None
+ # (else ignore other line types)
+
+ def ParseLcovFile(self, input_filename):
+ """Adds coverage data from a .lcov file.
+
+ Args:
+ input_filename: Input filename.
+ """
+ # TODO: All manner of error checking
+ lcov_file = None
+ try:
+ lcov_file = open(input_filename, 'rt')
+ self.ParseLcovData(lcov_file)
+ finally:
+ if lcov_file:
+ lcov_file.close()
+
+ def GetStat(self, stat, group='all', default=None):
+ """Gets a statistic from the coverage object.
+
+ Args:
+ stat: Statistic to get. May also be an evaluatable python expression,
+ using the stats. For example, 'stat1 - stat2'.
+ group: File group to match; if 'all', matches all groups.
+ default: Value to return if there was an error evaluating the stat. For
+ example, if the stat does not exist. If None, raises
+ CoverageStatError.
+
+ Returns:
+ The evaluated stat, or None if error.
+
+ Raises:
+ CoverageStatError: Error evaluating stat.
+ """
+ # TODO: specify a subdir to get the stat from, then walk the tree to
+ # print the stats from just that subdir
+
+ # Make sure the group exists
+ if group not in self.tree.stats_by_group:
+ if default is None:
+ raise CoverageStatError('Group %r not found.' % group)
+ else:
+ return default
+
+ stats = self.tree.stats_by_group[group]
+ try:
+ return eval(stat, {'__builtins__':{'S':self.GetStat}}, stats)
+ except Exception, e:
+ if default is None:
+ raise CoverageStatError('Error evaluating stat %r: %s' % (stat, e))
+ else:
+ return default
+
+ def PrintStat(self, stat, format=None, outfile=sys.stdout, **kwargs):
+ """Prints a statistic from the coverage object.
+
+ Args:
+ stat: Statistic to get. May also be an evaluatable python expression,
+ using the stats. For example, 'stat1 - stat2'.
+ format: Format string to use when printing stat. If None, prints the
+ stat and its evaluation.
+ outfile: File stream to output stat to; defaults to stdout.
+ kwargs: Additional args to pass to GetStat().
+ """
+ s = self.GetStat(stat, **kwargs)
+ if format is None:
+ outfile.write('GetStat(%r) = %s\n' % (stat, s))
+ else:
+ outfile.write(format % s + '\n')
+
+ def AddFiles(self, src_dir):
+ """Adds files to coverage information.
+
+ LCOV files only contains files which are compiled and instrumented as part
+ of running coverage. This function finds missing files and adds them.
+
+ Args:
+ src_dir: Directory on disk at which to start search. May be a relative
+ path on disk starting with '.' or '..', or an absolute path, or a
+ path relative to an alt_name for one of the roots
+ (for example, '#/src'). If the alt_name matches more than one root,
+ all matches will be attempted.
+
+ Note that dirs not underneath one of the root dirs and covered by an
+ inclusion rule will be ignored.
+ """
+ # Check for root dir alt_names in the path and replace with the actual
+ # root dirs, then recurse.
+ found_root = False
+ for root, alt_name in self.root_dirs:
+ replaced_root = re.sub('^' + re.escape(alt_name) + '(?=(/|$))', root,
+ src_dir)
+ if replaced_root != src_dir:
+ found_root = True
+ self.AddFiles(replaced_root)
+ if found_root:
+ return # Replaced an alt_name with a root_dir, so already recursed.
+
+ for (dirpath, dirnames, filenames) in self.add_files_walk(src_dir):
+ # Make a copy of the dirnames list so we can modify the original to
+ # prune subdirs we don't need to walk.
+ for d in list(dirnames):
+ # Add trailing '/' to directory names so dir-based regexps can match
+ # '/' instead of needing to specify '(/|$)'.
+ dpath = self.CleanupFilename(dirpath + '/' + d) + '/'
+ group, language = self.ClassifyFile(dpath)
+ if not group:
+ # Directory has been excluded, so don't traverse it
+ # TODO: Document the slight weirdness caused by this: If you
+ # AddFiles('./A'), and the rules include 'A/B/C/D' but not 'A/B',
+ # then it won't recurse into './A/B' so won't find './A/B/C/D'.
+ # Workarounds are to AddFiles('./A/B/C/D') or AddFiles('./A/B/C').
+ # The latter works because it explicitly walks the contents of the
+ # path passed to AddFiles(), so it finds './A/B/C/D'.
+ dirnames.remove(d)
+
+ for f in filenames:
+ covf = self.GetCoveredFile(dirpath + '/' + f, add=True)
+ # TODO: scan files for executable lines. Add these to the file as
+ # 'executable', but not 'instrumented' or 'covered'.
+ # TODO: if a file has no executable lines, don't add it.
+ if covf:
+ covf.UpdateCoverage()
+
+ def AddConfig(self, config_data, lcov_queue=None, addfiles_queue=None):
+ """Adds JSON-ish config data.
+
+ Args:
+ config_data: Config data string.
+ lcov_queue: If not None, object to append lcov_files to instead of
+ parsing them immediately.
+ addfiles_queue: If not None, object to append add_files to instead of
+ processing them immediately.
+ """
+ # TODO: All manner of error checking
+ cfg = eval(config_data, {'__builtins__':{}}, {})
+
+ for rootdict in cfg.get('roots', []):
+ self.AddRoot(rootdict['root'], alt_name=rootdict.get('altname', '#'))
+
+ for ruledict in cfg.get('rules', []):
+ self.AddRule(ruledict['regexp'],
+ include=ruledict.get('include'),
+ group=ruledict.get('group'),
+ language=ruledict.get('language'))
+
+ for add_lcov in cfg.get('lcov_files', []):
+ if lcov_queue is not None:
+ lcov_queue.append(add_lcov)
+ else:
+ self.ParseLcovFile(add_lcov)
+
+ for add_path in cfg.get('add_files', []):
+ if addfiles_queue is not None:
+ addfiles_queue.append(add_path)
+ else:
+ self.AddFiles(add_path)
+
+ self.print_stats += cfg.get('print_stats', [])
+
+ def ParseConfig(self, filename, **kwargs):
+ """Parses a configuration file.
+
+ Args:
+ filename: Config filename.
+ """
+ # TODO: All manner of error checking
+ f = None
+ try:
+ f = open(filename, 'rt')
+ # Need to strip CR's from CRLF-terminated lines or posix systems can't
+ # eval the data.
+ config_data = f.read().replace('\r\n', '\n')
+ # TODO: some sort of include syntax. Needs to be done at string-time
+ # rather than at eval()-time, so that it's possible to include parts of
+ # dicts. Path from a file to its include should be relative to the dir
+ # containing the file.
+ self.AddConfig(config_data, **kwargs)
+ finally:
+ if f:
+ f.close()
+
+ def UpdateTreeStats(self):
+ """Recalculates the tree stats from the currently covered files.
+
+ Also calculates coverage summary for files."""
+ self.tree = CoveredDir('')
+ for cov_file in self.files.itervalues():
+ # Add the file to the tree
+ # TODO: Don't really need to create the tree unless we're creating HTML
+ fdirs = cov_file.filename.split('/')
+ parent = self.tree
+ ancestors = [parent]
+ for d in fdirs[:-1]:
+ if d not in parent.subdirs:
+ parent.subdirs[d] = CoveredDir(d)
+ parent = parent.subdirs[d]
+ ancestors.append(parent)
+ # Final subdir actually contains the file
+ parent.files[fdirs[-1]] = cov_file
+
+ # Now add file's contribution to coverage by dir
+ for a in ancestors:
+ # Add to 'all' group
+ a.stats_by_group['all'].Add(cov_file.stats)
+
+ # Add to group file belongs to
+ if cov_file.group not in a.stats_by_group:
+ a.stats_by_group[cov_file.group] = CoverageStats()
+ cbyg = a.stats_by_group[cov_file.group]
+ cbyg.Add(cov_file.stats)
+
+ def PrintTree(self):
+ """Prints the tree stats."""
+ # Print the tree
+ print 'Lines of code coverage by directory:'
+ print self.tree.GetTree()
+
+#------------------------------------------------------------------------------
+
+
+def Main(argv):
+ """Main routine.
+
+ Args:
+ argv: list of arguments
+
+ Returns:
+ exit code, 0 for normal exit.
+ """
+ # Parse args
+ parser = OptionParser()
+ parser.add_option(
+ '-i', '--input', dest='inputs', type='string', action='append',
+ metavar='FILE',
+ help='read LCOV input from FILE')
+ parser.add_option(
+ '-r', '--root', dest='roots', type='string', action='append',
+ metavar='ROOT[=ALTNAME]',
+ help='add ROOT directory, optionally map in coverage results as ALTNAME')
+ parser.add_option(
+ '-c', '--config', dest='configs', type='string', action='append',
+ metavar='FILE',
+ help='read settings from configuration FILE')
+ parser.add_option(
+ '-a', '--addfiles', dest='addfiles', type='string', action='append',
+ metavar='PATH',
+ help='add files from PATH to coverage data')
+ parser.add_option(
+ '-t', '--tree', dest='tree', action='store_true',
+ help='print tree of code coverage by group')
+ parser.add_option(
+ '-u', '--uninstrumented', dest='uninstrumented', action='store_true',
+ help='list uninstrumented files')
+
+ parser.set_defaults(
+ inputs=[],
+ roots=[],
+ configs=[],
+ addfiles=[],
+ tree=False,
+ )
+
+ (options, args) = parser.parse_args()
+
+ cov = Coverage()
+
+ # Set root directories for coverage
+ for root_opt in options.roots:
+ if '=' in root_opt:
+ cov.AddRoot(*root_opt.split('='))
+ else:
+ cov.AddRoot(root_opt)
+
+ # Read config files
+ for config_file in options.configs:
+ cov.ParseConfig(config_file, lcov_queue=options.inputs,
+ addfiles_queue=options.addfiles)
+
+ # Parse lcov files
+ for input_filename in options.inputs:
+ cov.ParseLcovFile(input_filename)
+
+ # Add missing files
+ for add_path in options.addfiles:
+ cov.AddFiles(add_path)
+
+ # Print help if no files specified
+ if not cov.files:
+ print 'No covered files found.'
+ parser.print_help()
+ return 1
+
+ # Update tree stats
+ cov.UpdateTreeStats()
+
+ # Print uninstrumented filenames
+ if options.uninstrumented:
+ print 'Uninstrumented files:'
+ for f in sorted(cov.files):
+ covf = cov.files[f]
+ if not covf.stats.get('lines_instrumented'):
+ print ' %-6s %-6s %s' % (covf.group, covf.language, f)
+
+
+ # Print tree stats
+ if options.tree:
+ cov.PrintTree()
+
+ # Print stats
+ for ps_args in cov.print_stats:
+ cov.PrintStat(**ps_args)
+
+ # Normal exit
+ return 0
+
+
+#------------------------------------------------------------------------------
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv))
diff --git a/tools/code_coverage/croc_test.py b/tools/code_coverage/croc_test.py
new file mode 100644
index 0000000..028521c
--- /dev/null
+++ b/tools/code_coverage/croc_test.py
@@ -0,0 +1,694 @@
+#!/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 Crocodile."""
+
+import os
+import re
+import sys
+import StringIO
+import unittest
+import croc
+
+#------------------------------------------------------------------------------
+
+class TestCoverageStats(unittest.TestCase):
+ """Tests for croc.CoverageStats."""
+
+ def testAdd(self):
+ """Test Add()."""
+ c = croc.CoverageStats()
+
+ # Initially empty
+ self.assertEqual(c, {})
+
+ # Add items
+ c['a'] = 1
+ c['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})
+
+ # Add dict with overlapping items
+ 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++')
+
+ def testInit(self):
+ """Test init."""
+ f = self.cov_file
+
+ # Check initial values
+ self.assertEqual(f.filename, 'bob.cc')
+ self.assertEqual(f.group, 'source')
+ self.assertEqual(f.language, 'C++')
+ self.assertEqual(f.lines, {})
+ self.assertEqual(f.stats, {})
+
+ 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,
+ })
+
+ def testUpdateCoverageExeOnly(self):
+ """Test updating coverage when no lines are instrumented."""
+ f = self.cov_file
+ 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,
+ })
+
+ 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.UpdateCoverage()
+ self.assertEqual(f.stats, {
+ '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.UpdateCoverage()
+ self.assertEqual(f.stats, {
+ '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."""
+
+ def setUp(self):
+ self.cov_dir = croc.CoveredDir('/a/b/c')
+
+ def testInit(self):
+ """Test init."""
+ d = self.cov_dir
+
+ # Check initial values
+ self.assertEqual(d.dirpath, '/a/b/c')
+ self.assertEqual(d.files, {})
+ self.assertEqual(d.subdirs, {})
+ 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/')
+
+ def testGetTreeStats(self):
+ """Test getting tree with stats."""
+ d = self.cov_dir
+ d.stats_by_group['all'] = croc.CoverageStats(
+ lines_executable=50, lines_instrumented=30, lines_covered=20)
+ d.stats_by_group['bar'] = croc.CoverageStats(
+ lines_executable=0, lines_instrumented=0, lines_covered=0)
+ 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')
+
+ def testGetTreeSubdir(self):
+ """Test getting tree with subdirs."""
+ d1 = self.cov_dir = croc.CoveredDir('/a')
+ d2 = self.cov_dir = croc.CoveredDir('/a/b')
+ 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/')
+
+#------------------------------------------------------------------------------
+
+class TestCoverage(unittest.TestCase):
+ """Tests for croc.Coverage."""
+
+ def MockWalk(self, src_dir):
+ """Mock for os.walk().
+
+ Args:
+ src_dir: Source directory to walk.
+
+ Returns:
+ A list of (dirpath, dirnames, filenames) tuples.
+ """
+ self.mock_walk_calls.append(src_dir)
+ return self.mock_walk_return
+
+ def setUp(self):
+ """Per-test setup"""
+
+ # Empty coverage object
+ self.cov = croc.Coverage()
+
+ # Coverage object with minimal setup
+ 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('.*\\.c$', language='C')
+ self.cov_minimal.AddRule('.*\\.c##$', language='C##') # sharper than thou
+
+ # Data for MockWalk()
+ self.mock_walk_calls = []
+ self.mock_walk_return = []
+
+ 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'])
+
+ def testAddRoot(self):
+ """Test AddRoot() and CleanupFilename()."""
+ c = self.cov
+
+ # Check for identity on already-clean filenames
+ self.assertEqual(c.CleanupFilename(''), '')
+ self.assertEqual(c.CleanupFilename('a'), 'a')
+ self.assertEqual(c.CleanupFilename('.a'), '.a')
+ self.assertEqual(c.CleanupFilename('..a'), '..a')
+ self.assertEqual(c.CleanupFilename('a.b'), 'a.b')
+ self.assertEqual(c.CleanupFilename('a/b/c'), 'a/b/c')
+ self.assertEqual(c.CleanupFilename('a/b/c/'), 'a/b/c/')
+
+ # Backslash to forward slash
+ self.assertEqual(c.CleanupFilename('a\\b\\c'), 'a/b/c')
+
+ # Handle relative paths
+ self.assertEqual(c.CleanupFilename('.'),
+ c.CleanupFilename(os.path.abspath('.')))
+ self.assertEqual(c.CleanupFilename('..'),
+ c.CleanupFilename(os.path.abspath('..')))
+ self.assertEqual(c.CleanupFilename('./foo/bar'),
+ c.CleanupFilename(os.path.abspath('./foo/bar')))
+ self.assertEqual(c.CleanupFilename('../../a/b/c'),
+ 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')
+ 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')
+
+ # Can use previously defined roots in cleanup
+ c.AddRoot('#/nom/nom/nom', '#CANHAS')
+ self.assertEqual(c.CleanupFilename('foo/nom/nom/nom/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')
+
+ 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))
+
+ # 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))
+
+ # 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'))
+
+ # 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'))
+
+ def testGetCoveredFile(self):
+ """Test GetCoveredFile()."""
+ c = self.cov_minimal
+
+ # Not currently any covered files
+ 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##')
+
+ # 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)
+
+ # 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)
+
+ def testParseLcov(self):
+ """Test ParseLcovData()."""
+ c = self.cov_minimal
+
+ c.ParseLcovData([
+ '# Ignore unknown lines',
+ # File we should include'
+ 'SF:/src/a.c',
+ 'DA:10,1',
+ 'DA:11,0',
+ 'DA:12,1 \n', # Trailing whitespace should get stripped
+ 'end_of_record',
+ # File we should ignore
+ 'SF:/not_src/a.c',
+ 'DA:20,1',
+ 'end_of_record',
+ # Same as first source file, but alternate root
+ 'SF:c:\\source\\a.c',
+ 'DA:30,1',
+ 'end_of_record',
+ # Ignore extra end of record
+ 'end_of_record',
+ # Ignore data points after end of record
+ 'DA:40,1',
+ # Instrumented but uncovered file
+ 'SF:/src/b.c',
+ 'DA:50,0',
+ 'end_of_record',
+ ])
+
+ # We should know about two files
+ self.assertEqual(sorted(c.files), ['#/a.c', '#/b.c'])
+
+ # Check expected contents
+ 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,
+ 'files_instrumented': 1,
+ 'files_covered': 1,
+ 'lines_instrumented': 4,
+ 'lines_executable': 4,
+ 'lines_covered': 3,
+ })
+ b_c = c.GetCoveredFile('#/b.c')
+ self.assertEqual(b_c.lines, {50: 0})
+ self.assertEqual(b_c.stats, {
+ 'files_executable': 1,
+ 'files_instrumented': 1,
+ 'lines_instrumented': 1,
+ 'lines_executable': 1,
+ 'lines_covered': 0,
+ })
+
+ def testGetStat(self):
+ """Test GetStat() and PrintStat()."""
+ c = self.cov
+
+ # Add some stats, so there's something to report
+ c.tree.stats_by_group = {
+ 'all': {
+ 'count_a': 10,
+ 'count_b': 4,
+ 'foo': 'bar',
+ },
+ 'tests': {
+ 'count_a': 2,
+ 'count_b': 5,
+ 'baz': 'bob',
+ },
+ }
+
+ # 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')
+
+ # Test returning defaults
+ self.assertEqual(c.GetStat('nosuch', default=13), 13)
+ self.assertEqual(c.GetStat('baz', default='aaa'), 'aaa')
+ self.assertEqual(c.GetStat('foo', group='tests', default=0), 0)
+ self.assertEqual(c.GetStat('foo', group='nosuch', default=''), '')
+
+ # Test getting stats
+ self.assertEqual(c.GetStat('count_a'), 10)
+ self.assertEqual(c.GetStat('count_a', group='tests'), 2)
+ self.assertEqual(c.GetStat('foo', default='baz'), 'bar')
+
+ # Test stat math (eval)
+ self.assertEqual(c.GetStat('count_a - count_b'), 6)
+ 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 -')
+
+ # 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")')
+
+ # Test PrintStat()
+ # We won't see the first print, but at least verify it doesn't assert
+ c.PrintStat('count_a', format='(test to stdout: %s)')
+ # Send subsequent prints to a file
+ f = StringIO.StringIO()
+ c.PrintStat('count_b', outfile=f)
+ # Test specifying output format
+ c.PrintStat('count_a', format='Count A = %05d', outfile=f)
+ # Test specifing additional keyword args
+ c.PrintStat('count_a', group='tests', outfile=f)
+ c.PrintStat('nosuch', default=42, outfile=f)
+ self.assertEqual(f.getvalue(), ("""\
+GetStat('count_b') = 4
+Count A = 00010
+GetStat('count_a') = 2
+GetStat('nosuch') = 42
+"""))
+ f.close()
+
+ def testAddConfigEmpty(self):
+ """Test AddConfig() with empty config."""
+ c = self.cov
+ # Most minimal config is an empty dict; should do nothing
+ c.AddConfig('{} # And we ignore comments')
+
+ def testAddConfig(self):
+ """Test AddConfig()."""
+ c = self.cov
+ lcov_queue = []
+ addfiles_queue = []
+
+ c.AddConfig("""{
+ 'roots' : [
+ {'root' : '/foo'},
+ {'root' : '/bar', 'altname' : '#BAR'},
+ ],
+ 'rules' : [
+ {'regexp' : '^#', 'group' : 'apple'},
+ {'regexp' : 're2', 'include' : 1, 'language' : 'elvish'},
+ ],
+ 'lcov_files' : ['a.lcov', 'b.lcov'],
+ 'add_files' : ['/src', '#BAR/doo'],
+ 'print_stats' : [
+ {'stat' : 'count_a'},
+ {'stat' : 'count_b', 'group' : 'tests'},
+ ],
+ 'extra_key' : 'is ignored',
+ }""", 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(c.print_stats, [
+ {'stat': 'count_a'},
+ {'stat': 'count_b', 'group': 'tests'},
+ ])
+ # 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'],
+ ])
+
+ def testAddFilesSimple(self):
+ """Test AddFiles() simple call."""
+ c = self.cov_minimal
+ c.add_files_walk = self.MockWalk
+ c.AddFiles('/a/b/c')
+ self.assertEqual(self.mock_walk_calls, ['/a/b/c'])
+ 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')
+
+ # 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(c.files, {})
+
+ def testAddFilesNonEmpty(self):
+ """Test AddFiles() where files are returned."""
+
+ c = self.cov_minimal
+ c.add_files_walk = self.MockWalk
+
+ # Add a rule to exclude a subdir
+ c.AddRule('^#/proj1/excluded/', include=0)
+
+ # Set data for mock walk
+ self.mock_walk_return = [
+ [
+ '/src/proj1',
+ ['excluded', 'subdir'],
+ ['a.c', 'no.f', 'yes.c'],
+ ],
+ [
+ '/src/proj1/subdir',
+ [],
+ ['cherry.c'],
+ ],
+ ]
+
+ c.AddFiles('/src/proj1')
+
+ self.assertEqual(self.mock_walk_calls, ['/src/proj1'])
+
+ # Include files from the main dir and subdir
+ self.assertEqual(sorted(c.files), [
+ '#/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.
+ self.assertEqual(self.mock_walk_return[0][1], ['subdir'])
+
+ def testUpdateTreeStats(self):
+ """Test UpdateTreeStats()."""
+
+ c = self.cov_minimal
+
+
+
+ c.AddRule('.*_test', group='test')
+
+ # Fill the files list
+ c.ParseLcovData([
+ 'SF:/src/a.c',
+ 'DA:10,1', 'DA:11,1', 'DA:20,0',
+ 'end_of_record',
+ 'SF:/src/a_test.c',
+ 'DA:10,1', 'DA:11,1', 'DA:12,1',
+ 'end_of_record',
+ 'SF:/src/foo/b.c',
+ 'DA:10,1', 'DA:11,1', 'DA:20,0', 'DA:21,0', 'DA:30,0',
+ 'end_of_record',
+ 'SF:/src/foo/b_test.c',
+ 'DA:20,0', 'DA:21,0', 'DA:22,0',
+ 'end_of_record',
+ ])
+ c.UpdateTreeStats()
+
+ t = c.tree
+ self.assertEqual(t.dirpath, '')
+ self.assertEqual(sorted(t.files), [])
+ self.assertEqual(sorted(t.subdirs), ['#'])
+ self.assertEqual(t.stats_by_group, {
+ 'all': {
+ 'files_covered': 3,
+ 'files_executable': 4,
+ 'lines_executable': 14,
+ 'lines_covered': 7,
+ 'lines_instrumented': 14,
+ 'files_instrumented': 4,
+ },
+ 'my': {
+ 'files_covered': 2,
+ 'files_executable': 2,
+ 'lines_executable': 8,
+ 'lines_covered': 4,
+ 'lines_instrumented': 8,
+ 'files_instrumented': 2,
+ },
+ 'test': {
+ 'files_covered': 1,
+ 'files_executable': 2,
+ 'lines_executable': 6,
+ 'lines_covered': 3,
+ 'lines_instrumented': 6,
+ 'files_instrumented': 2,
+ },
+ })
+
+ 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, {
+ 'all': {
+ 'files_covered': 3,
+ 'files_executable': 4,
+ 'lines_executable': 14,
+ 'lines_covered': 7,
+ 'lines_instrumented': 14,
+ 'files_instrumented': 4,
+ },
+ 'my': {
+ 'files_covered': 2,
+ 'files_executable': 2,
+ 'lines_executable': 8,
+ 'lines_covered': 4,
+ 'lines_instrumented': 8,
+ 'files_instrumented': 2,
+ },
+ 'test': {
+ 'files_covered': 1,
+ 'files_executable': 2,
+ 'lines_executable': 6,
+ 'lines_covered': 3,
+ 'lines_instrumented': 6,
+ 'files_instrumented': 2,
+ },
+ })
+
+ t = t.subdirs['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, {
+ 'test': {
+ 'files_executable': 1,
+ 'files_instrumented': 1,
+ 'lines_executable': 3,
+ 'lines_instrumented': 3,
+ 'lines_covered': 0,
+ },
+ 'all': {
+ 'files_covered': 1,
+ 'files_executable': 2,
+ 'lines_executable': 8,
+ 'lines_covered': 2,
+ 'lines_instrumented': 8,
+ 'files_instrumented': 2,
+ },
+ 'my': {
+ 'files_covered': 1,
+ 'files_executable': 1,
+ 'lines_executable': 5,
+ 'lines_covered': 2,
+ 'lines_instrumented': 5,
+ 'files_instrumented': 1,
+ }
+ })
+
+ # TODO: test: less important, since these are thin wrappers around other
+ # tested methods.
+ # ParseConfig()
+ # ParseLcovFile()
+ # PrintTree()
+
+#------------------------------------------------------------------------------
+
+if __name__ == '__main__':
+ unittest.main()