Issue 10611. Issue 9857. Improve the way exception handling, including test skipping, is done inside TestCase.run
diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py
index 177a2fe..b02d475 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -25,7 +25,6 @@
Usually you can use TestResult.skip() or one of the skipping decorators
instead of raising this directly.
"""
- pass
class _ExpectedFailure(Exception):
"""
@@ -42,7 +41,17 @@
"""
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):
return obj
@@ -263,7 +272,7 @@
not have a method with the specified name.
"""
self._testMethodName = methodName
- self._resultForDoCleanups = None
+ self._outcomeForDoCleanups = None
try:
testMethod = getattr(self, methodName)
except AttributeError:
@@ -367,6 +376,36 @@
RuntimeWarning, 2)
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):
orig_result = result
if result is None:
@@ -375,7 +414,6 @@
if startTestRun is not None:
startTestRun()
- self._resultForDoCleanups = result
result.startTest(self)
testMethod = getattr(self, self._testMethodName)
@@ -390,51 +428,42 @@
result.stopTest(self)
return
try:
- success = False
- try:
- self.setUp()
- except SkipTest as e:
- self._addSkip(result, str(e))
- except Exception:
- result.addError(self, sys.exc_info())
+ outcome = _Outcome()
+ self._outcomeForDoCleanups = outcome
+
+ self._executeTestPart(self.setUp, outcome)
+ if outcome.success:
+ self._executeTestPart(testMethod, outcome, isTest=True)
+ self._executeTestPart(self.tearDown, outcome)
+
+ self.doCleanups()
+ if outcome.success:
+ result.addSuccess(self)
else:
- try:
- testMethod()
- except self.failureException:
- result.addFailure(self, sys.exc_info())
- except _ExpectedFailure as e:
- addExpectedFailure = getattr(result, 'addExpectedFailure', None)
- if addExpectedFailure 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:
+ if outcome.skipped is not None:
+ self._addSkip(result, outcome.skipped)
+ for exc_info in outcome.errors:
+ result.addError(self, exc_info)
+ for exc_info in outcome.failures:
+ result.addFailure(self, exc_info)
+ if outcome.unexpectedSuccess is not None:
addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
if addUnexpectedSuccess is not None:
addUnexpectedSuccess(self)
else:
warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures",
RuntimeWarning)
- result.addFailure(self, sys.exc_info())
- except SkipTest as e:
- self._addSkip(result, str(e))
- except Exception:
- result.addError(self, sys.exc_info())
- else:
- success = True
+ result.addFailure(self, outcome.unexpectedSuccess)
- try:
- self.tearDown()
- except Exception:
- result.addError(self, sys.exc_info())
- success = False
+ if outcome.expectedFailure is not None:
+ addExpectedFailure = getattr(result, 'addExpectedFailure', None)
+ if addExpectedFailure is not None:
+ addExpectedFailure(self, outcome.expectedFailure)
+ else:
+ warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
+ RuntimeWarning)
+ result.addSuccess(self)
- cleanUpSuccess = self.doCleanups()
- success = success and cleanUpSuccess
- if success:
- result.addSuccess(self)
finally:
result.stopTest(self)
if orig_result is None:
@@ -445,16 +474,15 @@
def doCleanups(self):
"""Execute all cleanup functions. Normally called for you after
tearDown."""
- result = self._resultForDoCleanups
- ok = True
+ outcome = self._outcomeForDoCleanups or _Outcome()
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
+ function, args, kwargs = self._cleanups.pop()
+ part = lambda: function(*args, **kwargs)
+ self._executeTestPart(part, outcome)
+
+ # return this for backwards compatibility
+ # even though we no longer us it internally
+ return outcome.success
def __call__(self, *args, **kwds):
return self.run(*args, **kwds)