Mercurial > public > fructose_gen
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/fructose_gen.py Sat Mar 19 19:53:12 2011 -0500 @@ -0,0 +1,222 @@ +#!/usr/bin/env python +""" +Copyright (C) 2011 by Brian Neal <bgneal@gmail.com> + +fructose_gen.py - A program to auto-generate the main() routine for the C++ +testing framework Fructose. + +""" +from __future__ import with_statement +import re +import optparse +import sys + + +USAGE = "usage: %prog [options] test1.h test2.h ... > main.cpp" +DESCRIPTION = "Generates the main() routine for the Fructose C++ testing framework" +VERSION = "%prog 0.1" +INDENT_CNT = 4 +INDENT = " " * INDENT_CNT + + +def strip_comments(s): + """ + A simple function to strip out C++ style // comments from a string. + This function is really dumb; it doesn't know about string literals, etc., + but it should suit our purposes for finding commented out test classes and + cases. + + """ + i = s.find('//') + return s if i == -1 else s[:i] + + +class TestClass(object): + """ + This class represents a Fructose test class. + Each test class has a name attribute and a list of test case names + (strings). + + """ + def __init__(self, name): + self.name = name + self.test_cases = [] + + +class TestFile(object): + """ + A class to represent a Fructose unit test file. + Each test file has a filename attribute and a list of test classes. + + """ + def __init__(self, filename): + self.filename = filename + self.test_classes = [] + + +class TestFileParser(object): + """ + Base class for parsing Fructose unit test files. + """ + def __init__(self, filename): + self.test_file = TestFile(filename) + + def parse(self): + """ + Parse the file by reading it line by line. + Returns a TestFile object that contains the test classes found within. + + """ + with open(self.test_file.filename, 'r') as f: + for line in f: + s = strip_comments(line) + if s: + self._parse_line(s) + + return self.test_file + + def _parse_line(self, line): + """ + Parses each line of the test file, calling into derived classes to + find test classes and test cases. + + """ + test_class = self._parse_test_class(line) + if test_class: + self.test_file.test_classes.append(test_class) + else: + test_case = self._parse_test_case(line) + if len(self.test_file.test_classes) and test_case: + self.test_file.test_classes[-1].test_cases.append(test_case) + + def _parse_test_class(self, line): + """Derived classes override this""" + raise NotImplementedError + + def _parse_test_case(self, line): + """Derived classes override this""" + raise NotImplementedError + + +class GeneratorFileParser(TestFileParser): + """ + This class parses Fructose test files using the generator style of code + generation. + + """ + CLASS_RE = re.compile(r'\bFRUCTOSE_(?:CLASS|STRUCT)\s*\(\s*([a-zA-Z_]\w*)\s*\)') + CASE_RE = re.compile(r'\bFRUCTOSE_TEST\s*\(\s*([a-zA-Z_]\w*)\s*\)') + + def _parse_test_class(self, line): + m = self.CLASS_RE.search(line) + return TestClass(m.group(1)) if m else None + + def _parse_test_case(self, line): + m = self.CASE_RE.search(line) + return m.group(1) if m else None + + +class XunitFileParser(TestFileParser): + """ + This class parses Fructose test files using the xUnit style of code + generation. + + """ + CLASS_RE = re.compile(r'\b(?:struct|class)\s+([a-zA-Z_]\w*)\s+:\s+public' + r'\s+(?:fructose\s*::\s*)?test_base\s*<\s*\1\s*>') + + CASE_RE = re.compile(r'\bvoid\s+(test\w+)\s*\(const\s+(?:std::)?string\s*&' + r'(?:\s+[a-zA-Z_]\w+)?\s*\)') + + def _parse_test_class(self, line): + m = self.CLASS_RE.search(line) + return TestClass(m.group(1)) if m else None + + def _parse_test_case(self, line): + m = self.CASE_RE.search(line) + return m.group(1) if m else None + + +def generate_test_instance(test_class): + """ + Generates the code to instantiate a test instance, register and run the + tests. + + """ + type_name = test_class.name + instance = type_name + '_instance' + + print "%s{" % INDENT + block_indent = INDENT * 2 + print "%s%s %s;" % (block_indent, type_name, instance) + for test_case in test_class.test_cases: + print '%s%s.add_test("%s", &%s::%s);' % ( + block_indent, + instance, + test_case, + type_name, + test_case, + ) + print "%sconst int r = %s.run(argc, argv);" % (block_indent, instance) + print "%sretval = retval == EXIT_SUCCESS ? r : EXIT_FAILURE;" % block_indent + print "%s}" % INDENT + + + +def generate_main(test_files): + """ + Generates the main() file given a list of TestFile objects. + + """ + for test_file in test_files: + print '#include "%s"' % test_file.filename + + print '\n#include <stdlib.h>\n' + print 'int main(int argc, char* argv[])\n{' + print '%sint retval = EXIT_SUCCESS;\n' % INDENT + + for test_file in test_files: + for test_class in test_file.test_classes: + generate_test_instance(test_class) + + print '\n%sreturn retval;\n}' % INDENT + + +def main(argv=None): + + parser = optparse.OptionParser(usage=USAGE, description=DESCRIPTION, + version=VERSION) + parser.set_defaults( + generator=False, + ) + parser.add_option("-g", "--generator", action="store_true", + help="use generator style code generation [default: %default]") + + opts, args = parser.parse_args(args=argv) + + xunit = not opts.generator + + parser_class = XunitFileParser if xunit else GeneratorFileParser + + if len(args) == 0: + sys.exit("No input files") + + test_files = [] + for test_file in args: + + test_parser = parser_class(test_file) + try: + test_files.append(test_parser.parse()) + except IOError, ex: + sys.stderr.write("Error parsing %s: %s, skipping" % (test_file, ex)) + + generate_main(test_files) + + +if __name__ == '__main__': + try: + main() + except IOError, ex: + sys.exit("IO Error: %s" % ex) + except KeyboardInterrupt: + sys.exit("Control-C interrupt")