Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changelog/10562.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``--stepwise`` now continues from the test that was running when the session was interrupted, for example by quitting the debugger with ``q``, instead of starting over.
16 changes: 15 additions & 1 deletion src/_pytest/stepwise.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from _pytest import nodes
from _pytest.cacheprovider import Cache
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config.argparsing import Parser
from _pytest.main import Session
from _pytest.nodeid import NodeId
Expand Down Expand Up @@ -107,6 +108,8 @@ def __init__(self, config: Config) -> None:
self.skip: bool = config.getoption("stepwise_skip")
self.reset: bool = config.getoption("stepwise_reset")
self.cached_info = self._load_cached_info()
# The test currently running, if any.
self.running_test: NodeId | None = None

def _load_cached_info(self) -> StepwiseCacheInfo:
cached_dict: dict[str, Any] | None = self.cache.get(STEPWISE_CACHE_DIR, None)
Expand Down Expand Up @@ -197,16 +200,27 @@ def pytest_runtest_logreport(self, report: TestReport) -> None:
if report.id == self.cached_info.last_failed:
self.cached_info.last_failed = None

def pytest_runtest_logstart(self, nodeid: str) -> None:
self.running_test = NodeId.parse(nodeid)

def pytest_runtest_logfinish(self) -> None:
self.running_test = None

def pytest_report_collectionfinish(self) -> list[str] | None:
if self.config.get_verbosity() >= 0 and self.report_status:
return [f"stepwise: {x}" for x in self.report_status]
return None

def pytest_sessionfinish(self) -> None:
def pytest_sessionfinish(self, exitstatus: int | ExitCode) -> None:
if hasattr(self.config, "workerinput"):
# Do not update cache if this process is a xdist worker to prevent
# race conditions (#10641).
return
if exitstatus == ExitCode.INTERRUPTED and self.running_test is not None:
# The session was interrupted while a test was running, for example
# by quitting the debugger, so that test has no failed report:
# continue from it next time (#10562).
self.cached_info.last_failed = self.running_test
self.cached_info.update_date_to_now()
last_failed = self.cached_info.last_failed
self.cache.set(
Expand Down
36 changes: 36 additions & 0 deletions testing/test_stepwise.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,3 +543,39 @@ def test_1(): pass
"* 1 passed in *",
]
)


def test_continue_from_test_interrupted_by_quitting_debugger(
pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
"""Quitting the debugger interrupts the session without a failed report,
--stepwise should continue from that test next time (#10562)."""
pytester.makepyfile(
"""
import os
import pytest

def test_1(): pass

def test_2():
if os.environ.get("QUIT_DEBUGGER"):
# What pytest does when "q" is used in the debugger.
pytest.exit("Quitting debugger")

def test_3(): pass
"""
)
monkeypatch.setenv("QUIT_DEBUGGER", "1")
result = pytester.runpytest("--stepwise", "-v")
result.stdout.fnmatch_lines(["*::test_1 *PASSED*", "*Quitting debugger*"])

monkeypatch.delenv("QUIT_DEBUGGER")
result = pytester.runpytest("--stepwise", "-v")
result.stdout.fnmatch_lines(
[
"stepwise: skipping 1 already passed items (cache from *, use --sw-reset to discard).",
"*::test_2 *PASSED*",
"*::test_3 *PASSED*",
"* 2 passed, 1 deselected in *",
]
)
Loading