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
8import os
9import StringIO
10import unittest
11import croc
12
13
14class 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
38class 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
118class 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
165class 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(), ("""\
491GetStat('count_b') = 4
492Count A = 00010
493GetStat('count_a') = 2
494GetStat('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
757if __name__ == '__main__':
758  unittest.main()
759