From caab0eacef8dccda042b01fb8cc6f48821bd0239 Mon Sep 17 00:00:00 2001 From: anatoly techtonik Date: Thu, 20 Mar 2014 17:19:22 +0300 Subject: Make runner for unit tests configurable, add TAPTestRunner that formats output according to TAP protocol http://testanything.org/ runtest.py --runner TestUnit.TAPTestRunner src\engine\SCons\ActionTests.py --- runtest.py | 14 +++- src/engine/SCons/ActionTests.py | 5 +- testing/framework/TestUnit/__init__.py | 5 ++ testing/framework/TestUnit/cli.py | 35 ++++++++++ testing/framework/TestUnit/taprunner.py | 120 ++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 testing/framework/TestUnit/__init__.py create mode 100644 testing/framework/TestUnit/cli.py create mode 100644 testing/framework/TestUnit/taprunner.py diff --git a/runtest.py b/runtest.py index 91ff8ba..267915a 100755 --- a/runtest.py +++ b/runtest.py @@ -157,6 +157,7 @@ Options: --passed Summarize which tests passed. -q --quiet Don't print the test being executed. --quit-on-failure Quit on any test failure + --runner CLASS Alternative test runner class for unit tests -s --short-progress Short progress, prints only the command line and a percentage value, based on the total and current number of tests. @@ -199,6 +200,8 @@ parser.add_option('-a', '--all', action='store_true', help="Run all tests.") parser.add_option('-o', '--output', help="Save the output from a test run to the log file.") +parser.add_option('--runner', metavar='class', + help="Test runner class for unit tests.") parser.add_option('--xml', help="Save results to file in SCons XML format.") (options, args) = parser.parse_args() @@ -612,14 +615,17 @@ old_pythonpath = os.environ.get('PYTHONPATH') # FIXME: the following is necessary to pull in half of the testing # harness from $srcdir/etc. Those modules should be transfered -# to QMTest/ once we completely cut over to using that as -# the harness, in which case this manipulation of PYTHONPATH +# to testing/, in which case this manipulation of PYTHONPATH # should be able to go away. pythonpaths = [ pythonpath_dir ] # Add path of the QMTest folder to PYTHONPATH +# [ ] move used parts from QMTest to testing/framework/ scriptpath = os.path.dirname(os.path.realpath(__file__)) pythonpaths.append(os.path.join(scriptpath, 'QMTest')) +# Add path for testing framework to PYTHONPATH +pythonpaths.append(os.path.join(scriptpath, 'testing', 'framework')) + os.environ['PYTHONPATH'] = os.pathsep.join(pythonpaths) @@ -731,7 +737,6 @@ if list_only: sys.stdout.write(t.path + "\n") sys.exit(0) -# if not python: if os.name == 'java': python = os.path.join(sys.prefix, 'jython') @@ -766,6 +771,9 @@ def run_test(t, io_lock, async=True): if debug: command_args.append(debug) command_args.append(t.path) + if options.runner: + # For example --runner TestUnit.TAPTestRunner + command_args.append('--runner ' + options.runner) t.command_args = [python] + command_args t.command_str = " ".join([escape(python)] + command_args) if printcommand: diff --git a/src/engine/SCons/ActionTests.py b/src/engine/SCons/ActionTests.py index 3e900d7..809e5ce 100644 --- a/src/engine/SCons/ActionTests.py +++ b/src/engine/SCons/ActionTests.py @@ -49,6 +49,7 @@ import SCons.Environment import SCons.Errors import TestCmd +import TestUnit # Initial setup of the common environment for all tests, # a temporary working directory containing a @@ -2108,8 +2109,8 @@ if __name__ == "__main__": for tclass in tclasses: names = unittest.getTestCaseNames(tclass, 'test_') suite.addTests(list(map(tclass, names))) - if not unittest.TextTestRunner().run(suite).wasSuccessful(): - sys.exit(1) + + TestUnit.run(suite) # Local Variables: # tab-width:4 diff --git a/testing/framework/TestUnit/__init__.py b/testing/framework/TestUnit/__init__.py new file mode 100644 index 0000000..51cf972 --- /dev/null +++ b/testing/framework/TestUnit/__init__.py @@ -0,0 +1,5 @@ + +__all__ = ['TAPTestRunner', 'TAPTestResult', 'run'] + +from .taprunner import TAPTestRunner, TAPTestResult +from .cli import run diff --git a/testing/framework/TestUnit/cli.py b/testing/framework/TestUnit/cli.py new file mode 100644 index 0000000..6aec735 --- /dev/null +++ b/testing/framework/TestUnit/cli.py @@ -0,0 +1,35 @@ +""" +Choose test runner class from --runner command line option +and execute test cases. +""" + +import unittest +import optparse +import sys + + +def get_runner(): + parser = optparse.OptionParser() + parser.add_option('--runner', default='unittest.TextTestRunner', + help='name of test runner class to use') + opts, args = parser.parse_args() + + fromsplit = opts.runner.rsplit('.', 1) + if len(fromsplit) < 2: + raise ValueError('Can\'t use module as a runner') + else: + runnermod = __import__(fromsplit[0]) + return getattr(runnermod, fromsplit[1]) + + +def run(suite=None): + runner = get_runner() + if suite: + if not runner().run(suite).wasSuccessful(): + sys.exit(1) + else: + unittest.main(argv=sys.argv[:1], testRunner=runner) + + +if __name__ == '__main__': + run() diff --git a/testing/framework/TestUnit/taprunner.py b/testing/framework/TestUnit/taprunner.py new file mode 100644 index 0000000..01e0e81 --- /dev/null +++ b/testing/framework/TestUnit/taprunner.py @@ -0,0 +1,120 @@ +""" +Format unittest results in Test Anything Protocol (TAP). +http://testanything.org/tap-version-13-specification.html + +Public domain work by: + anatoly techtonik + +""" + +from unittest import suite +from unittest.runner import TextTestRunner, TextTestResult + +__version__ = "0.1" + +class TAPTestResult(TextTestResult): + + def _process(self, test, msg, failtype = None, directive = None): + """ increase the counter, format and output TAP info """ + # counterhack: increase test counter + test.suite.tap_counter += 1 + msg = "%s %d" % (msg, test.suite.tap_counter) + if "not" not in msg: + msg += " " # justify + self.stream.write("%s - " % msg) + if failtype: + self.stream.write("%s - " % failtype) + self.stream.write("%s" % test.__class__.__name__) + self.stream.write(".%s" % test._testMethodName) + if directive: + self.stream.write(directive) + self.stream.write("\n") + # [ ] write test __doc__ (if exists) in comment + self.stream.flush() + + def addSuccess(self, test): + super(TextTestResult, self).addSuccess(test) + self._process(test, "ok") + + def addFailure(self, test, err): + super(TextTestResult, self).addFailure(test, err) + self._process(test, "not ok", "FAIL") + # [ ] add structured data about assertion + + def addError(self, test, err): + super(TextTestResult, self).addError(test, err) + self._process(test, "not ok", "ERROR") + # [ ] add structured data about exception + + def addSkip(self, test, reason): + super(TextTestResult, self).addSkip(test, reason) + self._process(test, "ok", directive=(" # SKIP %s" % reason)) + + def addExpectedFailure(self, test, err): + super(TextTestResult, self).addExpectedFailure(test, err) + self._process(test, "not ok", directive=(" # TODO")) + + def addUnexpectedSuccess(self, test): + super(TextTestResult, self).addUnexpectedSuccess(test) + self._process(test, "not ok", "FAIL (unexpected success)") + + """ + def printErrors(self): + def printErrorList(self, flavour, errors): + """ + + +class TAPTestRunner(TextTestRunner): + resultclass = TAPTestResult + + def run(self, test): + self.stream.write("TAP version 13\n") + # [ ] add commented block with test suite __doc__ + # [ ] check call with a single test + # if isinstance(test, suite.TestSuite): + self.stream.write("1..%s\n" % len(list(test))) + + # counterhack: inject test counter into test suite + test.tap_counter = 0 + # counterhack: inject reference to suite into each test case + for case in test: + case.suite = test + + return super(TAPTestRunner, self).run(test) + + +if __name__ == "__main__": + import sys + import unittest + + class Test(unittest.TestCase): + def test_ok(self): + pass + def test_fail(self): + self.assertTrue(False) + def test_error(self): + bad_symbol + @unittest.skip("skipin'") + def test_skip(self): + pass + @unittest.expectedFailure + def test_not_ready(self): + self.fail() + @unittest.expectedFailure + def test_invalid_fail_mark(self): + pass + def test_another_ok(self): + pass + + + suite = unittest.TestSuite([ + Test('test_ok'), + Test('test_fail'), + Test('test_error'), + Test('test_skip'), + Test('test_not_ready'), + Test('test_invalid_fail_mark'), + Test('test_another_ok') + ]) + if not TAPTestRunner().run(suite).wasSuccessful(): + sys.exit(1) -- cgit v0.12