annotate fructose_gen.py @ 3:d8aeeb7f6785 fructose1.1.0

Changed the license to LGPL to be compatible with Fructose itself.
author Brian Neal <bgneal@gmail.com>
date Sat, 14 May 2011 14:20:44 -0500
parents d098192f01d9
children
rev   line source
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")