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.
6 """Unit tests for Crocodile."""
14 class TestCoverageStats(unittest
.TestCase
):
15 """Tests for croc.CoverageStats."""
19 c
= croc
.CoverageStats()
22 self
.assertEqual(c
, {})
27 self
.assertEqual(c
, {'a': 1, 'b': 0})
29 # Add dict with non-overlapping items
31 self
.assertEqual(c
, {'a': 1, 'b': 0, 'c': 5})
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})
38 class TestCoveredFile(unittest
.TestCase
):
39 """Tests for croc.CoveredFile."""
42 self
.cov_file
= croc
.CoveredFile('bob.cc', group
='source', language
='C++')
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)
56 def testUpdateCoverageEmpty(self
):
57 """Test updating coverage when empty."""
60 self
.assertEqual(f
.stats
, {
61 'lines_executable': 0,
62 'lines_instrumented': 0,
64 'files_executable': 1,
67 def testUpdateCoverageExeOnly(self
):
68 """Test updating coverage when no lines are instrumented."""
70 f
.lines
= {1: None, 2: None, 4: None}
72 self
.assertEqual(f
.stats
, {
73 'lines_executable': 3,
74 'lines_instrumented': 0,
76 'files_executable': 1,
79 # Now mark the file instrumented via in_lcov
82 self
.assertEqual(f
.stats
, {
83 'lines_executable': 3,
84 'lines_instrumented': 0,
86 'files_executable': 1,
87 'files_instrumented': 1,
90 def testUpdateCoverageExeAndInstr(self
):
91 """Test updating coverage when no lines are covered."""
93 f
.lines
= {1: None, 2: None, 4: 0, 5: 0, 7: None}
95 self
.assertEqual(f
.stats
, {
96 'lines_executable': 5,
97 'lines_instrumented': 2,
99 'files_executable': 1,
100 'files_instrumented': 1,
103 def testUpdateCoverageWhenCovered(self
):
104 """Test updating coverage when lines are covered."""
106 f
.lines
= {1: None, 2: None, 3: 1, 4: 0, 5: 0, 6: 1, 7: None}
108 self
.assertEqual(f
.stats
, {
109 'lines_executable': 7,
110 'lines_instrumented': 4,
112 'files_executable': 1,
113 'files_instrumented': 1,
118 class TestCoveredDir(unittest
.TestCase
):
119 """Tests for croc.CoveredDir."""
122 self
.cov_dir
= croc
.CoveredDir('/a/b/c')
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': {}})
134 def testGetTreeEmpty(self
):
135 """Test getting empty tree."""
137 self
.assertEqual(d
.GetTree(), 'c/')
139 def testGetTreeStats(self
):
140 """Test getting tree with stats."""
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
151 'c/ all:20/30/50 foo:11/22/33')
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/')
165 class TestCoverage(unittest
.TestCase
):
166 """Tests for croc.Coverage."""
168 def MockWalk(self
, src_dir
):
169 """Mock for os.walk().
172 src_dir: Source directory to walk.
175 A list of (dirpath, dirnames, filenames) tuples.
177 self
.mock_walk_calls
.append(src_dir
)
178 return self
.mock_walk_return
180 def MockScanFile(self
, filename
, language
):
181 """Mock for croc_scan.ScanFile().
184 filename: Path to file to scan.
185 language: Language for file.
188 A list of executable lines.
190 self
.mock_scan_calls
.append([filename
, language
])
191 if filename
in self
.mock_scan_return
:
192 return self
.mock_scan_return
[filename
]
194 return self
.mock_scan_return
['default']
197 """Per-test setup."""
199 # Empty coverage object
200 self
.cov
= croc
.Coverage()
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
210 # Data for MockWalk()
211 self
.mock_walk_calls
= []
212 self
.mock_walk_return
= []
214 # Data for MockScanFile()
215 self
.mock_scan_calls
= []
216 self
.mock_scan_return
= {'default': [1]}
221 self
.assertEqual(c
.files
, {})
222 self
.assertEqual(c
.root_dirs
, [])
223 self
.assertEqual(c
.print_stats
, [])
224 self
.assertEqual(c
.rules
, [])
226 def testAddRoot(self
):
227 """Test AddRoot() and CleanupFilename()."""
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/')
239 # Backslash to forward slash
240 self
.assertEqual(c
.CleanupFilename('a\\b\\c'), 'a/b/c')
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')))
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')
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')
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')
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')
274 # Cannot specify a blank alt_name
275 self
.assertRaises(ValueError, c
.AddRoot
, 'some_dir', '')
277 def testAddRule(self
):
278 """Test AddRule() and ClassifyFile()."""
281 # With only the default rule, nothing gets kept
282 self
.assertEqual(c
.ClassifyFile('_/src/'), {})
283 self
.assertEqual(c
.ClassifyFile('_/src/a.c'), {})
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'})
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'})
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
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)
323 def testGetCoveredFile(self
):
324 """Test GetCoveredFile()."""
327 # Not currently any covered files
328 self
.assertEqual(c
.GetCoveredFile('_/a.c'), None)
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##'})
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
)
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
)
347 # TODO: Make sure that covered files require language, group, and include
348 # (since that checking is now done in GetCoveredFile() rather than
351 def testRemoveCoveredFile(self
):
352 """Test RemoveCoveredFile()."""
355 def testParseLcov(self
):
356 """Test ParseLcovData()."""
360 '# Ignore unknown lines',
361 # File we should include'
365 'DA:12,1 \n', # Trailing whitespace should get stripped
367 # File we should ignore
371 # Same as first source file, but alternate root
372 'SF:c:\\source\\a.c',
375 # Ignore extra end of record
377 # Ignore data points after end of record
379 # Instrumented but uncovered file
383 # Empty file (instrumented but no executable lines)
384 'SF:c:\\source\\c.c',
388 # We should know about three files
389 self
.assertEqual(sorted(c
.files
), ['_/a.c', '_/b.c', '_/c.c'])
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,
398 'lines_instrumented': 4,
399 'lines_executable': 4,
402 self
.assertEqual(a_c
.in_lcov
, True)
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,
413 self
.assertEqual(b_c
.in_lcov
, True)
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,
424 self
.assertEqual(c_c
.in_lcov
, True)
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
431 def testGetStat(self
):
432 """Test GetStat() and PrintStat()."""
435 # Add some stats, so there's something to report
436 c
.tree
.stats_by_group
= {
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')
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
=''), '')
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')
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'),
470 # Should catch eval errors
471 self
.assertRaises(croc
.CrocStatError
, c
.GetStat
, '100 / 0')
472 self
.assertRaises(croc
.CrocStatError
, c
.GetStat
, 'count_a -')
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")')
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
493 GetStat('count_a') = 2
494 GetStat('nosuch') = 42
498 def testAddConfigEmpty(self
):
499 """Test AddConfig() with empty config."""
501 # Most minimal config is an empty dict; should do nothing
502 c
.AddConfig('{} # And we ignore comments')
504 def testAddConfig(self
):
505 """Test AddConfig()."""
513 {'root' : '/bar', 'altname' : 'BAR'},
516 {'regexp' : '^_/', 'group' : 'apple'},
517 {'regexp' : 're2', 'include' : 1, 'language' : 'elvish'},
519 'lcov_files' : ['a.lcov', 'b.lcov'],
520 'add_files' : ['/src', 'BAR/doo'],
522 {'stat' : 'count_a'},
523 {'stat' : 'count_b', 'group' : 'tests'},
525 'extra_key' : 'is ignored',
526 }""", lcov_queue
=lcov_queue
, addfiles_queue
=addfiles_queue
)
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
, [
533 {'stat': 'count_b', 'group': 'tests'},
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'}],
542 def testAddFilesSimple(self
):
543 """Test AddFiles() simple call."""
545 c
.add_files_walk
= self
.MockWalk
546 c
.scan_file
= self
.MockScanFile
549 self
.assertEqual(self
.mock_walk_calls
, ['/a/b/c'])
550 self
.assertEqual(self
.mock_scan_calls
, [])
551 self
.assertEqual(c
.files
, {})
553 def testAddFilesRootMap(self
):
554 """Test AddFiles() with root mappings."""
556 c
.add_files_walk
= self
.MockWalk
557 c
.scan_file
= self
.MockScanFile
559 c
.AddRoot('_/subdir', 'SUBDIR')
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
, {})
569 def testAddFilesNonEmpty(self
):
570 """Test AddFiles() where files are returned."""
573 c
.add_files_walk
= self
.MockWalk
574 c
.scan_file
= self
.MockScanFile
576 # Add a rule to exclude a subdir
577 c
.AddRule('^_/proj1/excluded/', include
=0)
579 # Add a rule to exclude adding some fiels
580 c
.AddRule('.*noscan.c$', add_if_missing
=0)
582 # Set data for mock walk and scan
583 self
.mock_walk_return
= [
586 ['excluded', 'subdir'],
587 ['a.c', 'no.f', 'yes.c', 'noexe.c', 'bob_noscan.c'],
596 # Add a file with no executable lines; it should be scanned but not added
597 self
.mock_scan_return
['/src/proj1/noexe.c'] = []
599 c
.AddFiles('/src/proj1')
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'],
609 # Include files from the main dir and subdir
610 self
.assertEqual(sorted(c
.files
), [
612 '_/proj1/subdir/cherry.c',
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'])
620 def testEmptyTreeStats(self
):
621 """Make sure we don't choke when absolutely nothing happened.
623 How we might hit this: bot compile error."""
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,
632 'lines_instrumented': 0,
633 'lines_executable': 0 }})
635 def testUpdateTreeStats(self
):
636 """Test UpdateTreeStats()."""
639 c
.AddRule('.*_test', group
='test')
641 # Fill the files list
644 'DA:10,1', 'DA:11,1', 'DA:20,0',
647 'DA:10,1', 'DA:11,1', 'DA:12,1',
650 'DA:10,1', 'DA:11,1', 'DA:20,0', 'DA:21,0', 'DA:30,0',
652 'SF:/src/foo/b_test.c',
653 'DA:20,0', 'DA:21,0', 'DA:22,0',
659 self
.assertEqual(t
.dirpath
, '')
660 self
.assertEqual(sorted(t
.files
), [])
661 self
.assertEqual(sorted(t
.subdirs
), ['_'])
662 self
.assertEqual(t
.stats_by_group
, {
665 'files_executable': 4,
666 'lines_executable': 14,
668 'lines_instrumented': 14,
669 'files_instrumented': 4,
673 'files_executable': 2,
674 'lines_executable': 8,
676 'lines_instrumented': 8,
677 'files_instrumented': 2,
681 'files_executable': 2,
682 'lines_executable': 6,
684 'lines_instrumented': 6,
685 'files_instrumented': 2,
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
, {
696 'files_executable': 4,
697 'lines_executable': 14,
699 'lines_instrumented': 14,
700 'files_instrumented': 4,
704 'files_executable': 2,
705 'lines_executable': 8,
707 'lines_instrumented': 8,
708 'files_instrumented': 2,
712 'files_executable': 2,
713 'lines_executable': 6,
715 'lines_instrumented': 6,
716 'files_instrumented': 2,
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
, {
726 'files_executable': 1,
727 'files_instrumented': 1,
728 'lines_executable': 3,
729 'lines_instrumented': 3,
734 'files_executable': 2,
735 'lines_executable': 8,
737 'lines_instrumented': 8,
738 'files_instrumented': 2,
742 'files_executable': 1,
743 'lines_executable': 5,
745 'lines_instrumented': 5,
746 'files_instrumented': 1,
750 # TODO: test: less important, since these are thin wrappers around other
757 if __name__
== '__main__':