Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix skipTest inside subTest #169

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open

Conversation

ydshieh
Copy link

@ydshieh ydshieh commented Nov 18, 2024

Currently, the reporting has issues in the cases where skipTest is used inside subTest. For example

import unittest

class T(unittest.TestCase):
    def test_foo(self):
        for i in range(10):
            with self.subTest("custom message", i=i):
                if i < 4:
                    self.skipTest(f"skip {i}")
                assert i < 4

outputs

tests/test_foo.py::T::test_foo [custom message] (i=4) SUBSKIP (skip 0)
tests/test_foo.py::T::test_foo [custom message] (i=5) SUBSKIP (skip 1)
tests/test_foo.py::T::test_foo [custom message] (i=6) SUBSKIP (skip 2)
tests/test_foo.py::T::test_foo [custom message] (i=7) SUBSKIP (skip 3)
tests/test_foo.py::T::test_foo [custom message] (i=8) SUBFAIL
tests/test_foo.py::T::test_foo [custom message] (i=9) SUBFAIL
tests/test_foo.py::T::test_foo PASSED
...
=============== short test summary info ===============
[custom message] (i=8) SUBFAIL tests/test_foo.py::T::test_foo - AssertionError: assert 8 < 4
[custom message] (i=9) SUBFAIL tests/test_foo.py::T::test_foo - AssertionError: assert 9 < 4
=============== 2 failed, 1 passed, 4 skipped in 0.15s  =============== 

which is obviously wrong.

This PR fix the above issue. The new output is

tests/test_foo.py::T::test_foo [custom message] (i=0) SUBSKIP (skip 0)
tests/test_foo.py::T::test_foo [custom message] (i=1) SUBSKIP (skip 1)
tests/test_foo.py::T::test_foo [custom message] (i=2) SUBSKIP (skip 2)
tests/test_foo.py::T::test_foo [custom message] (i=3) SUBSKIP (skip 3)
tests/test_foo.py::T::test_foo [custom message] (i=4) SUBFAIL
tests/test_foo.py::T::test_foo [custom message] (i=5) SUBFAIL
tests/test_foo.py::T::test_foo [custom message] (i=6) SUBFAIL
tests/test_foo.py::T::test_foo [custom message] (i=7) SUBFAIL
tests/test_foo.py::T::test_foo [custom message] (i=8) SUBFAIL
tests/test_foo.py::T::test_foo [custom message] (i=9) SUBFAIL
tests/test_foo.py::T::test_foo PASSED                                                                                                                                                                        
...
=============== short test summary info ===============
 [custom message] (i=4) SUBFAIL tests/test_foo.py::T::test_foo - AssertionError: assert 4 < 4
[custom message] (i=5) SUBFAIL tests/test_foo.py::T::test_foo - AssertionError: assert 5 < 4
[custom message] (i=6) SUBFAIL tests/test_foo.py::T::test_foo - AssertionError: assert 6 < 4
[custom message] (i=7) SUBFAIL tests/test_foo.py::T::test_foo - AssertionError: assert 7 < 4
[custom message] (i=8) SUBFAIL tests/test_foo.py::T::test_foo - AssertionError: assert 8 < 4
[custom message] (i=9) SUBFAIL tests/test_foo.py::T::test_foo - AssertionError: assert 9 < 4
=============== 6 failed, 1 passed, 4 skipped in 0.20s  =============== 

Comment on lines 164 to 165
TestCaseFunction._originaladdSkip = copy.copy(TestCaseFunction.addSkip) # type: ignore[attr-defined]
TestCaseFunction.addSkip = _addSkip # type: ignore[method-assign]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _addSkip in unittest.case doesn't contain logic to call addSubTest for outcome.skipped (unlike _feedErrorsToResult), and it turns out this causes the reporting issue described in the PR.

Unlike addSubTest (where original TestCaseFunction doesn't have it), addSkip is defined TestCaseFunction, and here we save the original implementation and patch TestCaseFunction.addSkip to a newly defined _addSkip that allows us to call addSubTest.

The _originaladdSkip allows us to restore to it in pytest_unconfigure as well as it turns out necessary to call it too inside the newly defined _addSkip.

@dvrogozh
Copy link

tests/test_foo.py::T::test_foo PASSED

Why the whole test is marked as passed if all subtests either failed or skipped? or we need to consider that there might be a testing code in the test out of subtest code? If so, I wish for documentation example for that with description.

I also wonder how python unittest reports such failures? is pytest-subtests behavior aligned?

@ydshieh
Copy link
Author

ydshieh commented Nov 18, 2024

tests/test_foo.py::T::test_foo PASSED

Why the whole test is marked as passed if all subtests either failed or skipped? or we need to consider that there might be a testing code in the test out of subtest code? If so, I wish for documentation example for that with description.

I also wonder how python unittest reports such failures? is pytest-subtests behavior aligned?

I am not 100% sure though as I don't dive into the whole unittest / pytest-subtests, but

there might be a testing code in the test out of subtest code?`

is what I observed (even on the current main branch).

I also wonder how python unittest reports such failures? is pytest-subtests behavior aligned?

I will check.

well, unittest gives

FAILED (failures=6, skipped=4)

@@ -98,6 +100,24 @@ def _from_test_report(cls, test_report: TestReport) -> SubTestReport:
return super()._from_json(test_report._to_json())


def _addSkip(self: TestCaseFunction, testcase: TestCase, reason: str) -> None:
if isinstance(testcase, _SubTest):
self._originaladdSkip(testcase, reason) # type: ignore[attr-defined]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we don't really need call self._originaladdSkip for subtest skips, i.e. the following also works

        try:
            raise pytest.skip.Exception(reason, _use_item_location=True)
        except skip.Exception:
            exc_info = sys.exc_info()
            self.addSubTest(testcase.test_case, testcase, exc_info)

@ydshieh
Copy link
Author

ydshieh commented Nov 19, 2024

cc @nicoddemus

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ydshieh thanks for the PR

Other than my comments, we need a test to ensure this does not regress in the future.

src/pytest_subtests/plugin.py Outdated Show resolved Hide resolved
src/pytest_subtests/plugin.py Outdated Show resolved Hide resolved
src/pytest_subtests/plugin.py Outdated Show resolved Hide resolved
src/pytest_subtests/plugin.py Outdated Show resolved Hide resolved
@ydshieh
Copy link
Author

ydshieh commented Nov 20, 2024

Hi @ydshieh thanks for the PR

Other than my comments, we need a test to ensure this does not regress in the future.

Good point! I will add some tests

else:
# For python < 3.11: the non-subtest skips have to be added by `_originaladdSkip` only after all subtest
# failures are processed by `_addSubTest`.
if sys.version_info < (3, 11):
Copy link
Author

@ydshieh ydshieh Nov 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python unittest has some changes (since 3.11) which leads to this if/else here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants