annotate 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
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@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")