Conversation
4f38f9d to
0458427
Compare
| _subtest_msg_sentinel = object() | ||
|
|
||
|
|
||
| class _SubTestInfo: |
There was a problem hiding this comment.
_SubTestInfo is a bare object, but it's passed to result.addSubTest() where a unittest.TestCase is expected. Stdlib's TextTestResult.addSubTest calls getDescription(subtest) ->
subtest.shortDescription(), so running any testtools test that uses subTest under a plain unittest.TextTestRunner aborts the run:
AttributeError: '_SubTestInfo' object has no attribute 'shortDescription'
Reproduced with unittest.TextTestRunner(verbosity=2) on a case with four subtests, two failing.
There was a problem hiding this comment.
OK, I ended up making this class inherit from unittest.TestCase so it has shortDescription and every method/attribute one might expect to be able to use.
| self._run_user(_raise_force_fail_error) | ||
| failed = True | ||
| for subtest, err in getattr(self.case, "_subtest_failures", ()): | ||
| self.result.addSubTest(self.case, subtest, err) |
There was a problem hiding this comment.
If the result object doesn't implement addSubTest, the resulting AttributeError is caught and discarded by the surrounding _run_user machinery, and a test with genuinely failing subtests reports as passing:
events: ['startTest', 'stopTest']
was_successful: True
Silently converting failures into successes is the worst available failure mode here — worth handling the missing-method case explicitly, or guaranteeing addSubTest is present on the decorated result.
There was a problem hiding this comment.
OK, I am now calling addFailure() if addSubTest() does not exist. I think we can expect addFailure to be implemented?
| subtest = _SubTestInfo(self, msg, params) | ||
| try: | ||
| yield | ||
| except Exception: |
There was a problem hiding this comment.
This catches SkipTest, so self.skipTest(...) inside a subTest is recorded as an error rather than a skip. Against stdlib, for a case that skips one subtest and fails another.
There was a problem hiding this comment.
OK I caught SkipTest and reraised the exception.
| self, msg: object = _subtest_msg_sentinel, **params: Any | ||
| ) -> Iterator[None]: | ||
| """Return a context manager for a subTest.""" | ||
| subtest = _SubTestInfo(self, msg, params) |
There was a problem hiding this comment.
There's no nesting stack, so nested subTests lose the outer parameters.
There was a problem hiding this comment.
I did not even think about that use case! I am now keeping a list of parameters.
0458427 to
18ce8f5
Compare
|
@jelmer Thanks for the review, the new patchset should address all your comments. |
| old_params, self._subtest_params = self._subtest_params, merged_params | ||
| try: | ||
| yield | ||
| except SkipTest: |
There was a problem hiding this comment.
is raising here correct? I think we should record this subtest as skipped but still run the other ones
There was a problem hiding this comment.
Indeed, I should have logged the reason for skipping and moved on to the next subtests. This is fixed in the new patchset.
| for subtest, err in getattr(self.case, "_subtest_failures", ()): | ||
| try: | ||
| self.result.addSubTest(self.case, subtest, err) | ||
| except AttributeError: |
There was a problem hiding this comment.
this also catches AttributeErrors from within addSubTest, not just absence of addSubTest itself
There was a problem hiding this comment.
Good catch. I switched to testing whether the method exists before calling it.
18ce8f5 to
77ba2c7
Compare
Commit 267c0be added support for unittest.TestCase.subTest. This meant the following code: import unittest class TestSubTests(unittest.TestCase): def test_even_numbers(self): for i in range(5): with self.subTest(i=i): self.assertEqual(i % 2, 0) would list the exact values of "i" for which the test failed: $ stestr run test_example_subtest 2>&1 | grep ^Captured Captured traceback (i=1): Captured traceback (i=3): Captured traceback (i=1): Captured traceback (i=3): But the following code: import testtools class TestSubTests(testtools.TestCase): def test_even_numbers(self): for i in range(5): with self.subTest(i=i): self.assertEqual(i % 2, 0) would not produce a similar output: $ stestr run test_example_subtest_testtools 2>&1 | grep ^Captured Captured traceback: Captured traceback: This commit fixes this so that we get the same output whether we use unittest.TestCase.subTest or testtools.TestCase.subTest. Closes: testing-cabal#317 Assisted-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Cyril Roelandt <cyril@redhat.com>
77ba2c7 to
f662ae4
Compare
Subtest failures were buffered on the test case and replayed at the end of RunTest._run_core, bypassing testtools' own exception machinery. That lost several behaviours relative to a non-subtest failure: - a subclass overriding `skipException` had its skip reported as an error, since `subTest` caught the hardcoded `unittest.SkipTest` - `expectFailure` inside a subtest leaked the private `_ExpectedFailure` as an error instead of recording an expected failure - `MultipleExceptions` was reported verbatim rather than unpacked - details attached inside a failing subtest never reached the result - `AsynchronousDeferredRunTest._run_core` never drained the buffers, so a failing subtest reported as a pass Report each subtest as it completes instead, dispatching on the case's exception handlers and passing the details accumulated inside the block. Since reporting no longer lives in `_run_core`, the Twisted runner is covered too; it only needs to know not to also report success. Also align with unittest where the buffering had diverged. Follow-up to testing-cabal#630.
Commit 267c0be added support for
unittest.TestCase.subTest. This meant the following code:
would list the exact values of "i" for which the test failed:
$ stestr run test_example_subtest 2>&1 | grep ^Captured
Captured traceback (i=1):
Captured traceback (i=3):
Captured traceback (i=1):
Captured traceback (i=3):
But the following code:
would not produce a similar output:
$ stestr run test_example_subtest_testtools 2>&1 | grep ^Captured
Captured traceback:
Captured traceback:
This commit fixes this so that we get the same output whether we use
unittest.TestCase.subTest or testtools.TestCase.subTest.
Assisted-by: Claude Opus 4.6 (1M context) noreply@anthropic.com
Signed-off-by: Cyril Roelandt cyril@redhat.com