Skip to content
This repository was archived by the owner on Nov 11, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c62d3ae
fix
ydshieh Nov 18, 2024
c5c02fa
fix more: non subtest skip cases
ydshieh Nov 18, 2024
a439226
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 18, 2024
866c990
fix: # type: ignore[method-assign]
ydshieh Nov 18, 2024
590624d
Merge branch 'fix_skip' of https://github.com/ydshieh/pytest-subtests…
ydshieh Nov 18, 2024
a737260
fix more: # type: ignore[attr-defined]
ydshieh Nov 18, 2024
2e38614
fix more: # type: ignore[attr-defined]
ydshieh Nov 18, 2024
d7f0fa5
fix more: # type: ignore[attr-defined]
ydshieh Nov 18, 2024
aa4a17b
fix more: # type: ignore[attr-defined]
ydshieh Nov 18, 2024
d6e9e5e
fix more: # type: ignore[attr-defined]
ydshieh Nov 18, 2024
a99ea3b
fix None exec_info in subtest error
ydshieh Nov 18, 2024
07ef703
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 18, 2024
67993fe
remove copy
ydshieh Nov 20, 2024
23929b5
style
ydshieh Nov 20, 2024
7461dd6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 20, 2024
92e427c
comment
ydshieh Nov 20, 2024
a9c852e
fix python versions
ydshieh Nov 21, 2024
f9c0fba
fix recursion
ydshieh Nov 21, 2024
aecfe7a
fix recursion
ydshieh Nov 21, 2024
87fcb67
Add first test
ydshieh Nov 21, 2024
01c2b12
Add first test
ydshieh Nov 21, 2024
450e81e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 21, 2024
fd94cb9
add more tests
ydshieh Nov 21, 2024
f75438d
add more tests
ydshieh Nov 21, 2024
d14628b
Merge branch 'fix_skip' of https://github.com/ydshieh/pytest-subtests…
ydshieh Nov 21, 2024
15612c0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 21, 2024
9c4af2d
Fix tests due to column size
nicoddemus Dec 7, 2024
3b6c4ff
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 7, 2024
35711d8
Update CHANGELOG
nicoddemus Dec 7, 2024
475a356
Fix tests due to column size
nicoddemus Dec 7, 2024
da27259
Import protected _SubTest locally
nicoddemus Dec 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/pytest_subtests/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import Mapping
from typing import TYPE_CHECKING
from unittest import TestCase
from unittest.case import _SubTest # type: ignore[attr-defined]

import attr
import pluggy
Expand Down Expand Up @@ -98,6 +99,27 @@ 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
Copy Markdown
Contributor 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)

if self._excinfo is not None:
exc_info = self._excinfo[-1]
self.addSubTest(testcase.test_case, testcase, exc_info) # type: ignore[attr-defined]
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):

@ydshieh ydshieh Nov 21, 2024

Copy link
Copy Markdown
Contributor Author

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

subtest_errors = [
x
for x, y in self.instance._outcome.errors
if isinstance(x, _SubTest) and y is not None
]
if len(subtest_errors) == 0:
self._originaladdSkip(testcase, reason) # type: ignore[attr-defined]
else:
self._originaladdSkip(testcase, reason) # type: ignore[attr-defined]


