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