From 01c38219dce33488dc8775c07e1fb1df44bc20b0 Mon Sep 17 00:00:00 2001 From: Prasanna543210 Date: Wed, 28 Oct 2026 13:21:14 +0530 Subject: [PATCH 1/4] subtests: fix negative duration when the clock is mocked When a test freezes or mocks time.time/time.perf_counter (e.g. via monkeypatch), the duration computed for a subtest could end up negative, which produced invalid negative time values in the JUnit XML report. Compute the subtest duration with timing.Instant, which uses the clock values pytest captured itself, so user code that patches the time module no longer affects the reported duration. Add a regression test that freezes the clock inside a subtest and checks that the generated --junitxml report has no negative times. --- src/_pytest/subtests.py | 16 ++++++---------- testing/test_subtests.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/_pytest/subtests.py b/src/_pytest/subtests.py index 0c66e289a4d..307c72163fe 100644 --- a/src/_pytest/subtests.py +++ b/src/_pytest/subtests.py @@ -11,13 +11,12 @@ from contextlib import ExitStack from contextlib import nullcontext import dataclasses -import time from types import TracebackType from typing import Any from typing import TYPE_CHECKING import pluggy - +from _pytest import timing from _pytest._code import ExceptionInfo from _pytest._io.saferepr import saferepr from _pytest.capture import CaptureFixture @@ -216,8 +215,7 @@ class _SubTestContextManager: def __enter__(self) -> None: __tracebackhide__ = True - self._start = time.time() - self._precise_start = time.perf_counter() + self._timing = timing.Instant() self._exc_info = None self._exit_stack = ExitStack() @@ -242,16 +240,14 @@ def __exit__( self._exit_stack.close() - precise_stop = time.perf_counter() - duration = precise_stop - self._precise_start - stop = time.time() + duration = self._timing.elapsed() call_info = CallInfo[None]( None, exc_info, - start=self._start, - stop=stop, - duration=duration, + start=duration.start.time, + stop=duration.stop.time, + duration=duration.seconds, when="call", _ispytest=True, ) diff --git a/testing/test_subtests.py b/testing/test_subtests.py index a64897c6fc3..f30904806a2 100644 --- a/testing/test_subtests.py +++ b/testing/test_subtests.py @@ -1096,3 +1096,22 @@ def test(self): pytester.syspathinsert() result = pytester.runpytest("-n1", "-pxdist.plugin") result.assert_outcomes(passed=2) +def test_subtest_with_mocked_clock(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import time + + def test_with_subtest(subtests, monkeypatch): + real_sleep = time.sleep + monkeypatch.setattr(time, "perf_counter", lambda: 0.0) + monkeypatch.setattr(time, "time", lambda: 0.0) + + with subtests.test("frozen clock"): + real_sleep(0.01) + """ + ) + result = pytester.runpytest("--junitxml=report.xml") + result.assert_outcomes(passed=1) + + xml = pytester.path.joinpath("report.xml").read_text() + assert 'time="-' not in xml \ No newline at end of file From 760e58d4115625873553c8898d1659590c7a9592 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:55:04 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/subtests.py | 1 + testing/test_subtests.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/subtests.py b/src/_pytest/subtests.py index 307c72163fe..7c915940fa4 100644 --- a/src/_pytest/subtests.py +++ b/src/_pytest/subtests.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING import pluggy + from _pytest import timing from _pytest._code import ExceptionInfo from _pytest._io.saferepr import saferepr diff --git a/testing/test_subtests.py b/testing/test_subtests.py index f30904806a2..a8c9f0a5aa7 100644 --- a/testing/test_subtests.py +++ b/testing/test_subtests.py @@ -1096,6 +1096,8 @@ def test(self): pytester.syspathinsert() result = pytester.runpytest("-n1", "-pxdist.plugin") result.assert_outcomes(passed=2) + + def test_subtest_with_mocked_clock(pytester: pytest.Pytester) -> None: pytester.makepyfile( """ @@ -1114,4 +1116,4 @@ def test_with_subtest(subtests, monkeypatch): result.assert_outcomes(passed=1) xml = pytester.path.joinpath("report.xml").read_text() - assert 'time="-' not in xml \ No newline at end of file + assert 'time="-' not in xml From 9d4f2a30525da47a961dbf37d7557c83aaebf72f Mon Sep 17 00:00:00 2001 From: Prasanna543210 Date: Wed, 28 Oct 2026 13:29:18 +0530 Subject: [PATCH 3/4] Add changelog entry --- changelog/15277.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/15277.bugfix.rst diff --git a/changelog/15277.bugfix.rst b/changelog/15277.bugfix.rst new file mode 100644 index 00000000000..ff38059b7a6 --- /dev/null +++ b/changelog/15277.bugfix.rst @@ -0,0 +1 @@ +Fixed negative durations being reported for subtests when the clock is mocked or frozen. From eff05af89e6a0c1968e9447b80f3042a67a52838 Mon Sep 17 00:00:00 2001 From: Prasanna543210 Date: Wed, 28 Oct 2026 14:56:00 +0530 Subject: [PATCH 4/4] test_subtests: pass encoding='utf-8' to read_text() --- testing/test_subtests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_subtests.py b/testing/test_subtests.py index a8c9f0a5aa7..a5066b0e991 100644 --- a/testing/test_subtests.py +++ b/testing/test_subtests.py @@ -1115,5 +1115,5 @@ def test_with_subtest(subtests, monkeypatch): result = pytester.runpytest("--junitxml=report.xml") result.assert_outcomes(passed=1) - xml = pytester.path.joinpath("report.xml").read_text() + xml = pytester.path.joinpath("report.xml").read_text(encoding="utf-8") assert 'time="-' not in xml