Index: Doc/whatsnew/2.7.rst =================================================================== --- Doc/whatsnew/2.7.rst (revision 71941) +++ Doc/whatsnew/2.7.rst (working copy) @@ -473,6 +473,10 @@ to provide additional information about why the two objects are matching, much as the new sequence comparison methods do. + The :func:`unittest.main` now takes an optional ``exit`` argument. + If False ``main`` doesn't call :func:`sys.exit` allowing it to + be used from the interactive interpreter. :issue:`3379`. + * The :func:`is_zipfile` function in the :mod:`zipfile` module will now accept a file object, in addition to the path names accepted in earlier versions. (Contributed by Gabriel Genellina; :issue:`4756`.) Index: Doc/library/unittest.rst =================================================================== --- Doc/library/unittest.rst (revision 71941) +++ Doc/library/unittest.rst (working copy) @@ -1347,7 +1347,7 @@ applications which run test suites should provide alternate implementations. -.. function:: main([module[, defaultTest[, argv[, testRunner[, testLoader]]]]]) +.. function:: main([module[, defaultTest[, argv[, testRunner[, testLoader[, exit]]]]]]) A command-line program that runs a set of tests; this is primarily for making test modules conveniently executable. The simplest use for this function is to @@ -1357,4 +1357,15 @@ unittest.main() The *testRunner* argument can either be a test runner class or an already - created instance of it. + created instance of it. By default ``main`` calls :func:`sys.exit` with + an exit code indicated success or failure of the tests run. + + ``main`` supports being used from the interactive interpreter by passing in the + argument ``exit=False``. This displays the result on standard output without + calling :func:`sys.exit`:: + + >>> from unittest import main + >>> main(module='test_module', exit=False) + + Calling ``main`` actually returns an instance of the ``TestProgram`` class. + This stores the result of the tests run as the ``result`` attribute. Index: Lib/unittest.py =================================================================== --- Lib/unittest.py (revision 71941) +++ Lib/unittest.py (working copy) @@ -346,6 +346,7 @@ raise ValueError("no such test method in %s: %s" % \ (self.__class__, methodName)) self._testMethodDoc = testMethod.__doc__ + self._cleanups = [] # Map types to custom assertEqual functions that will compare # instances of said type in more detail to generate a more useful @@ -371,6 +372,14 @@ useful error message when the two arguments are not equal. """ self._type_equality_funcs[typeobj] = _AssertWrapper(function) + + def addCleanup(self, function, *args, **kwargs): + """Add a function, with arguments, to be called when the test is + completed. Functions added are called on a LIFO basis and are + called after tearDown on test failure or success. + + Cleanup items are called even if setUp fails (unlike tearDown).""" + self._cleanups.append((function, args, kwargs)) def setUp(self): "Hook method for setting up the test fixture before exercising it." @@ -432,40 +441,52 @@ result.startTest(self) testMethod = getattr(self, self._testMethodName) try: + success = False try: self.setUp() except SkipTest as e: result.addSkip(self, str(e)) - return except Exception: result.addError(self, sys.exc_info()) - return - - success = False - try: - testMethod() - except self.failureException: - result.addFailure(self, sys.exc_info()) - except _ExpectedFailure as e: - result.addExpectedFailure(self, e.exc_info) - except _UnexpectedSuccess: - result.addUnexpectedSuccess(self) - except SkipTest as e: - result.addSkip(self, str(e)) - except Exception: - result.addError(self, sys.exc_info()) else: - success = True - - try: - self.tearDown() - except Exception: - result.addError(self, sys.exc_info()) - success = False + try: + testMethod() + except self.failureException: + result.addFailure(self, sys.exc_info()) + except _ExpectedFailure as e: + result.addExpectedFailure(self, e.exc_info) + except _UnexpectedSuccess: + result.addUnexpectedSuccess(self) + except SkipTest as e: + result.addSkip(self, str(e)) + except Exception: + result.addError(self, sys.exc_info()) + else: + success = True + + try: + self.tearDown() + except Exception: + result.addError(self, sys.exc_info()) + success = False + + cleanUpSuccess = self._doCleanups(result) + success = success and cleanUpSuccess if success: result.addSuccess(self) finally: result.stopTest(self) + + def _doCleanups(self, result): + ok = True + while self._cleanups: + function, args, kwargs = self._cleanups.pop(-1) + try: + function(*args, **kwargs) + except Exception: + ok = False + result.addError(self, sys.exc_info()) + return ok def __call__(self, *args, **kwds): return self.run(*args, **kwds) @@ -1469,7 +1490,7 @@ """ def __init__(self, module='__main__', defaultTest=None, argv=None, testRunner=TextTestRunner, - testLoader=defaultTestLoader): + testLoader=defaultTestLoader, exit=True): if isinstance(module, basestring): self.module = __import__(module) for part in module.split('.')[1:]: @@ -1478,6 +1499,8 @@ self.module = module if argv is None: argv = sys.argv + + self.exit = exit self.verbosity = 1 self.defaultTest = defaultTest self.testRunner = testRunner @@ -1529,8 +1552,9 @@ else: # it is assumed to be a TestRunner instance testRunner = self.testRunner - result = testRunner.run(self.test) - sys.exit(not result.wasSuccessful()) + self.result = testRunner.run(self.test) + if self.exit: + sys.exit(not self.result.wasSuccessful()) main = TestProgram Index: Lib/test/test_unittest.py =================================================================== --- Lib/test/test_unittest.py (revision 71941) +++ Lib/test/test_unittest.py (working copy) @@ -9,9 +9,10 @@ import re from test import test_support import unittest -from unittest import TestCase +from unittest import TestCase, TestProgram import types from copy import deepcopy +from cStringIO import StringIO ### Support code ################################################################ @@ -3040,6 +3041,73 @@ "^unexpectedly identical: None : oops$"]) +class Test_TestProgram(TestCase): + + # Horrible white box test + def testNoExit(self): + result = object() + test = object() + + class FakeRunner(object): + def run(self, test): + self.test = test + return result + + runner = FakeRunner() + + try: + oldParseArgs = TestProgram.parseArgs + TestProgram.parseArgs = lambda *args: None + TestProgram.test = test + + program = TestProgram(testRunner=runner, exit=False) + + self.assertEqual(program.result, result) + self.assertEqual(runner.test, test) + + finally: + TestProgram.parseArgs = oldParseArgs + del TestProgram.test + + + class FooBar(unittest.TestCase): + def testPass(self): + assert True + def testFail(self): + assert False + + class FooBarLoader(unittest.TestLoader): + """Test loader that returns a suite containing FooBar.""" + def loadTestsFromModule(self, module): + return self.suiteClass( + [self.loadTestsFromTestCase(Test_TestProgram.FooBar)]) + + + def test_NonExit(self): + program = unittest.main(exit=False, + testRunner=unittest.TextTestRunner(stream=StringIO()), + testLoader=self.FooBarLoader()) + self.assertTrue(hasattr(program, 'result')) + + + def test_Exit(self): + self.assertRaises( + SystemExit, + unittest.main, + testRunner=unittest.TextTestRunner(stream=StringIO()), + exit=True, + testLoader=self.FooBarLoader()) + + + def test_ExitAsDefault(self): + self.assertRaises( + SystemExit, + unittest.main, + testRunner=unittest.TextTestRunner(stream=StringIO()), + testLoader=self.FooBarLoader()) + + + ###################################################################### ## Main ###################################################################### @@ -3047,7 +3115,8 @@ def test_main(): test_support.run_unittest(Test_TestCase, Test_TestLoader, Test_TestSuite, Test_TestResult, Test_FunctionTestCase, - Test_TestSkipping, Test_Assertions, TestLongMessage) + Test_TestSkipping, Test_Assertions, TestLongMessage, + Test_TestProgram) if __name__ == "__main__": test_main()