Terminate the test loop if shouldfail or shouldstop is set (#1026)

This commit is contained in:
Ben Brown
2024-04-01 11:00:50 -04:00
committed by GitHub
parent dde8a66da5
commit 84df4451ed
4 changed files with 42 additions and 12 deletions

2
changelog/1024.bugfix Normal file
View File

@@ -0,0 +1,2 @@
Added proper handling of ``shouldstop`` (such as set by ``--max-fail``) and ``shouldfail`` conditions in workers.
Previously, a worker might have continued executing further tests before the controller could terminate the session.

View File

@@ -182,6 +182,14 @@ class DSession:
self.shouldstop = f"{node} received keyboard-interrupt" self.shouldstop = f"{node} received keyboard-interrupt"
self.worker_errordown(node, "keyboard-interrupt") self.worker_errordown(node, "keyboard-interrupt")
return return
shouldfail = node.workeroutput["shouldfail"]
shouldstop = node.workeroutput["shouldstop"]
for shouldx in [shouldfail, shouldstop]:
if shouldx:
if not self.shouldstop:
self.shouldstop = shouldx
break
else:
if node in self.sched.nodes: if node in self.sched.nodes:
crashitem = self.sched.remove_node(node) crashitem = self.sched.remove_node(node)
assert not crashitem, (crashitem, node) assert not crashitem, (crashitem, node)

View File

@@ -103,6 +103,8 @@ class WorkerInteractor:
def pytest_sessionfinish(self, exitstatus): def pytest_sessionfinish(self, exitstatus):
# in pytest 5.0+, exitstatus is an IntEnum object # in pytest 5.0+, exitstatus is an IntEnum object
self.config.workeroutput["exitstatus"] = int(exitstatus) self.config.workeroutput["exitstatus"] = int(exitstatus)
self.config.workeroutput["shouldfail"] = self.session.shouldfail
self.config.workeroutput["shouldstop"] = self.session.shouldstop
yield yield
self.sendevent("workerfinished", workeroutput=self.config.workeroutput) self.sendevent("workerfinished", workeroutput=self.config.workeroutput)
@@ -155,6 +157,8 @@ class WorkerInteractor:
self.nextitem_index = self._get_next_item_index() self.nextitem_index = self._get_next_item_index()
while self.nextitem_index is not self.SHUTDOWN_MARK: while self.nextitem_index is not self.SHUTDOWN_MARK:
self.run_one_test() self.run_one_test()
if session.shouldfail or session.shouldstop:
break
return True return True
def run_one_test(self): def run_one_test(self):

View File

@@ -109,12 +109,12 @@ class TestDistribution:
) )
assert result.ret == 1 assert result.ret == 1
def test_exitfail_waits_for_workers_to_finish( def test_exitfirst_waits_for_workers_to_finish(
self, pytester: pytest.Pytester self, pytester: pytest.Pytester
) -> None: ) -> None:
"""The DSession waits for workers before exiting early on failure. """The DSession waits for workers before exiting early on failure.
When -x/--exitfail is set, the DSession wait for the workers to finish When -x/--exitfirst is set, the DSession wait for all workers to finish
before raising an Interrupt exception. This prevents reports from the before raising an Interrupt exception. This prevents reports from the
faiing test and other tests from being discarded. faiing test and other tests from being discarded.
""" """
@@ -138,15 +138,14 @@ class TestDistribution:
time.sleep(0.3) time.sleep(0.3)
""" """
) )
# Two workers are used
result = pytester.runpytest(p1, "-x", "-rA", "-v", "-n2") result = pytester.runpytest(p1, "-x", "-rA", "-v", "-n2")
assert result.ret == 2 assert result.ret == 2
result.stdout.re_match_lines([".*Interrupted: stopping.*[12].*"]) # DSession should stop when the first failure is reached. Two failures
m = re.search(r"== (\d+) failed, (\d+) passed in ", str(result.stdout)) # may actually occur, due to timing.
assert m outcomes = result.parseoutcomes()
n_failed, n_passed = (int(s) for s in m.groups()) assert "failed" in outcomes, "Expected at least one failure"
assert 1 <= n_failed <= 2 assert 1 <= outcomes["failed"] <= 2, "Expected no more than 2 failures"
assert 1 <= n_passed <= 3
assert (n_passed + n_failed) < 6
def test_basetemp_in_subprocesses(self, pytester: pytest.Pytester) -> None: def test_basetemp_in_subprocesses(self, pytester: pytest.Pytester) -> None:
p1 = pytester.makepyfile( p1 = pytester.makepyfile(
@@ -1180,6 +1179,23 @@ def test_internal_error_with_maxfail(pytester: pytest.Pytester) -> None:
assert "INTERNALERROR" not in result.stderr.str() assert "INTERNALERROR" not in result.stderr.str()
def test_maxfail_causes_early_termination(pytester: pytest.Pytester) -> None:
"""
Ensure subsequent tests on a worker aren't run when using --maxfail (#1024).
"""
pytester.makepyfile(
"""
def test1():
assert False
def test2():
pass
"""
)
result = pytester.runpytest_subprocess("--maxfail=1", "-n 1")
result.assert_outcomes(failed=1)
def test_internal_errors_propagate_to_controller(pytester: pytest.Pytester) -> None: def test_internal_errors_propagate_to_controller(pytester: pytest.Pytester) -> None:
pytester.makeconftest( pytester.makeconftest(
""" """