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/1278.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Worker process exit codes are now reported when the process terminates abnormally (e.g., via native code calling ``exit()`` directly). The exit code is included in the error message, helping diagnose crashes that bypass Python's exception handling.
28 changes: 27 additions & 1 deletion src/xdist/workermanage.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,25 @@ def notify_inproc(self, eventname: str, **kwargs: object) -> None:
self.log(f"queuing {eventname}(**{kwargs})")
self.putevent((eventname, kwargs))

def _get_worker_exit_code(self) -> int | None:
"""Get the exit code of the worker process, if available.

Returns the exit code if the worker process has terminated,
or None if the exit code cannot be determined (e.g., for
non-popen gateways or if the process hasn't terminated yet).
"""
try:
io = getattr(self.gateway, "_io", None)
if io is None:
return None
popen = getattr(io, "popen", None)
if popen is None:
return None
# Poll to check if process has terminated and get return code
return popen.poll()
except Exception:
return None

def process_from_remote(
self, eventcall: tuple[str, dict[str, Any]] | Literal[Marker.END]
) -> None:
Expand All @@ -404,7 +423,14 @@ def process_from_remote(
err: object | None = self.channel._getremoteerror() # type: ignore[no-untyped-call]
if not self._down:
if not err or isinstance(err, EOFError):
err = "Not properly terminated" # lost connection?
# Check if the worker process exited with non-zero status
# This handles cases like direct exit(1) calls from native code
# where Python exception handling is bypassed (#1278)
exit_code = self._get_worker_exit_code()
if exit_code is not None and exit_code != 0:
err = f"Worker exited with non-zero exit code: {exit_code}"
else:
err = "Not properly terminated" # lost connection?
self.notify_inproc("errordown", node=self, error=err)
self._down = True
return
Expand Down
18 changes: 18 additions & 0 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,24 @@ def test_ok():
)


def test_exit_code_reported_in_error(pytester: pytest.Pytester) -> None:
"""Ensure worker exit code is reported when process terminates (#1278).

This specifically tests for cases where native code terminates the process
directly (e.g., via C's exit() function), bypassing Python exception handling.
"""
pytester.makepyfile(
"""
import os
def test_exit_nonzero():
os._exit(42)
"""
)
result = pytester.runpytest("-n1", "-v")
# The error message should include the exit code
result.stdout.fnmatch_lines(["*exit code*42*"])


def test_multiple_log_reports(pytester: pytest.Pytester) -> None:
"""
Ensure that pytest-xdist supports plugins that emit multiple logreports
Expand Down
Loading