| @@ -0,0 +1,280 @@ | |||||
| ############################################################################ | |||||
| # David W. Robertson, LBNL | |||||
| # See Copyright for copyright notice! | |||||
| ########################################################################### | |||||
| import sys, types, os.path, pickle | |||||
| import StringIO, copy, re | |||||
| import unittest | |||||
| import ConfigParser | |||||
| """ | |||||
| utils: | |||||
| This module contains utility functions for use by test case modules, a | |||||
| class facilitating the use of ConfigParser with multiple test cases, a | |||||
| class encapsulating comparisons against a test file, and a test loader | |||||
| class with a different loading strategy than the default | |||||
| unittest.TestLoader. | |||||
| """ | |||||
| thisFileName = sys.modules[__name__].__file__ | |||||
| class configHandler(ConfigParser.ConfigParser): | |||||
| def __init__(self, name="config.py"): | |||||
| ConfigParser.ConfigParser.__init__(self) | |||||
| # first, look for one in this directory | |||||
| try: | |||||
| self.read(name) | |||||
| except IOError: | |||||
| self.read(os.path.dirname(thisFileName) + os.sep + name) | |||||
| def getConfigNames(self, section, numMethods, valueFunc=None): | |||||
| result = None | |||||
| for name, value in self.items(section): | |||||
| for i in range(0, numMethods): | |||||
| yield value # indicate which test in all cases | |||||
| if i == 0: | |||||
| result = None | |||||
| if valueFunc: | |||||
| try: | |||||
| result = valueFunc(value) | |||||
| except KeyboardInterrupt: | |||||
| sys.exit(-1) # for now | |||||
| except: # don't care, test will be skipped | |||||
| pass | |||||
| if valueFunc: | |||||
| yield result | |||||
| def length(self, section): | |||||
| return len(self.options(section)) | |||||
| def loadPickledObj(fname): | |||||
| """Not currently used. | |||||
| """ | |||||
| fname = os.path.dirname(thisFileName) + os.sep + fname + ".obj" | |||||
| f = open(fname, "r") | |||||
| obj = pickle.load(f) | |||||
| f.close() | |||||
| return obj | |||||
| def dumpPickledObj(obj, fname): | |||||
| """Not currently used""" | |||||
| fname = os.path.dirname(thisFileName) + os.sep + fname + ".obj" | |||||
| f = open(fname, "w") | |||||
| pickle.dump(obj, f) | |||||
| f.close() | |||||
| class TestDiff: | |||||
| """TestDiff encapsulates comparing a string or StringIO object | |||||
| against text in a test file. Test files are expected to | |||||
| be located in a subdirectory of the current directory, | |||||
| named data (if one doesn't exist, it will be created). | |||||
| If used in a test case, this should be instantiated in setUp and | |||||
| closed in tearDown. The calling unittest.TestCase instance is passed | |||||
| in on object creation. Optional compiled regular expressions | |||||
| can also be passed in, which are used to ignore strings | |||||
| that one knows in advance will be different, for example | |||||
| id="<hex digits>" . | |||||
| The initial running of the test will create the test | |||||
| files. When the tests are run again, the new output | |||||
| is compared against the old, line by line. To generate | |||||
| a new test file, remove the old one from data. | |||||
| """ | |||||
| def __init__(self, testInst, *ignoreList): | |||||
| self.dataFile = None | |||||
| self.testInst = testInst | |||||
| self.origStrFile = None | |||||
| # used to divide separate test blocks within the same | |||||
| # test file. | |||||
| self.divider = "#" + ">" * 75 + "\n" | |||||
| self.expectedFailures = copy.copy(ignoreList) | |||||
| self.testFilePath = "data" + os.sep | |||||
| if not os.path.exists(self.testFilePath): | |||||
| os.mkdir(self.testFilePath) | |||||
| def setDiffFile(self, fname): | |||||
| """setDiffFile attempts to open the test file with the | |||||
| given name, and read it into a StringIO instance. | |||||
| If the file does not exist, it opens the file for | |||||
| writing. | |||||
| """ | |||||
| filename = fname | |||||
| if self.dataFile and not self.dataFile.closed: | |||||
| self.dataFile.close() | |||||
| try: | |||||
| self.dataFile = open(self.testFilePath + filename, "r") | |||||
| self.origStrFile = StringIO.StringIO(self.dataFile.read()) | |||||
| except IOError: | |||||
| try: | |||||
| self.dataFile = open(self.testFilePath + filename, "w") | |||||
| except IOError: | |||||
| print "exception" | |||||
| def failUnlessEqual(self, buffer): | |||||
| """failUnlessEqual takes either a string or a StringIO | |||||
| instance as input, and compares it against the original | |||||
| output from the test file. | |||||
| """ | |||||
| # if not already a string IO | |||||
| if not isinstance(buffer, StringIO.StringIO): | |||||
| testStrFile = StringIO.StringIO(buffer) | |||||
| else: | |||||
| testStrFile = buffer | |||||
| testStrFile.seek(0) | |||||
| if self.dataFile.mode == "r": | |||||
| for testLine in testStrFile: | |||||
| origLine = self.origStrFile.readline() | |||||
| # skip divider | |||||
| if origLine == self.divider: | |||||
| origLine = self.origStrFile.readline() | |||||
| # take out expected failure strings before | |||||
| # comparing original against new output | |||||
| for cexpr in self.expectedFailures: | |||||
| origLine = cexpr.sub('', origLine) | |||||
| testLine = cexpr.sub('', testLine) | |||||
| self.testInst.failUnlessEqual(origLine, testLine) | |||||
| else: # write new test file | |||||
| for line in testStrFile: | |||||
| self.dataFile.write(line) | |||||
| self.dataFile.write(self.divider) | |||||
| testStrFile.close() | |||||
| def close(self): | |||||
| """Closes handle to original test file. | |||||
| """ | |||||
| if self.dataFile and not self.dataFile.closed: | |||||
| self.dataFile.close() | |||||
| class MatchTestLoader(unittest.TestLoader): | |||||
| """Overrides unittest.TestLoader.loadTestsFromNames to provide a | |||||
| simpler and less verbose way to select a subset of tests to run. | |||||
| If all tests will always be run, use unittest.TestLoader instead. | |||||
| If a top-level test invokes test cases in other scripts, | |||||
| MatchTestLoader should be created with topLevel set to True | |||||
| to get the correct results. For example, | |||||
| def main(): | |||||
| loader = utils.MatchTestLoader(True, None, "makeTestSuite") | |||||
| unittest.main(defaultTest="makeTestSuite", testLoader=loader) | |||||
| The defaultTest argument in the constructor indicates the test to run | |||||
| if no additional arguments beyond the test script name are provided. | |||||
| """ | |||||
| def __init__(self, topLevel, config, defaultTest): | |||||
| unittest.TestLoader.__init__(self) | |||||
| self.testMethodPrefix = "test" | |||||
| self.defaultTest = defaultTest | |||||
| self.topLevel = topLevel | |||||
| self.config = config | |||||
| self.nameGenerator = None | |||||
| def loadTestsFromNames(self, unused, module=None): | |||||
| """Instead of loading the test names passed into it from | |||||
| unittest.TestProgram, loadTestsFromNames uses arguments from the | |||||
| command-line instead. This method is necessary because the | |||||
| superclass method called from unittest.TestProgram would fail on | |||||
| sub-string arguments, and because the default test would | |||||
| not be called properly in some cases, if using the argument | |||||
| passed in. | |||||
| There can be multiple names, both in full or as a substring, on | |||||
| the command-line. After getting the arguments, this method | |||||
| calls the corresponding method in the superclass. | |||||
| """ | |||||
| self.testArgs = [] | |||||
| if not self.topLevel or (len(sys.argv) != 1): | |||||
| for arg in sys.argv[1:]: | |||||
| if arg.find("-") != 0: | |||||
| self.testArgs.append(arg) | |||||
| if not self.testArgs: | |||||
| self.testArgs = [None] | |||||
| suites = unittest.TestLoader.loadTestsFromNames(self, | |||||
| (self.defaultTest,), module) | |||||
| return suites | |||||
| def loadTestsFromConfig(self, testCaseClass, section, fname="config.py", | |||||
| valueFunc=None): | |||||
| config = configHandler(fname) | |||||
| numTestCases = self.getTestCaseNumber(testCaseClass) | |||||
| self.nameGenerator = config.getConfigNames(section, | |||||
| numTestCases, valueFunc) | |||||
| configLen = config.length(section) | |||||
| suite = unittest.TestSuite() | |||||
| suite.addTest(self.loadTestsFromTestCase(testCaseClass)) | |||||
| for i in range(1, configLen): | |||||
| suite.addTest(self.loadTestsFromTestCase(testCaseClass)) | |||||
| return suite | |||||
| def getTestCaseNumber(self, testCaseClass): | |||||
| """looks for any test methods whose name contains testStr, checking | |||||
| if a test method has already been added. If there is not a match, | |||||
| it checks for an exact match with the test case name, and | |||||
| returns the number of test cases. | |||||
| """ | |||||
| methods = self.getTestCaseNames(testCaseClass) | |||||
| prevAdded = [] | |||||
| counter = 0 | |||||
| for testStr in self.testArgs: | |||||
| # print "test argument ", testStr | |||||
| if testStr: | |||||
| for m in methods: | |||||
| if m.find(testStr) >= 0 and m not in prevAdded: | |||||
| counter = counter + 1 | |||||
| prevAdded.append(m) | |||||
| if counter: | |||||
| return counter | |||||
| if (not testStr) or (testCaseClass.__name__ == testStr): | |||||
| for m in methods: | |||||
| counter = counter + 1 | |||||
| prevAdded.append(m) | |||||
| # print "found %d cases" % counter | |||||
| return counter | |||||
| def loadTestsFromTestCase(self, testCaseClass): | |||||
| """looks for any test methods whose name contains testStr, checking | |||||
| if a test method has already been added. If there is not a match, | |||||
| it checks for an exact match with the test case name, and loads | |||||
| all methods if so. | |||||
| """ | |||||
| methods = self.getTestCaseNames(testCaseClass) | |||||
| prevAdded = [] | |||||
| suites = unittest.TestSuite() | |||||
| for testStr in self.testArgs: | |||||
| # print testStr | |||||
| if testStr: | |||||
| for m in methods: | |||||
| if m.find(testStr) >= 0 and m not in prevAdded: | |||||
| suites.addTest(testCaseClass(m)) | |||||
| prevAdded.append(m) | |||||
| if suites.countTestCases(): | |||||
| return suites | |||||
| for testStr in self.testArgs: | |||||
| if (not testStr) or (testCaseClass.__name__ == testStr): | |||||
| for m in methods: | |||||
| suites.addTest(testCaseClass(m)) | |||||
| prevAdded.append(m) | |||||
| if suites.countTestCases(): | |||||
| return suites | |||||
| return suites | |||||