bgneal@0
|
1 #!/usr/bin/env python
|
bgneal@0
|
2 """
|
bgneal@0
|
3 Copyright (C) 2011 by Brian Neal <bgneal@gmail.com>
|
bgneal@0
|
4
|
bgneal@0
|
5 fructose_gen.py - A program to auto-generate the main() routine for the C++
|
bgneal@0
|
6 testing framework Fructose.
|
bgneal@0
|
7
|
bgneal@3
|
8 This program is free software: you can redistribute it and/or modify
|
bgneal@3
|
9 it under the terms of the GNU Lesser General Public License as published by
|
bgneal@3
|
10 the Free Software Foundation, either version 3 of the License, or
|
bgneal@3
|
11 (at your option) any later version.
|
bgneal@3
|
12
|
bgneal@3
|
13 This program is distributed in the hope that it will be useful,
|
bgneal@3
|
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
bgneal@3
|
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
bgneal@3
|
16 GNU Lesser General Public License for more details.
|
bgneal@3
|
17
|
bgneal@3
|
18 You should have received a copy of the GNU Lesser General Public License
|
bgneal@3
|
19 along with this program. If not, see <http://www.gnu.org/licenses/>.
|
bgneal@3
|
20
|
bgneal@0
|
21 """
|
bgneal@0
|
22 from __future__ import with_statement
|
bgneal@0
|
23 import re
|
bgneal@0
|
24 import optparse
|
bgneal@0
|
25 import sys
|
bgneal@0
|
26
|
bgneal@0
|
27
|
bgneal@0
|
28 USAGE = "usage: %prog [options] test1.h test2.h ... > main.cpp"
|
bgneal@0
|
29 DESCRIPTION = "Generates the main() routine for the Fructose C++ testing framework"
|
bgneal@0
|
30 VERSION = "%prog 0.1"
|
bgneal@0
|
31 INDENT_CNT = 4
|
bgneal@0
|
32 INDENT = " " * INDENT_CNT
|
bgneal@0
|
33
|
bgneal@0
|
34
|
bgneal@0
|
35 def strip_comments(s):
|
bgneal@0
|
36 """
|
bgneal@0
|
37 A simple function to strip out C++ style // comments from a string.
|
bgneal@0
|
38 This function is really dumb; it doesn't know about string literals, etc.,
|
bgneal@0
|
39 but it should suit our purposes for finding commented out test classes and
|
bgneal@0
|
40 cases.
|
bgneal@0
|
41
|
bgneal@0
|
42 """
|
bgneal@0
|
43 i = s.find('//')
|
bgneal@0
|
44 return s if i == -1 else s[:i]
|
bgneal@0
|
45
|
bgneal@0
|
46
|
bgneal@0
|
47 class TestClass(object):
|
bgneal@0
|
48 """
|
bgneal@0
|
49 This class represents a Fructose test class.
|
bgneal@0
|
50 Each test class has a name attribute and a list of test case names
|
bgneal@0
|
51 (strings).
|
bgneal@0
|
52
|
bgneal@0
|
53 """
|
bgneal@0
|
54 def __init__(self, name):
|
bgneal@0
|
55 self.name = name
|
bgneal@0
|
56 self.test_cases = []
|
bgneal@0
|
57
|
bgneal@0
|
58
|
bgneal@0
|
59 class TestFile(object):
|
bgneal@0
|
60 """
|
bgneal@0
|
61 A class to represent a Fructose unit test file.
|
bgneal@0
|
62 Each test file has a filename attribute and a list of test classes.
|
bgneal@0
|
63
|
bgneal@0
|
64 """
|
bgneal@0
|
65 def __init__(self, filename):
|
bgneal@0
|
66 self.filename = filename
|
bgneal@0
|
67 self.test_classes = []
|
bgneal@0
|
68
|
bgneal@0
|
69
|
bgneal@0
|
70 class TestFileParser(object):
|
bgneal@0
|
71 """
|
bgneal@0
|
72 Base class for parsing Fructose unit test files.
|
bgneal@0
|
73 """
|
bgneal@0
|
74 def __init__(self, filename):
|
bgneal@0
|
75 self.test_file = TestFile(filename)
|
bgneal@0
|
76
|
bgneal@0
|
77 def parse(self):
|
bgneal@0
|
78 """
|
bgneal@0
|
79 Parse the file by reading it line by line.
|
bgneal@0
|
80 Returns a TestFile object that contains the test classes found within.
|
bgneal@0
|
81
|
bgneal@0
|
82 """
|
bgneal@0
|
83 with open(self.test_file.filename, 'r') as f:
|
bgneal@0
|
84 for line in f:
|
bgneal@0
|
85 s = strip_comments(line)
|
bgneal@0
|
86 if s:
|
bgneal@0
|
87 self._parse_line(s)
|
bgneal@0
|
88
|
bgneal@0
|
89 return self.test_file
|
bgneal@0
|
90
|
bgneal@0
|
91 def _parse_line(self, line):
|
bgneal@0
|
92 """
|
bgneal@0
|
93 Parses each line of the test file, calling into derived classes to
|
bgneal@0
|
94 find test classes and test cases.
|
bgneal@0
|
95
|
bgneal@0
|
96 """
|
bgneal@0
|
97 test_class = self._parse_test_class(line)
|
bgneal@0
|
98 if test_class:
|
bgneal@0
|
99 self.test_file.test_classes.append(test_class)
|
bgneal@0
|
100 else:
|
bgneal@0
|
101 test_case = self._parse_test_case(line)
|
bgneal@0
|
102 if len(self.test_file.test_classes) and test_case:
|
bgneal@0
|
103 self.test_file.test_classes[-1].test_cases.append(test_case)
|
bgneal@0
|
104
|
bgneal@0
|
105 def _parse_test_class(self, line):
|
bgneal@0
|
106 """Derived classes override this"""
|
bgneal@0
|
107 raise NotImplementedError
|
bgneal@0
|
108
|
bgneal@0
|
109 def _parse_test_case(self, line):
|
bgneal@0
|
110 """Derived classes override this"""
|
bgneal@0
|
111 raise NotImplementedError
|
bgneal@0
|
112
|
bgneal@0
|
113
|
bgneal@0
|
114 class GeneratorFileParser(TestFileParser):
|
bgneal@0
|
115 """
|
bgneal@0
|
116 This class parses Fructose test files using the generator style of code
|
bgneal@0
|
117 generation.
|
bgneal@0
|
118
|
bgneal@0
|
119 """
|
bgneal@0
|
120 CLASS_RE = re.compile(r'\bFRUCTOSE_(?:CLASS|STRUCT)\s*\(\s*([a-zA-Z_]\w*)\s*\)')
|
bgneal@0
|
121 CASE_RE = re.compile(r'\bFRUCTOSE_TEST\s*\(\s*([a-zA-Z_]\w*)\s*\)')
|
bgneal@0
|
122
|
bgneal@0
|
123 def _parse_test_class(self, line):
|
bgneal@0
|
124 m = self.CLASS_RE.search(line)
|
bgneal@0
|
125 return TestClass(m.group(1)) if m else None
|
bgneal@0
|
126
|
bgneal@0
|
127 def _parse_test_case(self, line):
|
bgneal@0
|
128 m = self.CASE_RE.search(line)
|
bgneal@0
|
129 return m.group(1) if m else None
|
bgneal@0
|
130
|
bgneal@0
|
131
|
bgneal@0
|
132 class XunitFileParser(TestFileParser):
|
bgneal@0
|
133 """
|
bgneal@0
|
134 This class parses Fructose test files using the xUnit style of code
|
bgneal@0
|
135 generation.
|
bgneal@0
|
136
|
bgneal@0
|
137 """
|
bgneal@0
|
138 CLASS_RE = re.compile(r'\b(?:struct|class)\s+([a-zA-Z_]\w*)\s+:\s+public'
|
bgneal@0
|
139 r'\s+(?:fructose\s*::\s*)?test_base\s*<\s*\1\s*>')
|
bgneal@0
|
140
|
bgneal@0
|
141 CASE_RE = re.compile(r'\bvoid\s+(test\w+)\s*\(const\s+(?:std::)?string\s*&'
|
bgneal@0
|
142 r'(?:\s+[a-zA-Z_]\w+)?\s*\)')
|
bgneal@0
|
143
|
bgneal@0
|
144 def _parse_test_class(self, line):
|
bgneal@0
|
145 m = self.CLASS_RE.search(line)
|
bgneal@0
|
146 return TestClass(m.group(1)) if m else None
|
bgneal@0
|
147
|
bgneal@0
|
148 def _parse_test_case(self, line):
|
bgneal@0
|
149 m = self.CASE_RE.search(line)
|
bgneal@0
|
150 return m.group(1) if m else None
|
bgneal@0
|
151
|
bgneal@0
|
152
|
bgneal@0
|
153 def generate_test_instance(test_class):
|
bgneal@0
|
154 """
|
bgneal@0
|
155 Generates the code to instantiate a test instance, register and run the
|
bgneal@0
|
156 tests.
|
bgneal@0
|
157
|
bgneal@0
|
158 """
|
bgneal@0
|
159 type_name = test_class.name
|
bgneal@0
|
160 instance = type_name + '_instance'
|
bgneal@0
|
161
|
bgneal@0
|
162 print "%s{" % INDENT
|
bgneal@0
|
163 block_indent = INDENT * 2
|
bgneal@0
|
164 print "%s%s %s;" % (block_indent, type_name, instance)
|
bgneal@0
|
165 for test_case in test_class.test_cases:
|
bgneal@0
|
166 print '%s%s.add_test("%s", &%s::%s);' % (
|
bgneal@0
|
167 block_indent,
|
bgneal@0
|
168 instance,
|
bgneal@0
|
169 test_case,
|
bgneal@0
|
170 type_name,
|
bgneal@0
|
171 test_case,
|
bgneal@0
|
172 )
|
bgneal@0
|
173 print "%sconst int r = %s.run(argc, argv);" % (block_indent, instance)
|
bgneal@0
|
174 print "%sretval = retval == EXIT_SUCCESS ? r : EXIT_FAILURE;" % block_indent
|
bgneal@0
|
175 print "%s}" % INDENT
|
bgneal@0
|
176
|
bgneal@0
|
177
|
bgneal@0
|
178
|
bgneal@0
|
179 def generate_main(test_files):
|
bgneal@0
|
180 """
|
bgneal@0
|
181 Generates the main() file given a list of TestFile objects.
|
bgneal@0
|
182
|
bgneal@0
|
183 """
|
bgneal@0
|
184 for test_file in test_files:
|
bgneal@0
|
185 print '#include "%s"' % test_file.filename
|
bgneal@0
|
186
|
bgneal@0
|
187 print '\n#include <stdlib.h>\n'
|
bgneal@0
|
188 print 'int main(int argc, char* argv[])\n{'
|
bgneal@0
|
189 print '%sint retval = EXIT_SUCCESS;\n' % INDENT
|
bgneal@0
|
190
|
bgneal@0
|
191 for test_file in test_files:
|
bgneal@0
|
192 for test_class in test_file.test_classes:
|
bgneal@0
|
193 generate_test_instance(test_class)
|
bgneal@0
|
194
|
bgneal@0
|
195 print '\n%sreturn retval;\n}' % INDENT
|
bgneal@0
|
196
|
bgneal@0
|
197
|
bgneal@0
|
198 def main(argv=None):
|
bgneal@0
|
199
|
bgneal@0
|
200 parser = optparse.OptionParser(usage=USAGE, description=DESCRIPTION,
|
bgneal@0
|
201 version=VERSION)
|
bgneal@0
|
202 parser.set_defaults(
|
bgneal@0
|
203 generator=False,
|
bgneal@0
|
204 )
|
bgneal@0
|
205 parser.add_option("-g", "--generator", action="store_true",
|
bgneal@0
|
206 help="use generator style code generation [default: %default]")
|
bgneal@0
|
207
|
bgneal@0
|
208 opts, args = parser.parse_args(args=argv)
|
bgneal@0
|
209
|
bgneal@0
|
210 xunit = not opts.generator
|
bgneal@0
|
211
|
bgneal@0
|
212 parser_class = XunitFileParser if xunit else GeneratorFileParser
|
bgneal@0
|
213
|
bgneal@0
|
214 if len(args) == 0:
|
bgneal@0
|
215 sys.exit("No input files")
|
bgneal@0
|
216
|
bgneal@0
|
217 test_files = []
|
bgneal@0
|
218 for test_file in args:
|
bgneal@0
|
219
|
bgneal@0
|
220 test_parser = parser_class(test_file)
|
bgneal@0
|
221 try:
|
bgneal@0
|
222 test_files.append(test_parser.parse())
|
bgneal@0
|
223 except IOError, ex:
|
bgneal@0
|
224 sys.stderr.write("Error parsing %s: %s, skipping" % (test_file, ex))
|
bgneal@0
|
225
|
bgneal@0
|
226 generate_main(test_files)
|
bgneal@0
|
227
|
bgneal@0
|
228
|
bgneal@0
|
229 if __name__ == '__main__':
|
bgneal@0
|
230 try:
|
bgneal@0
|
231 main()
|
bgneal@0
|
232 except IOError, ex:
|
bgneal@0
|
233 sys.exit("IO Error: %s" % ex)
|
bgneal@0
|
234 except KeyboardInterrupt:
|
bgneal@0
|
235 sys.exit("Control-C interrupt")
|