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