Propagate internal errors to the master node
This should help users diagnose internal errors in workers like exceptions from hooks or in pytest itself.
This commit is contained in:
1
changelog/608.feature.rst
Normal file
1
changelog/608.feature.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Internal errors in workers are now propagated to the master node.
|
||||||
@@ -174,6 +174,24 @@ class DSession:
|
|||||||
assert not crashitem, (crashitem, node)
|
assert not crashitem, (crashitem, node)
|
||||||
self._active_nodes.remove(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):
|
def worker_errordown(self, node, error):
|
||||||
"""Emitted by the WorkerController when a node dies."""
|
"""Emitted by the WorkerController when a node dies."""
|
||||||
self.config.hook.pytest_testnodedown(node=node, error=error)
|
self.config.hook.pytest_testnodedown(node=node, error=error)
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ class WorkerInteractor:
|
|||||||
self.channel.send((name, kwargs))
|
self.channel.send((name, kwargs))
|
||||||
|
|
||||||
def pytest_internalerror(self, excrepr):
|
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)
|
self.log("IERROR>", line)
|
||||||
|
interactor.sendevent("internal_error", formatted_error=formatted_error)
|
||||||
|
|
||||||
def pytest_sessionstart(self, session):
|
def pytest_sessionstart(self, session):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|||||||
@@ -324,6 +324,8 @@ class WorkerController:
|
|||||||
self.log("ignoring {}({})".format(eventname, kwargs))
|
self.log("ignoring {}({})".format(eventname, kwargs))
|
||||||
elif eventname == "workerready":
|
elif eventname == "workerready":
|
||||||
self.notify_inproc(eventname, node=self, **kwargs)
|
self.notify_inproc(eventname, node=self, **kwargs)
|
||||||
|
elif eventname == "internal_error":
|
||||||
|
self.notify_inproc(eventname, node=self, **kwargs)
|
||||||
elif eventname == "workerfinished":
|
elif eventname == "workerfinished":
|
||||||
self._down = True
|
self._down = True
|
||||||
self.workeroutput = kwargs["workeroutput"]
|
self.workeroutput = kwargs["workeroutput"]
|
||||||
|
|||||||
@@ -1138,6 +1138,18 @@ def test_internal_error_with_maxfail(testdir):
|
|||||||
assert "INTERNALERROR" not in result.stderr.str()
|
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:
|
class TestLoadScope:
|
||||||
def test_by_module(self, testdir):
|
def test_by_module(self, testdir):
|
||||||
test_file = """
|
test_file = """
|
||||||
|
|||||||
Reference in New Issue
Block a user