#!/usr/bin/env python # Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Unit tests for Crocodile.""" import os 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', group='source', language='C++') def testInit(self): """Test init.""" f = self.cov_file # Check initial values self.assertEqual(f.filename, 'bob.cc') self.assertEqual(f.attrs, {'group': 'source', 'language': 'C++'}) self.assertEqual(f.lines, {}) self.assertEqual(f.stats, {}) self.assertEqual(f.local_path, None) self.assertEqual(f.in_lcov, False) def testUpdateCoverageEmpty(self): """Test updating coverage when empty.""" f = self.cov_file f.UpdateCoverage() self.assertEqual(f.stats, { 'lines_executable': 0, 'lines_instrumented': 0, 'lines_covered': 0, 'files_executable': 1, }) 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, }) # Now mark the file instrumented via in_lcov f.in_lcov = True f.UpdateCoverage() self.assertEqual(f.stats, { 'lines_executable': 3, 'lines_instrumented': 0, 'lines_covered': 0, 'files_executable': 1, 'files_instrumented': 1, }) def testUpdateCoverageExeAndInstr(self): """Test updating coverage when no lines are covered.""" f = self.cov_file f.lines = {1: None, 2: None, 4: 0, 5: 0, 7: None} f.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(), '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(), '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 b/\n d/\n e/\n 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 MockScanFile(self, filename, language): """Mock for croc_scan.ScanFile(). Args: filename: Path to file to scan. language: Language for file. Returns: A list of executable lines. """ self.mock_scan_calls.append([filename, language]) if filename in self.mock_scan_return: return self.mock_scan_return[filename] else: return self.mock_scan_return['default'] def setUp(self): """Per-test setup.""" # 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 = [] # Data for MockScanFile() self.mock_scan_calls = [] self.mock_scan_return = {'default': [1]} def testInit(self): """Test init.""" c = self.cov self.assertEqual(c.files, {}) self.assertEqual(c.root_dirs, []) self.assertEqual(c.print_stats, []) self.assertEqual(c.rules, []) 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') # Cannot specify a blank alt_name self.assertRaises(ValueError, c.AddRoot, 'some_dir', '') def testAddRule(self): """Test AddRule() and ClassifyFile().""" c = self.cov # With only the default rule, nothing gets kept self.assertEqual(c.ClassifyFile('_/src/'), {}) self.assertEqual(c.ClassifyFile('_/src/a.c'), {}) # Add rules to include a tree and set a default group c.AddRule('^_/src/', include=1, group='source') self.assertEqual(c.ClassifyFile('_/src/'), {'include': 1, 'group': 'source'}) self.assertEqual(c.ClassifyFile('_/notsrc/'), {}) self.assertEqual(c.ClassifyFile('_/src/a.c'), {'include': 1, 'group': 'source'}) # Define some languages and groups c.AddRule('.*\\.(c|h)$', language='C') c.AddRule('.*\\.py$', language='Python') c.AddRule('.*_test\\.', group='test') self.assertEqual(c.ClassifyFile('_/src/a.c'), {'include': 1, 'group': 'source', 'language': 'C'}) self.assertEqual(c.ClassifyFile('_/src/a.h'), {'include': 1, 'group': 'source', 'language': 'C'}) self.assertEqual(c.ClassifyFile('_/src/a.cpp'), {'include': 1, 'group': 'source'}) self.assertEqual(c.ClassifyFile('_/src/a_test.c'), {'include': 1, 'group': 'test', 'language': 'C'}) self.assertEqual(c.ClassifyFile('_/src/test_a.c'), {'include': 1, 'group': 'source', 'language': 'C'}) self.assertEqual(c.ClassifyFile('_/src/foo/bar.py'), {'include': 1, 'group': 'source', 'language': 'Python'}) self.assertEqual(c.ClassifyFile('_/src/test.py'), {'include': 1, 'group': 'source', 'language': 'Python'}) # Exclude a path (for example, anything in a build output dir) c.AddRule('.*/build/', include=0) # But add back in a dir which matched the above rule but isn't a build # output dir c.AddRule('_/src/tools/build/', include=1) self.assertEqual(c.ClassifyFile('_/src/build.c').get('include'), 1) self.assertEqual(c.ClassifyFile('_/src/build/').get('include'), 0) self.assertEqual(c.ClassifyFile('_/src/build/a.c').get('include'), 0) self.assertEqual(c.ClassifyFile('_/src/tools/build/').get('include'), 1) self.assertEqual(c.ClassifyFile('_/src/tools/build/t.c').get('include'), 1) def testGetCoveredFile(self): """Test GetCoveredFile().""" c = self.cov_minimal # Not currently any covered files self.assertEqual(c.GetCoveredFile('_/a.c'), None) # 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.attrs, {'include': 1, 'group': 'my', 'language': 'C'}) self.assertEqual(b_c.filename, '_/b.c##') self.assertEqual(b_c.attrs, {'include': 1, 'group': 'my', 'language': 'C##'}) # Specifying the same filename should return the existing object self.assertEqual(c.GetCoveredFile('_/a.c'), a_c) self.assertEqual(c.GetCoveredFile('_/a.c', add=True), a_c) # Filenames get cleaned on the way in, as do root paths self.assertEqual(c.GetCoveredFile('/src/a.c'), a_c) self.assertEqual(c.GetCoveredFile('c:\\source\\a.c'), a_c) # TODO: Make sure that covered files require language, group, and include # (since that checking is now done in GetCoveredFile() rather than # ClassifyFile()) def testRemoveCoveredFile(self): """Test RemoveCoveredFile().""" # TODO: TEST ME! def testParseLcov(self): """Test ParseLcovData().""" c = self.cov_minimal 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', # Empty file (instrumented but no executable lines) 'SF:c:\\source\\c.c', 'end_of_record', ]) # We should know about three files self.assertEqual(sorted(c.files), ['_/a.c', '_/b.c', '_/c.c']) # Check expected contents a_c = c.GetCoveredFile('_/a.c') 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, }) self.assertEqual(a_c.in_lcov, True) b_c = c.GetCoveredFile('_/b.c') self.assertEqual(b_c.lines, {50: 0}) self.assertEqual(b_c.stats, { 'files_executable': 1, 'files_instrumented': 1, 'lines_instrumented': 1, 'lines_executable': 1, 'lines_covered': 0, }) self.assertEqual(b_c.in_lcov, True) c_c = c.GetCoveredFile('_/c.c') self.assertEqual(c_c.lines, {}) self.assertEqual(c_c.stats, { 'files_executable': 1, 'files_instrumented': 1, 'lines_instrumented': 0, 'lines_executable': 0, 'lines_covered': 0, }) self.assertEqual(c_c.in_lcov, True) # TODO: Test that files are marked as instrumented if they come from lcov, # even if they don't have any instrumented lines. (and that in_lcov is set # for those files - probably should set that via some method rather than # directly...) def testGetStat(self): """Test GetStat() and PrintStat().""" 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.CrocStatError, c.GetStat, 'nosuch') self.assertRaises(croc.CrocStatError, c.GetStat, 'baz') self.assertRaises(croc.CrocStatError, c.GetStat, 'foo', group='tests') self.assertRaises(croc.CrocStatError, c.GetStat, 'foo', group='nosuch') # Test returning defaults self.assertEqual(c.GetStat('nosuch', default=13), 13) 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.CrocStatError, c.GetStat, '100 / 0') self.assertRaises(croc.CrocStatError, c.GetStat, 'count_a -') # Test nested stats via S() self.assertEqual(c.GetStat('count_a - S("count_a", group="tests")'), 8) self.assertRaises(croc.CrocStatError, c.GetStat, 'S()') self.assertRaises(croc.CrocStatError, c.GetStat, 'S("nosuch")') # Test PrintStat() # We won't see the first print, but at least verify it doesn't assert 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, [ ['^_/', {'group': 'apple'}], ['re2', {'include': 1, 'language': 'elvish'}], ]) def testAddFilesSimple(self): """Test AddFiles() simple call.""" c = self.cov_minimal c.add_files_walk = self.MockWalk c.scan_file = self.MockScanFile c.AddFiles('/a/b/c') self.assertEqual(self.mock_walk_calls, ['/a/b/c']) self.assertEqual(self.mock_scan_calls, []) self.assertEqual(c.files, {}) def testAddFilesRootMap(self): """Test AddFiles() with root mappings.""" c = self.cov_minimal c.add_files_walk = self.MockWalk c.scan_file = self.MockScanFile c.AddRoot('_/subdir', 'SUBDIR') # AddFiles() should replace the 'SUBDIR' alt_name, then match both # possible roots for the '_' alt_name. c.AddFiles('SUBDIR/foo') self.assertEqual(self.mock_walk_calls, ['/src/subdir/foo', 'c:/source/subdir/foo']) self.assertEqual(self.mock_scan_calls, []) self.assertEqual(c.files, {}) def testAddFilesNonEmpty(self): """Test AddFiles() where files are returned.""" c = self.cov_minimal c.add_files_walk = self.MockWalk c.scan_file = self.MockScanFile # Add a rule to exclude a subdir c.AddRule('^_/proj1/excluded/', include=0) # Add a rule to exclude adding some fiels c.AddRule('.*noscan.c$', add_if_missing=0) # Set data for mock walk and scan self.mock_walk_return = [ [ '/src/proj1', ['excluded', 'subdir'], ['a.c', 'no.f', 'yes.c', 'noexe.c', 'bob_noscan.c'], ], [ '/src/proj1/subdir', [], ['cherry.c'], ], ] # Add a file with no executable lines; it should be scanned but not added self.mock_scan_return['/src/proj1/noexe.c'] = [] c.AddFiles('/src/proj1') self.assertEqual(self.mock_walk_calls, ['/src/proj1']) self.assertEqual(self.mock_scan_calls, [ ['/src/proj1/a.c', 'C'], ['/src/proj1/yes.c', 'C'], ['/src/proj1/noexe.c', 'C'], ['/src/proj1/subdir/cherry.c', 'C'], ]) # Include files from the main dir and subdir self.assertEqual(sorted(c.files), [ '_/proj1/a.c', '_/proj1/subdir/cherry.c', '_/proj1/yes.c']) # 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 testEmptyTreeStats(self): """Make sure we don't choke when absolutely nothing happened. How we might hit this: bot compile error.""" c = self.cov_minimal t = c.tree t.stats_by_group['all'].AddDefaults() self.assertEqual(t.stats_by_group, { 'all': { 'files_covered': 0, 'files_instrumented': 0, 'files_executable': 0, 'lines_covered': 0, 'lines_instrumented': 0, 'lines_executable': 0 }}) 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()