Skip to content

Commit 84df445

Browse files
authored
Terminate the test loop if shouldfail or shouldstop is set (#1026)
1 parent dde8a66 commit 84df445

File tree

4 files changed

+42
-12
lines changed

4 files changed

+42
-12
lines changed

changelog/1024.bugfix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added proper handling of ``shouldstop`` (such as set by ``--max-fail``) and ``shouldfail`` conditions in workers.
2+
Previously, a worker might have continued executing further tests before the controller could terminate the session.

src/xdist/dsession.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,17 @@ def worker_workerfinished(self, node):
182182
self.shouldstop = f"{node} received keyboard-interrupt"
183183
self.worker_errordown(node, "keyboard-interrupt")
184184
return
185-
if node in self.sched.nodes:
186-
crashitem = self.sched.remove_node(node)
187-
assert not crashitem, (crashitem, node)
185+
shouldfail = node.workeroutput["shouldfail"]
186+
shouldstop = node.workeroutput["shouldstop"]
187+
for shouldx in [shouldfail, shouldstop]:
188+
if shouldx:
189+
if not self.shouldstop:
190+
self.shouldstop = shouldx
191+
break
192+
else:
193+
if node in self.sched.nodes:
194+
crashitem = self.sched.remove_node(node)
195+
assert not crashitem, (crashitem, node)
188196
self._active_nodes.remove(node)
189197

190198
def worker_internal_error(self, node, formatted_error):

src/xdist/remote.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ def pytest_sessionstart(self, session):
103103
def pytest_sessionfinish(self, exitstatus):
104104
# in pytest 5.0+, exitstatus is an IntEnum object
105105
self.config.workeroutput["exitstatus"] = int(exitstatus)
106+
self.config.workeroutput["shouldfail"] = self.session.shouldfail
107+
self.config.workeroutput["shouldstop"] = self.session.shouldstop
106108
yield
107109
self.sendevent("workerfinished", workeroutput=self.config.workeroutput)
108110

@@ -155,6 +157,8 @@ def pytest_runtestloop(self, session):
155157
self.nextitem_index = self._get_next_item_index()
156158
while self.nextitem_index is not self.SHUTDOWN_MARK:
157159
self.run_one_test()
160+
if session.shouldfail or session.shouldstop:
161+
break
158162
return True
159163

160164
def run_one_test(self):

testing/acceptance_test.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,12 @@ def test_skip():
109109
)
110110
assert result.ret == 1
111111

112-
def test_exitfail_waits_for_workers_to_finish(
112+
def test_exitfirst_waits_for_workers_to_finish(
113113
self, pytester: pytest.Pytester
114114
) -> None:
115115
"""The DSession waits for workers before exiting early on failure.
116116
117-
When -x/--exitfail is set, the DSession wait for the workers to finish
117+
When -x/--exitfirst is set, the DSession wait for all workers to finish
118118
before raising an Interrupt exception. This prevents reports from the
119119
faiing test and other tests from being discarded.
120120
"""
@@ -138,15 +138,14 @@ def test_fail6():
138138
time.sleep(0.3)
139139
"""
140140
)
141+
# Two workers are used
141142
result = pytester.runpytest(p1, "-x", "-rA", "-v", "-n2")
142143
assert result.ret == 2
143-
result.stdout.re_match_lines([".*Interrupted: stopping.*[12].*"])
144-
m = re.search(r"== (\d+) failed, (\d+) passed in ", str(result.stdout))
145-
assert m
146-
n_failed, n_passed = (int(s) for s in m.groups())
147-
assert 1 <= n_failed <= 2
148-
assert 1 <= n_passed <= 3
149-
assert (n_passed + n_failed) < 6
144+
# DSession should stop when the first failure is reached. Two failures
145+
# may actually occur, due to timing.
146+
outcomes = result.parseoutcomes()
147+
assert "failed" in outcomes, "Expected at least one failure"
148+
assert 1 <= outcomes["failed"] <= 2, "Expected no more than 2 failures"
150149

151150
def test_basetemp_in_subprocesses(self, pytester: pytest.Pytester) -> None:
152151
p1 = pytester.makepyfile(
@@ -1180,6 +1179,23 @@ def test_aaa1(crasher):
11801179
assert "INTERNALERROR" not in result.stderr.str()
11811180

11821181

1182+
def test_maxfail_causes_early_termination(pytester: pytest.Pytester) -> None:
1183+
"""
1184+
Ensure subsequent tests on a worker aren't run when using --maxfail (#1024).
1185+
"""
1186+
pytester.makepyfile(
1187+
"""
1188+
def test1():
1189+
assert False
1190+
1191+
def test2():
1192+
pass
1193+
"""
1194+
)
1195+
result = pytester.runpytest_subprocess("--maxfail=1", "-n 1")
1196+
result.assert_outcomes(failed=1)
1197+
1198+
11831199
def test_internal_errors_propagate_to_controller(pytester: pytest.Pytester) -> None:
11841200
pytester.makeconftest(
11851201
"""

0 commit comments

Comments
 (0)