def _addSubTest(
self: TestCaseFunction,
test_case: Any,
Expand All @@ -122,10 +144,39 @@ def _addSubTest(
node=self, call=call_info, report=sub_report
)

# For python < 3.11: add non-subtest skips once all subtest failures are processed by # `_addSubTest`.
if sys.version_info < (3, 11):
non_subtest_skip = [
(x, y)
for x, y in self.instance._outcome.skipped
if not isinstance(x, _SubTest)
]
subtest_errors = [
(x, y)
for x, y in self.instance._outcome.errors
if isinstance(x, _SubTest) and y is not None
]
# Check if we have non-subtest skips: if there are also sub failures, non-subtest skips are not treated in
# `_addSubTest` and have to be added using `_originaladdSkip` after all subtest failures are processed.
if len(non_subtest_skip) > 0 and len(subtest_errors) > 0:
# Make sure we have processed the last subtest failure
last_subset_error = subtest_errors[-1]
if exc_info is last_subset_error[-1]:
# Add non-subtest skips (as they could not be treated in `_addSkip`)
for testcase, reason in non_subtest_skip:
self._originaladdSkip(testcase, reason) # type: ignore[attr-defined]


def pytest_configure(config: pytest.Config) -> None:
TestCaseFunction.addSubTest = _addSubTest # type: ignore[attr-defined]
TestCaseFunction.failfast = False # type: ignore[attr-defined]
# This condition is to prevent `TestCaseFunction._originaladdSkip` being assigned again in a subprocess from a
# parent python process where `addSkip` is already `_addSkip`. A such case is when running tests in
# `test_subtests.py` where `pytester.runpytest` is used. Without this guard condition, `_originaladdSkip` is
# assigned to `_addSkip` which is wrong as well as causing an infinite recursion in some cases.
if not hasattr(TestCaseFunction, "_originaladdSkip"):
TestCaseFunction._originaladdSkip = TestCaseFunction.addSkip # type: ignore[attr-defined]
TestCaseFunction.addSkip = _addSkip # type: ignore[method-assign]

# Hack (#86): the terminal does not know about the "subtests"
# status, so it will by default turn the output to yellow.
Expand Down Expand Up @@ -154,6 +205,9 @@ def pytest_unconfigure() -> None:
del TestCaseFunction.addSubTest
if hasattr(TestCaseFunction, "failfast"):
del TestCaseFunction.failfast
if hasattr(TestCaseFunction, "_originaladdSkip"):
TestCaseFunction.addSkip = TestCaseFunction._originaladdSkip # type: ignore[method-assign]
del TestCaseFunction._originaladdSkip


@pytest.fixture
Expand Down
56 changes: 56 additions & 0 deletions tests/test_subtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,62 @@ def test_foo(self):
["collected 1 item", "* 3 xfailed, 1 passed in *"]
)

@pytest.mark.parametrize("runner", ["unittest", "pytest-normal", "pytest-xdist"])
def test_skip_with_failure(
self,
pytester: pytest.Pytester,
runner: Literal["unittest", "pytest-normal", "pytest-xdist"],
) -> None:
p = pytester.makepyfile(
"""
import pytest
from unittest import expectedFailure, TestCase, main

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

if __name__ == '__main__':
main()
"""
)
if runner == "unittest":
result = pytester.runpython(p)
result.stderr.re_match_lines(
[
"FAIL: test_foo \(__main__\.T\) \[custom message\] \(i=4\).*",
"FAIL: test_foo \(__main__\.T\) \[custom message\] \(i=9\).*",
"Ran 1 test in .*",
"FAILED \(failures=6, skipped=4\)",
]
)
elif runner == "pytest-normal":
result = pytester.runpytest(p, "-v", "-rsf")
result.stdout.re_match_lines(
[
r"test_skip_with_failure.py::T::test_foo \[custom message\] \(i=0\) SUBSKIP \(skip subtest i=0\) .*",
r"test_skip_with_failure.py::T::test_foo \[custom message\] \(i=3\) SUBSKIP \(skip subtest i=3\) .*",
r"test_skip_with_failure.py::T::test_foo \[custom message\] \(i=4\) SUBFAIL .*",
r"test_skip_with_failure.py::T::test_foo \[custom message\] \(i=9\) SUBFAIL .*",
"test_skip_with_failure.py::T::test_foo PASSED .*",
"[custom message] (i=0) SUBSKIP [1] test_skip_with_failure.py:5: skip subtest i=0",
"[custom message] (i=0) SUBSKIP [1] test_skip_with_failure.py:5: skip subtest i=3",
"[custom message] (i=4) SUBFAIL test_skip_with_failure.py::T::test_foo - AssertionError: assert 4 < 4",
"[custom message] (i=9) SUBFAIL test_skip_with_failure.py::T::test_foo - AssertionError: assert 9 < 4",
".* 6 failed, 1 passed, 4 skipped in .*",
]
)
else:
pytest.xfail("Not producing the expected results (#5)")
result = pytester.runpytest(p) # type:ignore[unreachable]
result.stdout.fnmatch_lines(
["collected 1 item", "* 3 skipped, 1 passed in *"]
)


class TestCapture:
def create_file(self, pytester: pytest.Pytester) -> None:
Expand Down