diff --git a/changelog/608.feature.rst b/changelog/608.feature.rst new file mode 100644 index 0000000..130cc11 --- /dev/null +++ b/changelog/608.feature.rst @@ -0,0 +1 @@ +Internal errors in workers are now propagated to the master node. diff --git a/src/xdist/dsession.py b/src/xdist/dsession.py index 07ef091..3db8e02 100644 --- a/src/xdist/dsession.py +++ b/src/xdist/dsession.py @@ -174,6 +174,24 @@ class DSession: assert not crashitem, (crashitem, node) self._active_nodes.remove(node) + def worker_internal_error(self, node, formatted_error): + """ + pytest_internalerror() was called on the worker. + + pytest_internalerror() arguments are an excinfo and an excrepr, which can't + be serialized, so we go with a poor man's solution of raising an exception + here ourselves using the formatted message. + """ + self._active_nodes.remove(node) + try: + assert False, formatted_error + except AssertionError: + from _pytest._code import ExceptionInfo + + excinfo = ExceptionInfo.from_current() + excrepr = excinfo.getrepr() + self.config.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo) + def worker_errordown(self, node, error): """Emitted by the WorkerController when a node dies.""" self.config.hook.pytest_testnodedown(node=node, error=error) diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 97dc180..aaa45be 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -33,8 +33,10 @@ class WorkerInteractor: self.channel.send((name, kwargs)) def pytest_internalerror(self, excrepr): - for line in str(excrepr).split("\n"): + formatted_error = str(excrepr) + for line in formatted_error.split("\n"): self.log("IERROR>", line) + interactor.sendevent("internal_error", formatted_error=formatted_error) def pytest_sessionstart(self, session): self.session = session diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index dfcb59b..6a705d3 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -324,6 +324,8 @@ class WorkerController: self.log("ignoring {}({})".format(eventname, kwargs)) elif eventname == "workerready": self.notify_inproc(eventname, node=self, **kwargs) + elif eventname == "internal_error": + self.notify_inproc(eventname, node=self, **kwargs) elif eventname == "workerfinished": self._down = True self.workeroutput = kwargs["workeroutput"] diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b71d1d9..c273bfa 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1138,6 +1138,18 @@ def test_internal_error_with_maxfail(testdir): assert "INTERNALERROR" not in result.stderr.str() +def test_internal_errors_propagate_to_master(testdir): + testdir.makeconftest( + """ + def pytest_collection_modifyitems(): + raise RuntimeError("Some runtime error") + """ + ) + testdir.makepyfile("def test(): pass") + result = testdir.runpytest("-n1") + result.stdout.fnmatch_lines(["*RuntimeError: Some runtime error*"]) + + class TestLoadScope: def test_by_module(self, testdir): test_file = """