Commit b3468f79 authored by Michael Foord's avatar Michael Foord

Issue 10611. Issue 9857. Improve the way exception handling, including test...

Issue 10611. Issue 9857. Improve the way exception handling, including test skipping, is done inside TestCase.run
parent addc6f5a
...@@ -25,7 +25,6 @@ class SkipTest(Exception): ...@@ -25,7 +25,6 @@ class SkipTest(Exception):
Usually you can use TestResult.skip() or one of the skipping decorators Usually you can use TestResult.skip() or one of the skipping decorators
instead of raising this directly. instead of raising this directly.
""" """
pass
class _ExpectedFailure(Exception): class _ExpectedFailure(Exception):
""" """
...@@ -42,7 +41,17 @@ class _UnexpectedSuccess(Exception): ...@@ -42,7 +41,17 @@ class _UnexpectedSuccess(Exception):
""" """
The test was supposed to fail, but it didn't! The test was supposed to fail, but it didn't!
""" """
pass
class _Outcome(object):
def __init__(self):
self.success = True
self.skipped = None
self.unexpectedSuccess = None
self.expectedFailure = None
self.errors = []
self.failures = []
def _id(obj): def _id(obj):
return obj return obj
...@@ -263,7 +272,7 @@ class TestCase(object): ...@@ -263,7 +272,7 @@ class TestCase(object):
not have a method with the specified name. not have a method with the specified name.
""" """
self._testMethodName = methodName self._testMethodName = methodName
self._resultForDoCleanups = None self._outcomeForDoCleanups = None
try: try:
testMethod = getattr(self, methodName) testMethod = getattr(self, methodName)
except AttributeError: except AttributeError:
...@@ -367,6 +376,36 @@ class TestCase(object): ...@@ -367,6 +376,36 @@ class TestCase(object):
RuntimeWarning, 2) RuntimeWarning, 2)
result.addSuccess(self) result.addSuccess(self)
def _executeTestPart(self, function, outcome, isTest=False):
try:
function()
except KeyboardInterrupt:
raise
except SkipTest as e:
outcome.success = False
outcome.skipped = str(e)
except _UnexpectedSuccess:
exc_info = sys.exc_info()
outcome.success = False
if isTest:
outcome.unexpectedSuccess = exc_info
else:
outcome.errors.append(exc_info)
except _ExpectedFailure:
outcome.success = False
exc_info = sys.exc_info()
if isTest:
outcome.expectedFailure = exc_info
else:
outcome.errors.append(exc_info)
except self.failureException:
outcome.success = False
outcome.failures.append(sys.exc_info())
exc_info = sys.exc_info()
except:
outcome.success = False
outcome.errors.append(sys.exc_info())
def run(self, result=None): def run(self, result=None):
orig_result = result orig_result = result
if result is None: if result is None:
...@@ -375,7 +414,6 @@ class TestCase(object): ...@@ -375,7 +414,6 @@ class TestCase(object):
if startTestRun is not None: if startTestRun is not None:
startTestRun() startTestRun()
self._resultForDoCleanups = result
result.startTest(self) result.startTest(self)
testMethod = getattr(self, self._testMethodName) testMethod = getattr(self, self._testMethodName)
...@@ -390,51 +428,42 @@ class TestCase(object): ...@@ -390,51 +428,42 @@ class TestCase(object):
result.stopTest(self) result.stopTest(self)
return return
try: try:
success = False outcome = _Outcome()
try: self._outcomeForDoCleanups = outcome
self.setUp()
except SkipTest as e: self._executeTestPart(self.setUp, outcome)
self._addSkip(result, str(e)) if outcome.success:
except Exception: self._executeTestPart(testMethod, outcome, isTest=True)
result.addError(self, sys.exc_info()) self._executeTestPart(self.tearDown, outcome)
self.doCleanups()
if outcome.success:
result.addSuccess(self)
else: else:
try: if outcome.skipped is not None:
testMethod() self._addSkip(result, outcome.skipped)
except self.failureException: for exc_info in outcome.errors:
result.addFailure(self, sys.exc_info()) result.addError(self, exc_info)
except _ExpectedFailure as e: for exc_info in outcome.failures:
addExpectedFailure = getattr(result, 'addExpectedFailure', None) result.addFailure(self, exc_info)
if addExpectedFailure is not None: if outcome.unexpectedSuccess is not None:
addExpectedFailure(self, e.exc_info)
else:
warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
RuntimeWarning)
result.addSuccess(self)
except _UnexpectedSuccess:
addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None) addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
if addUnexpectedSuccess is not None: if addUnexpectedSuccess is not None:
addUnexpectedSuccess(self) addUnexpectedSuccess(self)
else: else:
warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures", warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures",
RuntimeWarning) RuntimeWarning)
result.addFailure(self, sys.exc_info()) result.addFailure(self, outcome.unexpectedSuccess)
except SkipTest as e:
self._addSkip(result, str(e)) if outcome.expectedFailure is not None:
except Exception: addExpectedFailure = getattr(result, 'addExpectedFailure', None)
result.addError(self, sys.exc_info()) if addExpectedFailure is not None:
else: addExpectedFailure(self, outcome.expectedFailure)
success = True else:
warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
RuntimeWarning)
result.addSuccess(self)
try:
self.tearDown()
except Exception:
result.addError(self, sys.exc_info())
success = False
cleanUpSuccess = self.doCleanups()
success = success and cleanUpSuccess
if success:
result.addSuccess(self)
finally: finally:
result.stopTest(self) result.stopTest(self)
if orig_result is None: if orig_result is None:
...@@ -445,16 +474,15 @@ class TestCase(object): ...@@ -445,16 +474,15 @@ class TestCase(object):
def doCleanups(self): def doCleanups(self):
"""Execute all cleanup functions. Normally called for you after """Execute all cleanup functions. Normally called for you after
tearDown.""" tearDown."""
result = self._resultForDoCleanups outcome = self._outcomeForDoCleanups or _Outcome()
ok = True
while self._cleanups: while self._cleanups:
function, args, kwargs = self._cleanups.pop(-1) function, args, kwargs = self._cleanups.pop()
try: part = lambda: function(*args, **kwargs)
function(*args, **kwargs) self._executeTestPart(part, outcome)
except Exception:
ok = False # return this for backwards compatibility
result.addError(self, sys.exc_info()) # even though we no longer us it internally
return ok return outcome.success
def __call__(self, *args, **kwds): def __call__(self, *args, **kwds):
return self.run(*args, **kwds) return self.run(*args, **kwds)
......
...@@ -177,8 +177,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing): ...@@ -177,8 +177,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
super(Foo, self).test() super(Foo, self).test()
raise RuntimeError('raised by Foo.test') raise RuntimeError('raised by Foo.test')
expected = ['startTest', 'setUp', 'test', 'addError', 'tearDown', expected = ['startTest', 'setUp', 'test', 'tearDown',
'stopTest'] 'addError', 'stopTest']
Foo(events).run(result) Foo(events).run(result)
self.assertEqual(events, expected) self.assertEqual(events, expected)
...@@ -195,8 +195,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing): ...@@ -195,8 +195,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
super(Foo, self).test() super(Foo, self).test()
raise RuntimeError('raised by Foo.test') raise RuntimeError('raised by Foo.test')
expected = ['startTestRun', 'startTest', 'setUp', 'test', 'addError', expected = ['startTestRun', 'startTest', 'setUp', 'test',
'tearDown', 'stopTest', 'stopTestRun'] 'tearDown', 'addError', 'stopTest', 'stopTestRun']
Foo(events).run() Foo(events).run()
self.assertEqual(events, expected) self.assertEqual(events, expected)
...@@ -216,8 +216,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing): ...@@ -216,8 +216,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
super(Foo, self).test() super(Foo, self).test()
self.fail('raised by Foo.test') self.fail('raised by Foo.test')
expected = ['startTest', 'setUp', 'test', 'addFailure', 'tearDown', expected = ['startTest', 'setUp', 'test', 'tearDown',
'stopTest'] 'addFailure', 'stopTest']
Foo(events).run(result) Foo(events).run(result)
self.assertEqual(events, expected) self.assertEqual(events, expected)
...@@ -231,8 +231,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing): ...@@ -231,8 +231,8 @@ class Test_TestCase(unittest.TestCase, TestEquality, TestHashing):
super(Foo, self).test() super(Foo, self).test()
self.fail('raised by Foo.test') self.fail('raised by Foo.test')
expected = ['startTestRun', 'startTest', 'setUp', 'test', 'addFailure', expected = ['startTestRun', 'startTest', 'setUp', 'test',
'tearDown', 'stopTest', 'stopTestRun'] 'tearDown', 'addFailure', 'stopTest', 'stopTestRun']
events = [] events = []
Foo(events).run() Foo(events).run()
self.assertEqual(events, expected) self.assertEqual(events, expected)
...@@ -1126,3 +1126,82 @@ test case ...@@ -1126,3 +1126,82 @@ test case
# exercise the TestCase instance in a way that will invoke # exercise the TestCase instance in a way that will invoke
# the type equality lookup mechanism # the type equality lookup mechanism
unpickled_test.assertEqual(set(), set()) unpickled_test.assertEqual(set(), set())
def testKeyboardInterrupt(self):
def _raise(self=None):
raise KeyboardInterrupt
def nothing(self):
pass
class Test1(unittest.TestCase):
test_something = _raise
class Test2(unittest.TestCase):
setUp = _raise
test_something = nothing
class Test3(unittest.TestCase):
test_something = nothing
tearDown = _raise
class Test4(unittest.TestCase):
def test_something(self):
self.addCleanup(_raise)
for klass in (Test1, Test2, Test3, Test4):
with self.assertRaises(KeyboardInterrupt):
klass('test_something').run()
def testSkippingEverywhere(self):
def _skip(self=None):
raise unittest.SkipTest('some reason')
def nothing(self):
pass
class Test1(unittest.TestCase):
test_something = _skip
class Test2(unittest.TestCase):
setUp = _skip
test_something = nothing
class Test3(unittest.TestCase):
test_something = nothing
tearDown = _skip
class Test4(unittest.TestCase):
def test_something(self):
self.addCleanup(_skip)
for klass in (Test1, Test2, Test3, Test4):
result = unittest.TestResult()
klass('test_something').run(result)
self.assertEqual(len(result.skipped), 1)
self.assertEqual(result.testsRun, 1)
def testSystemExit(self):
def _raise(self=None):
raise SystemExit
def nothing(self):
pass
class Test1(unittest.TestCase):
test_something = _raise
class Test2(unittest.TestCase):
setUp = _raise
test_something = nothing
class Test3(unittest.TestCase):
test_something = nothing
tearDown = _raise
class Test4(unittest.TestCase):
def test_something(self):
self.addCleanup(_raise)
for klass in (Test1, Test2, Test3, Test4):
result = unittest.TestResult()
klass('test_something').run(result)
self.assertEqual(len(result.errors), 1)
self.assertEqual(result.testsRun, 1)
...@@ -58,8 +58,8 @@ class Test_FunctionTestCase(unittest.TestCase): ...@@ -58,8 +58,8 @@ class Test_FunctionTestCase(unittest.TestCase):
def tearDown(): def tearDown():
events.append('tearDown') events.append('tearDown')
expected = ['startTest', 'setUp', 'test', 'addError', 'tearDown', expected = ['startTest', 'setUp', 'test', 'tearDown',
'stopTest'] 'addError', 'stopTest']
unittest.FunctionTestCase(test, setUp, tearDown).run(result) unittest.FunctionTestCase(test, setUp, tearDown).run(result)
self.assertEqual(events, expected) self.assertEqual(events, expected)
...@@ -84,8 +84,8 @@ class Test_FunctionTestCase(unittest.TestCase): ...@@ -84,8 +84,8 @@ class Test_FunctionTestCase(unittest.TestCase):
def tearDown(): def tearDown():
events.append('tearDown') events.append('tearDown')
expected = ['startTest', 'setUp', 'test', 'addFailure', 'tearDown', expected = ['startTest', 'setUp', 'test', 'tearDown',
'stopTest'] 'addFailure', 'stopTest']
unittest.FunctionTestCase(test, setUp, tearDown).run(result) unittest.FunctionTestCase(test, setUp, tearDown).run(result)
self.assertEqual(events, expected) self.assertEqual(events, expected)
......
...@@ -34,9 +34,7 @@ class TestCleanUp(unittest.TestCase): ...@@ -34,9 +34,7 @@ class TestCleanUp(unittest.TestCase):
[(cleanup1, (1, 2, 3), dict(four='hello', five='goodbye')), [(cleanup1, (1, 2, 3), dict(four='hello', five='goodbye')),
(cleanup2, (), {})]) (cleanup2, (), {})])
result = test.doCleanups() self.assertTrue(test.doCleanups())
self.assertTrue(result)
self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))]) self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))])
def testCleanUpWithErrors(self): def testCleanUpWithErrors(self):
...@@ -44,14 +42,12 @@ class TestCleanUp(unittest.TestCase): ...@@ -44,14 +42,12 @@ class TestCleanUp(unittest.TestCase):
def testNothing(self): def testNothing(self):
pass pass
class MockResult(object): class MockOutcome(object):
success = True
errors = [] errors = []
def addError(self, test, exc_info):
self.errors.append((test, exc_info))
result = MockResult()
test = TestableTest('testNothing') test = TestableTest('testNothing')
test._resultForDoCleanups = result test._outcomeForDoCleanups = MockOutcome
exc1 = Exception('foo') exc1 = Exception('foo')
exc2 = Exception('bar') exc2 = Exception('bar')
...@@ -65,10 +61,11 @@ class TestCleanUp(unittest.TestCase): ...@@ -65,10 +61,11 @@ class TestCleanUp(unittest.TestCase):
test.addCleanup(cleanup2) test.addCleanup(cleanup2)
self.assertFalse(test.doCleanups()) self.assertFalse(test.doCleanups())
self.assertFalse(MockOutcome.success)
(test1, (Type1, instance1, _)), (test2, (Type2, instance2, _)) = reversed(MockResult.errors) (Type1, instance1, _), (Type2, instance2, _) = reversed(MockOutcome.errors)
self.assertEqual((test1, Type1, instance1), (test, Exception, exc1)) self.assertEqual((Type1, instance1), (Exception, exc1))
self.assertEqual((test2, Type2, instance2), (test, Exception, exc2)) self.assertEqual((Type2, instance2), (Exception, exc2))
def testCleanupInRun(self): def testCleanupInRun(self):
blowUp = False blowUp = False
......
...@@ -23,6 +23,11 @@ Core and Builtins ...@@ -23,6 +23,11 @@ Core and Builtins
Library Library
------- -------
- Issue #10611: SystemExit exception will no longer kill a unittest run.
- Issue #9857: It is now possible to skip a test in a setUp, tearDown or clean
up function.
- Issue #10573: use actual/expected consistently in unittest methods. - Issue #10573: use actual/expected consistently in unittest methods.
The order of the args of assertCountEqual is also changed. The order of the args of assertCountEqual is also changed.
...@@ -322,7 +327,7 @@ Library ...@@ -322,7 +327,7 @@ Library
- configparser: the SafeConfigParser class has been renamed to ConfigParser. - configparser: the SafeConfigParser class has been renamed to ConfigParser.
The legacy ConfigParser class has been removed but its interpolation mechanism The legacy ConfigParser class has been removed but its interpolation mechanism
is still available as LegacyInterpolation. is still available as LegacyInterpolation.
- configparser: Usage of RawConfigParser is now discouraged for new projects - configparser: Usage of RawConfigParser is now discouraged for new projects
in favor of ConfigParser(interpolation=None). in favor of ConfigParser(interpolation=None).
......
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
################################################################## ##################################################################
[project attributes] [project attributes]
proj.directory-list = [{'dirloc': loc('..'), proj.directory-list = [{'dirloc': loc('..'),
'excludes': [u'Lib/__pycache__', 'excludes': [u'Lib/unittest/test/__pycache__',
u'Lib/__pycache__',
u'Doc/build', u'Doc/build',
u'Lib/unittest/__pycache__',
u'build'], u'build'],
'filter': '*', 'filter': '*',
'include_hidden': False, 'include_hidden': False,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment