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