Home | History | Annotate | Download | only in code_coverage
      1 #!/usr/bin/env python
      2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Unit tests for Crocodile."""
      7 
      8 import os
      9 import StringIO
     10 import unittest
     11 import croc
     12 
     13 
     14 class TestCoverageStats(unittest.TestCase):
     15   """Tests for croc.CoverageStats."""
     16 
     17   def testAdd(self):
     18     """Test Add()."""
     19     c = croc.CoverageStats()
     20 
     21     # Initially empty
     22     self.assertEqual(c, {})
     23 
     24     # Add items
     25     c['a'] = 1
     26     c['b'] = 0
     27     self.assertEqual(c, {'a': 1, 'b': 0})
     28 
     29     # Add dict with non-overlapping items
     30     c.Add({'c': 5})
     31     self.assertEqual(c, {'a': 1, 'b': 0, 'c': 5})
     32 
     33     # Add dict with overlapping items
     34     c.Add({'a': 4, 'd': 3})
     35     self.assertEqual(c, {'a': 5, 'b': 0, 'c': 5, 'd': 3})
     36 
     37 
     38 class TestCoveredFile(unittest.TestCase):
     39   """Tests for croc.CoveredFile."""
     40 
     41   def setUp(self):
     42     self.cov_file = croc.CoveredFile('bob.cc', group='source', language='C++')
     43 
     44   def testInit(self):
     45     """Test init."""
     46     f = self.cov_file
     47 
     48     # Check initial values
     49     self.assertEqual(f.filename, 'bob.cc')
     50     self.assertEqual(f.attrs, {'group': 'source', 'language': 'C++'})
     51     self.assertEqual(f.lines, {})
     52     self.assertEqual(f.stats, {})
     53     self.assertEqual(f.local_path, None)
     54     self.assertEqual(f.in_lcov, False)
     55 
     56   def testUpdateCoverageEmpty(self):
     57     """Test updating coverage when empty."""
     58     f = self.cov_file
     59     f.UpdateCoverage()
     60     self.assertEqual(f.stats, {
     61         'lines_executable': 0,
     62         'lines_instrumented': 0,
     63         'lines_covered': 0,
     64         'files_executable': 1,
     65     })
     66 
     67   def testUpdateCoverageExeOnly(self):
     68     """Test updating coverage when no lines are instrumented."""
     69     f = self.cov_file
     70     f.lines = {1: None, 2: None, 4: None}
     71     f.UpdateCoverage()
     72     self.assertEqual(f.stats, {
     73         'lines_executable': 3,
     74         'lines_instrumented': 0,
     75         'lines_covered': 0,
     76         'files_executable': 1,
     77     })
     78 
     79     # Now mark the file instrumented via in_lcov
     80     f.in_lcov = True
     81     f.UpdateCoverage()
     82     self.assertEqual(f.stats, {
     83         'lines_executable': 3,
     84         'lines_instrumented': 0,
     85         'lines_covered': 0,
     86         'files_executable': 1,
     87         'files_instrumented': 1,
     88     })
     89 
     90   def testUpdateCoverageExeAndInstr(self):
     91     """Test updating coverage when no lines are covered."""
     92     f = self.cov_file
     93     f.lines = {1: None, 2: None, 4: 0, 5: 0, 7: None}
     94     f.UpdateCoverage()
     95     self.assertEqual(f.stats, {
     96         'lines_executable': 5,
     97         'lines_instrumented': 2,
     98         'lines_covered': 0,
     99         'files_executable': 1,
    100         'files_instrumented': 1,
    101     })
    102 
    103   def testUpdateCoverageWhenCovered(self):
    104     """Test updating coverage when lines are covered."""
    105     f = self.cov_file
    106     f.lines = {1: None, 2: None, 3: 1, 4: 0, 5: 0, 6: 1, 7: None}
    107     f.UpdateCoverage()
    108     self.assertEqual(f.stats, {
    109         'lines_executable': 7,
    110         'lines_instrumented': 4,
    111         'lines_covered': 2,
    112         'files_executable': 1,
    113         'files_instrumented': 1,
    114         'files_covered': 1,
    115     })
    116 
    117 
    118 class TestCoveredDir(unittest.TestCase):
    119   """Tests for croc.CoveredDir."""
    120 
    121   def setUp(self):
    122     self.cov_dir = croc.CoveredDir('/a/b/c')
    123 
    124   def testInit(self):
    125     """Test init."""
    126     d = self.cov_dir
    127 
    128     # Check initial values
    129     self.assertEqual(d.dirpath, '/a/b/c')
    130     self.assertEqual(d.files, {})
    131     self.assertEqual(d.subdirs, {})
    132     self.assertEqual(d.stats_by_group, {'all': {}})
    133 
    134   def testGetTreeEmpty(self):
    135     """Test getting empty tree."""
    136     d = self.cov_dir
    137     self.assertEqual(d.GetTree(), 'c/')
    138 
    139   def testGetTreeStats(self):
    140     """Test getting tree with stats."""
    141     d = self.cov_dir
    142     d.stats_by_group['all'] = croc.CoverageStats(
    143         lines_executable=50, lines_instrumented=30, lines_covered=20)
    144     d.stats_by_group['bar'] = croc.CoverageStats(
    145         lines_executable=0, lines_instrumented=0, lines_covered=0)
    146     d.stats_by_group['foo'] = croc.CoverageStats(
    147         lines_executable=33, lines_instrumented=22, lines_covered=11)
    148     # 'bar' group is skipped because it has no executable lines
    149     self.assertEqual(
    150         d.GetTree(),
    151         'c/                               all:20/30/50   foo:11/22/33')
    152 
    153   def testGetTreeSubdir(self):
    154     """Test getting tree with subdirs."""
    155     d1 = self.cov_dir = croc.CoveredDir('/a')
    156     d2 = self.cov_dir = croc.CoveredDir('/a/b')
    157     d3 = self.cov_dir = croc.CoveredDir('/a/c')
    158     d4 = self.cov_dir = croc.CoveredDir('/a/b/d')
    159     d5 = self.cov_dir = croc.CoveredDir('/a/b/e')
    160     d1.subdirs = {'/a/b': d2, '/a/c': d3}
    161     d2.subdirs = {'/a/b/d': d4, '/a/b/e': d5}
    162     self.assertEqual(d1.GetTree(), 'a/\n  b/\n    d/\n    e/\n  c/')
    163 
    164 
    165 class TestCoverage(unittest.TestCase):
    166   """Tests for croc.Coverage."""
    167 
    168   def MockWalk(self, src_dir):
    169     """Mock for os.walk().
    170 
    171     Args:
    172       src_dir: Source directory to walk.
    173 
    174     Returns:
    175       A list of (dirpath, dirnames, filenames) tuples.
    176     """
    177     self.mock_walk_calls.append(src_dir)
    178     return self.mock_walk_return
    179 
    180   def MockScanFile(self, filename, language):
    181     """Mock for croc_scan.ScanFile().
    182 
    183     Args:
    184       filename: Path to file to scan.
    185       language: Language for file.
    186 
    187     Returns:
    188       A list of executable lines.
    189     """
    190     self.mock_scan_calls.append([filename, language])
    191     if filename in self.mock_scan_return:
    192       return self.mock_scan_return[filename]
    193     else:
    194       return self.mock_scan_return['default']
    195 
    196   def setUp(self):
    197     """Per-test setup."""
    198 
    199     # Empty coverage object
    200     self.cov = croc.Coverage()
    201 
    202     # Coverage object with minimal setup
    203     self.cov_minimal = croc.Coverage()
    204     self.cov_minimal.AddRoot('/src')
    205     self.cov_minimal.AddRoot('c:\\source')
    206     self.cov_minimal.AddRule('^_/', include=1, group='my')
    207     self.cov_minimal.AddRule('.*\\.c$', language='C')
    208     self.cov_minimal.AddRule('.*\\.c##$', language='C##')  # sharper than thou
    209 
    210     # Data for MockWalk()
    211     self.mock_walk_calls = []
    212     self.mock_walk_return = []
    213 
    214     # Data for MockScanFile()
    215     self.mock_scan_calls = []
    216     self.mock_scan_return = {'default': [1]}
    217 
    218   def testInit(self):
    219     """Test init."""
    220     c = self.cov
    221     self.assertEqual(c.files, {})
    222     self.assertEqual(c.root_dirs, [])
    223     self.assertEqual(c.print_stats, [])
    224     self.assertEqual(c.rules, [])
    225 
    226   def testAddRoot(self):
    227     """Test AddRoot() and CleanupFilename()."""
    228     c = self.cov
    229 
    230     # Check for identity on already-clean filenames
    231     self.assertEqual(c.CleanupFilename(''), '')
    232     self.assertEqual(c.CleanupFilename('a'), 'a')
    233     self.assertEqual(c.CleanupFilename('.a'), '.a')
    234     self.assertEqual(c.CleanupFilename('..a'), '..a')
    235     self.assertEqual(c.CleanupFilename('a.b'), 'a.b')
    236     self.assertEqual(c.CleanupFilename('a/b/c'), 'a/b/c')
    237     self.assertEqual(c.CleanupFilename('a/b/c/'), 'a/b/c/')
    238 
    239     # Backslash to forward slash
    240     self.assertEqual(c.CleanupFilename('a\\b\\c'), 'a/b/c')
    241 
    242     # Handle relative paths
    243     self.assertEqual(c.CleanupFilename('.'),
    244                      c.CleanupFilename(os.path.abspath('.')))
    245     self.assertEqual(c.CleanupFilename('..'),
    246                      c.CleanupFilename(os.path.abspath('..')))
    247     self.assertEqual(c.CleanupFilename('./foo/bar'),
    248                      c.CleanupFilename(os.path.abspath('./foo/bar')))
    249     self.assertEqual(c.CleanupFilename('../../a/b/c'),
    250                      c.CleanupFilename(os.path.abspath('../../a/b/c')))
    251 
    252     # Replace alt roots
    253     c.AddRoot('foo')
    254     self.assertEqual(c.CleanupFilename('foo'), '_')
    255     self.assertEqual(c.CleanupFilename('foo/bar/baz'), '_/bar/baz')
    256     self.assertEqual(c.CleanupFilename('aaa/foo'), 'aaa/foo')
    257 
    258     # Alt root replacement is applied for all roots
    259     c.AddRoot('foo/bar', '_B')
    260     self.assertEqual(c.CleanupFilename('foo/bar/baz'), '_B/baz')
    261 
    262     # Can use previously defined roots in cleanup
    263     c.AddRoot('_/nom/nom/nom', '_CANHAS')
    264     self.assertEqual(c.CleanupFilename('foo/nom/nom/nom/cheezburger'),
    265                      '_CANHAS/cheezburger')
    266 
    267     # Verify roots starting with UNC paths or drive letters work, and that
    268     # more than one root can point to the same alt_name
    269     c.AddRoot('/usr/local/foo', '_FOO')
    270     c.AddRoot('D:\\my\\foo', '_FOO')
    271     self.assertEqual(c.CleanupFilename('/usr/local/foo/a/b'), '_FOO/a/b')
    272     self.assertEqual(c.CleanupFilename('D:\\my\\foo\\c\\d'), '_FOO/c/d')
    273 
    274     # Cannot specify a blank alt_name
    275     self.assertRaises(ValueError, c.AddRoot, 'some_dir', '')
    276 
    277   def testAddRule(self):
    278     """Test AddRule() and ClassifyFile()."""
    279     c = self.cov
    280 
    281     # With only the default rule, nothing gets kept
    282     self.assertEqual(c.ClassifyFile('_/src/'), {})
    283     self.assertEqual(c.ClassifyFile('_/src/a.c'), {})
    284 
    285     # Add rules to include a tree and set a default group
    286     c.AddRule('^_/src/', include=1, group='source')
    287     self.assertEqual(c.ClassifyFile('_/src/'),
    288                      {'include': 1, 'group': 'source'})
    289     self.assertEqual(c.ClassifyFile('_/notsrc/'), {})
    290     self.assertEqual(c.ClassifyFile('_/src/a.c'),
    291                      {'include': 1, 'group': 'source'})
    292 
    293     # Define some languages and groups
    294     c.AddRule('.*\\.(c|h)$', language='C')
    295     c.AddRule('.*\\.py$', language='Python')
    296     c.AddRule('.*_test\\.', group='test')
    297     self.assertEqual(c.ClassifyFile('_/src/a.c'),
    298                      {'include': 1, 'group': 'source', 'language': 'C'})
    299     self.assertEqual(c.ClassifyFile('_/src/a.h'),
    300                      {'include': 1, 'group': 'source', 'language': 'C'})
    301     self.assertEqual(c.ClassifyFile('_/src/a.cpp'),
    302                      {'include': 1, 'group': 'source'})
    303     self.assertEqual(c.ClassifyFile('_/src/a_test.c'),
    304                      {'include': 1, 'group': 'test', 'language': 'C'})
    305     self.assertEqual(c.ClassifyFile('_/src/test_a.c'),
    306                      {'include': 1, 'group': 'source', 'language': 'C'})
    307     self.assertEqual(c.ClassifyFile('_/src/foo/bar.py'),
    308                      {'include': 1, 'group': 'source', 'language': 'Python'})
    309     self.assertEqual(c.ClassifyFile('_/src/test.py'),
    310                      {'include': 1, 'group': 'source', 'language': 'Python'})
    311 
    312     # Exclude a path (for example, anything in a build output dir)
    313     c.AddRule('.*/build/', include=0)
    314     # But add back in a dir which matched the above rule but isn't a build
    315     # output dir
    316     c.AddRule('_/src/tools/build/', include=1)
    317     self.assertEqual(c.ClassifyFile('_/src/build.c').get('include'), 1)
    318     self.assertEqual(c.ClassifyFile('_/src/build/').get('include'), 0)
    319     self.assertEqual(c.ClassifyFile('_/src/build/a.c').get('include'), 0)
    320     self.assertEqual(c.ClassifyFile('_/src/tools/build/').get('include'), 1)
    321     self.assertEqual(c.ClassifyFile('_/src/tools/build/t.c').get('include'), 1)
    322 
    323   def testGetCoveredFile(self):
    324     """Test GetCoveredFile()."""
    325     c = self.cov_minimal
    326 
    327     # Not currently any covered files
    328     self.assertEqual(c.GetCoveredFile('_/a.c'), None)
    329 
    330     # Add some files
    331     a_c = c.GetCoveredFile('_/a.c', add=True)
    332     b_c = c.GetCoveredFile('_/b.c##', add=True)
    333     self.assertEqual(a_c.filename, '_/a.c')
    334     self.assertEqual(a_c.attrs, {'include': 1, 'group': 'my', 'language': 'C'})
    335     self.assertEqual(b_c.filename, '_/b.c##')
    336     self.assertEqual(b_c.attrs,
    337                      {'include': 1, 'group': 'my', 'language': 'C##'})
    338 
    339     # Specifying the same filename should return the existing object
    340     self.assertEqual(c.GetCoveredFile('_/a.c'), a_c)
    341     self.assertEqual(c.GetCoveredFile('_/a.c', add=True), a_c)
    342 
    343     # Filenames get cleaned on the way in, as do root paths
    344     self.assertEqual(c.GetCoveredFile('/src/a.c'), a_c)
    345     self.assertEqual(c.GetCoveredFile('c:\\source\\a.c'), a_c)
    346 
    347     # TODO: Make sure that covered files require language, group, and include
    348     # (since that checking is now done in GetCoveredFile() rather than
    349     # ClassifyFile())
    350 
    351   def testRemoveCoveredFile(self):
    352     """Test RemoveCoveredFile()."""
    353     # TODO: TEST ME!
    354 
    355   def testParseLcov(self):
    356     """Test ParseLcovData()."""
    357     c = self.cov_minimal
    358 
    359     c.ParseLcovData([
    360         '# Ignore unknown lines',
    361         # File we should include'
    362         'SF:/src/a.c',
    363         'DA:10,1',
    364         'DA:11,0',
    365         'DA:12,1   \n',  # Trailing whitespace should get stripped
    366         'end_of_record',
    367         # File we should ignore
    368         'SF:/not_src/a.c',
    369         'DA:20,1',
    370         'end_of_record',
    371         # Same as first source file, but alternate root
    372         'SF:c:\\source\\a.c',
    373         'DA:30,1',
    374         'end_of_record',
    375         # Ignore extra end of record
    376         'end_of_record',
    377         # Ignore data points after end of record
    378         'DA:40,1',
    379         # Instrumented but uncovered file
    380         'SF:/src/b.c',
    381         'DA:50,0',
    382         'end_of_record',
    383         # Empty file (instrumented but no executable lines)
    384         'SF:c:\\source\\c.c',
    385         'end_of_record',
    386     ])
    387 
    388     # We should know about three files
    389     self.assertEqual(sorted(c.files), ['_/a.c', '_/b.c', '_/c.c'])
    390 
    391     # Check expected contents
    392     a_c = c.GetCoveredFile('_/a.c')
    393     self.assertEqual(a_c.lines, {10: 1, 11: 0, 12: 1, 30: 1})
    394     self.assertEqual(a_c.stats, {
    395         'files_executable': 1,
    396         'files_instrumented': 1,
    397         'files_covered': 1,
    398         'lines_instrumented': 4,
    399         'lines_executable': 4,
    400         'lines_covered': 3,
    401     })
    402     self.assertEqual(a_c.in_lcov, True)
    403 
    404     b_c = c.GetCoveredFile('_/b.c')
    405     self.assertEqual(b_c.lines, {50: 0})
    406     self.assertEqual(b_c.stats, {
    407         'files_executable': 1,
    408         'files_instrumented': 1,
    409         'lines_instrumented': 1,
    410         'lines_executable': 1,
    411         'lines_covered': 0,
    412     })
    413     self.assertEqual(b_c.in_lcov, True)
    414 
    415     c_c = c.GetCoveredFile('_/c.c')
    416     self.assertEqual(c_c.lines, {})
    417     self.assertEqual(c_c.stats, {
    418         'files_executable': 1,
    419         'files_instrumented': 1,
    420         'lines_instrumented': 0,
    421         'lines_executable': 0,
    422         'lines_covered': 0,
    423     })
    424     self.assertEqual(c_c.in_lcov, True)
    425 
    426     # TODO: Test that files are marked as instrumented if they come from lcov,
    427     # even if they don't have any instrumented lines.  (and that in_lcov is set
    428     # for those files - probably should set that via some method rather than
    429     # directly...)
    430 
    431   def testGetStat(self):
    432     """Test GetStat() and PrintStat()."""
    433     c = self.cov
    434 
    435     # Add some stats, so there's something to report
    436     c.tree.stats_by_group = {
    437         'all': {
    438             'count_a': 10,
    439             'count_b': 4,
    440             'foo': 'bar',
    441         },
    442         'tests': {
    443             'count_a': 2,
    444             'count_b': 5,
    445             'baz': 'bob',
    446         },
    447     }
    448 
    449     # Test missing stats and groups
    450     self.assertRaises(croc.CrocStatError, c.GetStat, 'nosuch')
    451     self.assertRaises(croc.CrocStatError, c.GetStat, 'baz')
    452     self.assertRaises(croc.CrocStatError, c.GetStat, 'foo', group='tests')
    453     self.assertRaises(croc.CrocStatError, c.GetStat, 'foo', group='nosuch')
    454 
    455     # Test returning defaults
    456     self.assertEqual(c.GetStat('nosuch', default=13), 13)
    457     self.assertEqual(c.GetStat('baz', default='aaa'), 'aaa')
    458     self.assertEqual(c.GetStat('foo', group='tests', default=0), 0)
    459     self.assertEqual(c.GetStat('foo', group='nosuch', default=''), '')
    460 
    461     # Test getting stats
    462     self.assertEqual(c.GetStat('count_a'), 10)
    463     self.assertEqual(c.GetStat('count_a', group='tests'), 2)
    464     self.assertEqual(c.GetStat('foo', default='baz'), 'bar')
    465 
    466     # Test stat math (eval)
    467     self.assertEqual(c.GetStat('count_a - count_b'), 6)
    468     self.assertEqual(c.GetStat('100.0 * count_a / count_b', group='tests'),
    469                      40.0)
    470     # Should catch eval errors
    471     self.assertRaises(croc.CrocStatError, c.GetStat, '100 / 0')
    472     self.assertRaises(croc.CrocStatError, c.GetStat, 'count_a -')
    473 
    474     # Test nested stats via S()
    475     self.assertEqual(c.GetStat('count_a - S("count_a", group="tests")'), 8)
    476     self.assertRaises(croc.CrocStatError, c.GetStat, 'S()')
    477     self.assertRaises(croc.CrocStatError, c.GetStat, 'S("nosuch")')
    478 
    479     # Test PrintStat()
    480     # We won't see the first print, but at least verify it doesn't assert
    481     c.PrintStat('count_a', format='(test to stdout: %s)')
    482     # Send subsequent prints to a file
    483     f = StringIO.StringIO()
    484     c.PrintStat('count_b', outfile=f)
    485     # Test specifying output format
    486     c.PrintStat('count_a', format='Count A = %05d', outfile=f)
    487     # Test specifing additional keyword args
    488     c.PrintStat('count_a', group='tests', outfile=f)
    489     c.PrintStat('nosuch', default=42, outfile=f)
    490     self.assertEqual(f.getvalue(), ("""\
    491 GetStat('count_b') = 4
    492 Count A = 00010
    493 GetStat('count_a') = 2
    494 GetStat('nosuch') = 42
    495 """))
    496     f.close()
    497 
    498   def testAddConfigEmpty(self):
    499     """Test AddConfig() with empty config."""
    500     c = self.cov
    501     # Most minimal config is an empty dict; should do nothing
    502     c.AddConfig('{} # And we ignore comments')
    503 
    504   def testAddConfig(self):
    505     """Test AddConfig()."""
    506     c = self.cov
    507     lcov_queue = []
    508     addfiles_queue = []
    509 
    510     c.AddConfig("""{
    511         'roots' : [
    512           {'root' : '/foo'},
    513           {'root' : '/bar', 'altname' : 'BAR'},
    514         ],
    515         'rules' : [
    516           {'regexp' : '^_/', 'group' : 'apple'},
    517           {'regexp' : 're2', 'include' : 1, 'language' : 'elvish'},
    518         ],
    519         'lcov_files' : ['a.lcov', 'b.lcov'],
    520         'add_files' : ['/src', 'BAR/doo'],
    521         'print_stats' : [
    522           {'stat' : 'count_a'},
    523           {'stat' : 'count_b', 'group' : 'tests'},
    524         ],
    525         'extra_key' : 'is ignored',
    526     }""", lcov_queue=lcov_queue, addfiles_queue=addfiles_queue)
    527 
    528     self.assertEqual(lcov_queue, ['a.lcov', 'b.lcov'])
    529     self.assertEqual(addfiles_queue, ['/src', 'BAR/doo'])
    530     self.assertEqual(c.root_dirs, [['/foo', '_'], ['/bar', 'BAR']])
    531     self.assertEqual(c.print_stats, [
    532         {'stat': 'count_a'},
    533         {'stat': 'count_b', 'group': 'tests'},
    534     ])
    535     # Convert compiled re's back to patterns for comparison
    536     rules = [[r[0].pattern] + r[1:] for r in c.rules]
    537     self.assertEqual(rules, [
    538         ['^_/', {'group': 'apple'}],
    539         ['re2', {'include': 1, 'language': 'elvish'}],
    540     ])
    541 
    542   def testAddFilesSimple(self):
    543     """Test AddFiles() simple call."""
    544     c = self.cov_minimal
    545     c.add_files_walk = self.MockWalk
    546     c.scan_file = self.MockScanFile
    547 
    548     c.AddFiles('/a/b/c')
    549     self.assertEqual(self.mock_walk_calls, ['/a/b/c'])
    550     self.assertEqual(self.mock_scan_calls, [])
    551     self.assertEqual(c.files, {})
    552 
    553   def testAddFilesRootMap(self):
    554     """Test AddFiles() with root mappings."""
    555     c = self.cov_minimal
    556     c.add_files_walk = self.MockWalk
    557     c.scan_file = self.MockScanFile
    558 
    559     c.AddRoot('_/subdir', 'SUBDIR')
    560 
    561     # AddFiles() should replace the 'SUBDIR' alt_name, then match both
    562     # possible roots for the '_' alt_name.
    563     c.AddFiles('SUBDIR/foo')
    564     self.assertEqual(self.mock_walk_calls,
    565                      ['/src/subdir/foo', 'c:/source/subdir/foo'])
    566     self.assertEqual(self.mock_scan_calls, [])
    567     self.assertEqual(c.files, {})
    568 
    569   def testAddFilesNonEmpty(self):
    570     """Test AddFiles() where files are returned."""
    571 
    572     c = self.cov_minimal
    573     c.add_files_walk = self.MockWalk
    574     c.scan_file = self.MockScanFile
    575 
    576     # Add a rule to exclude a subdir
    577     c.AddRule('^_/proj1/excluded/', include=0)
    578 
    579     # Add a rule to exclude adding some fiels
    580     c.AddRule('.*noscan.c$', add_if_missing=0)
    581 
    582     # Set data for mock walk and scan
    583     self.mock_walk_return = [
    584         [
    585             '/src/proj1',
    586             ['excluded', 'subdir'],
    587             ['a.c', 'no.f', 'yes.c', 'noexe.c', 'bob_noscan.c'],
    588         ],
    589         [
    590             '/src/proj1/subdir',
    591             [],
    592             ['cherry.c'],
    593         ],
    594     ]
    595 
    596     # Add a file with no executable lines; it should be scanned but not added
    597     self.mock_scan_return['/src/proj1/noexe.c'] = []
    598 
    599     c.AddFiles('/src/proj1')
    600 
    601     self.assertEqual(self.mock_walk_calls, ['/src/proj1'])
    602     self.assertEqual(self.mock_scan_calls, [
    603         ['/src/proj1/a.c', 'C'],
    604         ['/src/proj1/yes.c', 'C'],
    605         ['/src/proj1/noexe.c', 'C'],
    606         ['/src/proj1/subdir/cherry.c', 'C'],
    607     ])
    608 
    609     # Include files from the main dir and subdir
    610     self.assertEqual(sorted(c.files), [
    611         '_/proj1/a.c',
    612         '_/proj1/subdir/cherry.c',
    613         '_/proj1/yes.c'])
    614 
    615     # Excluded dir should have been pruned from the mock walk data dirnames.
    616     # In the real os.walk() call this prunes the walk.
    617     self.assertEqual(self.mock_walk_return[0][1], ['subdir'])
    618 
    619 
    620   def testEmptyTreeStats(self):
    621     """Make sure we don't choke when absolutely nothing happened.
    622 
    623     How we might hit this: bot compile error."""
    624     c = self.cov_minimal
    625     t = c.tree
    626     t.stats_by_group['all'].AddDefaults()
    627     self.assertEqual(t.stats_by_group, {
    628         'all': { 'files_covered': 0,
    629                  'files_instrumented': 0,
    630                  'files_executable': 0,
    631                  'lines_covered': 0,
    632                  'lines_instrumented': 0,
    633                  'lines_executable': 0 }})
    634 
    635   def testUpdateTreeStats(self):
    636     """Test UpdateTreeStats()."""
    637 
    638     c = self.cov_minimal
    639     c.AddRule('.*_test', group='test')
    640 
    641     # Fill the files list
    642     c.ParseLcovData([
    643         'SF:/src/a.c',
    644         'DA:10,1', 'DA:11,1', 'DA:20,0',
    645         'end_of_record',
    646         'SF:/src/a_test.c',
    647         'DA:10,1', 'DA:11,1', 'DA:12,1',
    648         'end_of_record',
    649         'SF:/src/foo/b.c',
    650         'DA:10,1', 'DA:11,1', 'DA:20,0', 'DA:21,0', 'DA:30,0',
    651         'end_of_record',
    652         'SF:/src/foo/b_test.c',
    653         'DA:20,0', 'DA:21,0', 'DA:22,0',
    654         'end_of_record',
    655     ])
    656     c.UpdateTreeStats()
    657 
    658     t = c.tree
    659     self.assertEqual(t.dirpath, '')
    660     self.assertEqual(sorted(t.files), [])
    661     self.assertEqual(sorted(t.subdirs), ['_'])
    662     self.assertEqual(t.stats_by_group, {
    663         'all': {
    664             'files_covered': 3,
    665             'files_executable': 4,
    666             'lines_executable': 14,
    667             'lines_covered': 7,
    668             'lines_instrumented': 14,
    669             'files_instrumented': 4,
    670         },
    671         'my': {
    672             'files_covered': 2,
    673             'files_executable': 2,
    674             'lines_executable': 8,
    675             'lines_covered': 4,
    676             'lines_instrumented': 8,
    677             'files_instrumented': 2,
    678         },
    679         'test': {
    680             'files_covered': 1,
    681             'files_executable': 2,
    682             'lines_executable': 6,
    683             'lines_covered': 3,
    684             'lines_instrumented': 6,
    685             'files_instrumented': 2,
    686         },
    687     })
    688 
    689     t = t.subdirs['_']
    690     self.assertEqual(t.dirpath, '_')
    691     self.assertEqual(sorted(t.files), ['a.c', 'a_test.c'])
    692     self.assertEqual(sorted(t.subdirs), ['foo'])
    693     self.assertEqual(t.stats_by_group, {
    694         'all': {
    695             'files_covered': 3,
    696             'files_executable': 4,
    697             'lines_executable': 14,
    698             'lines_covered': 7,
    699             'lines_instrumented': 14,
    700             'files_instrumented': 4,
    701         },
    702         'my': {
    703             'files_covered': 2,
    704             'files_executable': 2,
    705             'lines_executable': 8,
    706             'lines_covered': 4,
    707             'lines_instrumented': 8,
    708             'files_instrumented': 2,
    709         },
    710         'test': {
    711             'files_covered': 1,
    712             'files_executable': 2,
    713             'lines_executable': 6,
    714             'lines_covered': 3,
    715             'lines_instrumented': 6,
    716             'files_instrumented': 2,
    717         },
    718     })
    719 
    720     t = t.subdirs['foo']
    721     self.assertEqual(t.dirpath, '_/foo')
    722     self.assertEqual(sorted(t.files), ['b.c', 'b_test.c'])
    723     self.assertEqual(sorted(t.subdirs), [])
    724     self.assertEqual(t.stats_by_group, {
    725         'test': {
    726             'files_executable': 1,
    727             'files_instrumented': 1,
    728             'lines_executable': 3,
    729             'lines_instrumented': 3,
    730             'lines_covered': 0,
    731         },
    732         'all': {
    733             'files_covered': 1,
    734             'files_executable': 2,
    735             'lines_executable': 8,
    736             'lines_covered': 2,
    737             'lines_instrumented': 8,
    738             'files_instrumented': 2,
    739         },
    740         'my': {
    741             'files_covered': 1,
    742             'files_executable': 1,
    743             'lines_executable': 5,
    744             'lines_covered': 2,
    745             'lines_instrumented': 5,
    746             'files_instrumented': 1,
    747         }
    748     })
    749 
    750   # TODO: test: less important, since these are thin wrappers around other
    751   # tested methods.
    752   #     ParseConfig()
    753   #     ParseLcovFile()
    754   #     PrintTree()
    755 
    756 
    757 if __name__ == '__main__':
    758   unittest.main()
    759