# HG changeset patch # Parent 6d278f426417063b12703f980cb17411849b6326 Issue 12681: Add reason to expectedFailure decorator. diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -552,6 +552,12 @@ Mark the test as an expected failure. If the test fails when run, the test is not counted as a failure. +.. decorator:: expectedFailure(reason) + + Mark the test as an expected failure. If the test fails when run, the test + is not counted as a failure. *reason* should describe why the test is + expected to fail. + .. exception:: SkipTest(reason) This exception is raised to skip a test. @@ -1677,14 +1683,15 @@ .. attribute:: expectedFailures + A list containing 3-tuples of :class:`TestCase` instances, strings + holding formatted tracebacks and strings holding the reason for the failure. + Each tuple represents an expected failure of the test case. + + .. attribute:: unexpectedSuccesses + A list containing 2-tuples of :class:`TestCase` instances and strings - holding formatted tracebacks. Each tuple represents an expected failure - of the test case. - - .. attribute:: unexpectedSuccesses - - A list containing :class:`TestCase` instances that were marked as expected - failures, but succeeded. + holding the expected reason for the failure. Each tuple represents a test + that were marked as expected failures, but succeeded. .. attribute:: shouldStop @@ -1798,23 +1805,25 @@ instance's :attr:`skipped` attribute. - .. method:: addExpectedFailure(test, err) + .. method:: addExpectedFailure(test, err, message) Called when the test case *test* fails, but was marked with the :func:`expectedFailure` decorator. - The default implementation appends a tuple ``(test, formatted_err)`` to + The default implementation appends a tuple ``(test, formatted_err, message)`` to the instance's :attr:`expectedFailures` attribute, where *formatted_err* - is a formatted traceback derived from *err*. - - - .. method:: addUnexpectedSuccess(test) + is a formatted traceback derived from *err*, and *message* is the reason that + the test is expected to fail. + + + .. method:: addUnexpectedSuccess(test, message) Called when the test case *test* was marked with the :func:`expectedFailure` decorator, but succeeded. - The default implementation appends the test to the instance's - :attr:`unexpectedSuccesses` attribute. + The default implementation appends a tuple ``(test, message)`` to the instance's + :attr:`unexpectedSuccesses` attribute, where *message* is the reason that the + test is expected to fail. .. method:: addSubTest(test, subtest, outcome) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -111,6 +111,15 @@ return _id def expectedFailure(test_item): + if isinstance(test_item, str): + # this is the message, the actual test_item + message = test_item + + @functools.wraps(expectedFailure) + def inner(actual_test_item): + actual_test_item.__unittest_expected_failure_message__ = message + return expectedFailure(actual_test_item) + return inner test_item.__unittest_expecting_failure__ = True return test_item @@ -436,7 +445,7 @@ else: result.addError(test, exc_info) - def _addExpectedFailure(self, result, exc_info): + def _addExpectedFailure(self, result, exc_info, message=None): try: addExpectedFailure = result.addExpectedFailure except AttributeError: @@ -444,9 +453,9 @@ RuntimeWarning) result.addSuccess(self) else: - addExpectedFailure(self, exc_info) + addExpectedFailure(self, exc_info, message) - def _addUnexpectedSuccess(self, result): + def _addUnexpectedSuccess(self, result, message=None): try: addUnexpectedSuccess = result.addUnexpectedSuccess except AttributeError: @@ -459,7 +468,7 @@ except _UnexpectedSuccess: result.addFailure(self, sys.exc_info()) else: - addUnexpectedSuccess(self) + addUnexpectedSuccess(self, message) def run(self, result=None): orig_result = result @@ -484,6 +493,8 @@ return expecting_failure = getattr(testMethod, "__unittest_expecting_failure__", False) + expected_failure_message = getattr( + testMethod, "__unittest_expected_failure_message__", None) try: outcome = _Outcome(result) self._outcome = outcome @@ -505,9 +516,12 @@ if outcome.success: if expecting_failure: if outcome.expectedFailure: - self._addExpectedFailure(result, outcome.expectedFailure) + self._addExpectedFailure( + result, outcome.expectedFailure, + expected_failure_message) else: - self._addUnexpectedSuccess(result) + self._addUnexpectedSuccess( + result, expected_failure_message) else: result.addSuccess(self) return result diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -145,15 +145,15 @@ """Called when a test is skipped.""" self.skipped.append((test, reason)) - def addExpectedFailure(self, test, err): + def addExpectedFailure(self, test, err, message=None): """Called when an expected failure/error occured.""" self.expectedFailures.append( - (test, self._exc_info_to_string(err, test))) + (test, self._exc_info_to_string(err, test), message)) @failfast - def addUnexpectedSuccess(self, test): + def addUnexpectedSuccess(self, test, message=None): """Called when a test was expected to fail, but succeed.""" - self.unexpectedSuccesses.append(test) + self.unexpectedSuccesses.append((test, message)) def wasSuccessful(self): "Tells whether or not this result was a success" diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py --- a/Lib/unittest/runner.py +++ b/Lib/unittest/runner.py @@ -87,18 +87,18 @@ self.stream.write("s") self.stream.flush() - def addExpectedFailure(self, test, err): - super(TextTestResult, self).addExpectedFailure(test, err) + def addExpectedFailure(self, test, err, message=None): + super(TextTestResult, self).addExpectedFailure(test, err, message) if self.showAll: - self.stream.writeln("expected failure") + self.stream.writeln("expected failure {0!r}".format(message)) elif self.dots: self.stream.write("x") self.stream.flush() - def addUnexpectedSuccess(self, test): - super(TextTestResult, self).addUnexpectedSuccess(test) + def addUnexpectedSuccess(self, test, message=None): + super(TextTestResult, self).addUnexpectedSuccess(test, message) if self.showAll: - self.stream.writeln("unexpected success") + self.stream.writeln("unexpected success {0!r}".format(message)) elif self.dots: self.stream.write("u") self.stream.flush() diff --git a/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -179,6 +179,66 @@ self.assertTrue(test_case is test) self.assertIsInstance(formatted_exc, str) + # "addExpectedFailure(test, err)" + def test_addExpectedFailure(self): + class Foo(unittest.TestCase): + @unittest.expectedFailure + def test_1(self): + pass + + @unittest.expectedFailure('This fails because 42') + def test_2(self): + pass + + test = Foo('test_1') + try: + test.fail("foo") + except: + exc_info_tuple = sys.exc_info() + + result = unittest.TestResult() + + result.startTest(test) + result.addExpectedFailure(test, exc_info_tuple) + result.stopTest(test) + + self.assertTrue(result.wasSuccessful()) + self.assertEqual(len(result.errors), 0) + self.assertEqual(len(result.failures), 0) + self.assertEqual(len(result.expectedFailures), 1) + self.assertEqual(result.testsRun, 1) + self.assertEqual(result.shouldStop, False) + + test_case, formatted_exc, message = result.expectedFailures[0] + self.assertTrue(test_case is test) + self.assertIsInstance(formatted_exc, str) + self.assertIsNone(message) + + test = Foo('test_2') + try: + test.fail("foo") + except: + exc_info_tuple = sys.exc_info() + + result = unittest.TestResult() + + result.startTest(test) + result.addExpectedFailure( + test, exc_info_tuple, 'This fails because 42') + result.stopTest(test) + + self.assertTrue(result.wasSuccessful()) + self.assertEqual(len(result.errors), 0) + self.assertEqual(len(result.failures), 0) + self.assertEqual(len(result.expectedFailures), 1) + self.assertEqual(result.testsRun, 1) + self.assertEqual(result.shouldStop, False) + + test_case, formatted_exc, message = result.expectedFailures[0] + self.assertTrue(test_case is test) + self.assertIsInstance(formatted_exc, str) + self.assertEqual(message, 'This fails because 42') + # "addError(test, err)" # ... # "Called when the test case test raises an unexpected exception err diff --git a/Lib/unittest/test/test_skipping.py b/Lib/unittest/test/test_skipping.py --- a/Lib/unittest/test/test_skipping.py +++ b/Lib/unittest/test/test_skipping.py @@ -145,6 +145,21 @@ self.assertIs(result.expectedFailures[0][0], test) self.assertTrue(result.wasSuccessful()) + def test_expected_failure_with_message(self): + class Foo(unittest.TestCase): + @unittest.expectedFailure("test message") + def test_die_with_message(self): + self.fail("help me!") + events = [] + result = LoggingResult(events) + test = Foo("test_die_with_message") + test.run(result) + self.assertEqual(events, + ['startTest', 'addExpectedFailure', 'stopTest']) + self.assertEqual(result.expectedFailures[0][0], test) + self.assertEqual(result.expectedFailures[0][2], "test message") + self.assertTrue(result.wasSuccessful()) + def test_unexpected_success(self): class Foo(unittest.TestCase): @unittest.expectedFailure @@ -157,7 +172,7 @@ self.assertEqual(events, ['startTest', 'addUnexpectedSuccess', 'stopTest']) self.assertFalse(result.failures) - self.assertEqual(result.unexpectedSuccesses, [test]) + self.assertEqual(result.unexpectedSuccesses, [(test, None)]) self.assertTrue(result.wasSuccessful()) def test_unexpected_success_subtests(self): @@ -181,7 +196,22 @@ 'addSubTestSuccess', 'addSubTestSuccess', 'addUnexpectedSuccess', 'stopTest']) self.assertFalse(result.failures) - self.assertEqual(result.unexpectedSuccesses, [test]) + self.assertEqual(result.unexpectedSuccesses, [(test, None)]) + self.assertTrue(result.wasSuccessful()) + + def test_unexpected_success_with_message(self): + class Foo(unittest.TestCase): + @unittest.expectedFailure("test message") + def test_die(self): + pass + events = [] + result = LoggingResult(events) + test = Foo("test_die") + test.run(result) + self.assertEqual(events, + ['startTest', 'addUnexpectedSuccess', 'stopTest']) + self.assertFalse(result.failures) + self.assertEqual(result.unexpectedSuccesses, [(test, "test message")]) self.assertTrue(result.wasSuccessful()) def test_skip_doesnt_run_setup(self):