Skip to content

Commit 75fe74b

Browse files
committed
Script to find regression tests that cover given source lines
An intended use case is to find regression tests that need to be adapted when a loss of coverage in some files is observed when a new feature is merged. For example, when improving the constant propagator to propagate more operations, some existing tests that were intended to test the constraint encoding of those operations might then be solved via constant propagation. Thus, the existing tests need to be adapted to use non-constants as inputs.
1 parent 5d2e8b9 commit 75fe74b

File tree

1 file changed

+131
-0
lines changed

1 file changed

+131
-0
lines changed

scripts/find-covering-tests.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python3
2+
3+
# Find regression tests that cover given source lines
4+
5+
import argparse
6+
import os
7+
import re
8+
import subprocess
9+
import sys
10+
11+
description =\
12+
'''\
13+
Find cbmc regression tests that cover given source lines
14+
15+
The program for which to analyse coverage needs to be built with gcc/g++/ld and
16+
flag --coverage (or equivalent), and the gcov command needs to be available.
17+
18+
Example (assuming the script is invoked from the cbmc root directory):
19+
20+
find-covering-tests.py \\
21+
--directory regression/cbmc \\
22+
--command '../test.pl -c cbmc' \\
23+
--source-line 'src/cbmc_parse_options.cpp:322' \\
24+
--source-line 'src/goto_symex.cpp:53'
25+
26+
The invocation above determines the regression tests in regression/cbmc which
27+
cover line 322 of file cbmc_parse_options.cpp or line 53 of file goto_symex.cpp.
28+
'''
29+
30+
def remove_existing_coverage_data(source_lines):
31+
source_files = [item[0] for item in source_lines]
32+
33+
for filename in source_files:
34+
pre, ext = os.path.splitext(filename)
35+
gcda_file = pre + '.gcda'
36+
37+
gcov_file = filename + '.gcov'
38+
39+
try:
40+
os.remove(gcda_file)
41+
os.remove(gcov_file)
42+
except:
43+
pass
44+
45+
46+
def parse_source_lines(source_lines):
47+
return [(item[0], int(item[1])) for item in
48+
map(lambda s: s.split(':'), source_lines)]
49+
50+
51+
def is_covered_source_line(filename, line):
52+
if not os.path.isfile(filename):
53+
return False
54+
55+
d = os.path.dirname(filename)
56+
b = os.path.basename(filename)
57+
subprocess.run(['gcov', b],
58+
cwd=d, stdout=subprocess.DEVNULL,
59+
stderr=subprocess.DEVNULL,
60+
check=True)
61+
62+
gcov_file = filename + '.gcov'
63+
if not os.path.isfile(gcov_file):
64+
return False
65+
66+
f = open(gcov_file)
67+
s = f.read()
68+
f.close()
69+
70+
mo = re.search('^\s*[1-9][0-9]*:\s+' + str(line) + ':', s, re.MULTILINE)
71+
return mo is not None
72+
73+
74+
def get_covered_source_lines(source_lines):
75+
covered_source_lines = []
76+
77+
for filename, line in source_lines:
78+
if is_covered_source_line(filename, line):
79+
covered_source_lines.append((filename, line))
80+
81+
return covered_source_lines
82+
83+
84+
def run(config):
85+
source_lines = parse_source_lines(config.source_line)
86+
87+
dirs = filter(
88+
lambda entry: os.path.isdir(os.path.join(config.directory, entry)),
89+
os.listdir(config.directory))
90+
91+
for d in dirs:
92+
remove_existing_coverage_data(source_lines)
93+
print('Running test ' + d)
94+
subprocess.run([config.command + ' ' + d],
95+
cwd=config.directory,
96+
shell=True,
97+
stdout=subprocess.DEVNULL,
98+
stderr=subprocess.DEVNULL,
99+
check=True)
100+
101+
covered_source_lines = get_covered_source_lines(source_lines)
102+
if covered_source_lines:
103+
print(' Covered source lines:')
104+
for filename, line in covered_source_lines:
105+
print(' ' + filename + ':' + str(line))
106+
else:
107+
print(' Does not cover any of the given source lines')
108+
109+
110+
if __name__ == '__main__':
111+
112+
parser = argparse.ArgumentParser(
113+
formatter_class=argparse.RawDescriptionHelpFormatter,
114+
description=description)
115+
116+
parser.add_argument(
117+
'--source-line',
118+
action='append',
119+
metavar='<filename>:<line>',
120+
required=True,
121+
help='source lines for which to determine which tests cover them, can be '
122+
'repeated')
123+
parser.add_argument('--command', required=True,
124+
help='regression test command, typically an invocation of test.pl')
125+
parser.add_argument('--directory', required=True,
126+
help='directory containing regression test directories')
127+
128+
config = parser.parse_args()
129+
130+
run(config)
131+

0 commit comments

Comments
 (0)