diff --git a/changelog/1278.bugfix.rst b/changelog/1278.bugfix.rst new file mode 100644 index 00000000..58e3a39e --- /dev/null +++ b/changelog/1278.bugfix.rst @@ -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. diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index 201c8e71..56450260 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -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: @@ -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 diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 1b44985d..105e16dc 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -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