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