Use execnet main_thread_only execmodel (#1027)

Use the execnet main_thread_only execmodel so that code which expects
to run in the main thread will just work.  This execmodel has been
merged to the execnet master branch via pytest-dev/execnet#243, so this
patch should not be merged until there is a released version of execnet
supporting the main_thread_only execmodel.

Closes #620
This commit is contained in:
Zac Medico 2024-04-19 04:03:32 -07:00 committed by GitHub
parent 0a4238f6da
commit 20e3ac774e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 13 additions and 5 deletions

View File

@ -0,0 +1,3 @@
``pytest-xdist`` workers now always execute the tests in the main thread.
Previously some tests might end up executing in a separate thread other than ``main`` in the workers, due to some internal `èxecnet`` details. This can cause problems specially with async frameworks where the event loop is running in the ``main`` thread (for example `#620 <https://github.com/pytest-dev/pytest-xdist/issues/620>`__).

1
changelog/620.bugfix Normal file
View File

@ -0,0 +1 @@
Use the ``execnet`` new ``main_thread_only`` "execmodel" so that code which expects to only run in the main thread will now work as expected.

View File

@ -82,7 +82,7 @@ class RemoteControl:
print("RemoteControl:", msg) print("RemoteControl:", msg)
def initgateway(self) -> execnet.Gateway: def initgateway(self) -> execnet.Gateway:
return execnet.makegateway("popen") return execnet.makegateway("execmodel=main_thread_only//popen")
def setup(self) -> None: def setup(self) -> None:
if hasattr(self, "gateway"): if hasattr(self, "gateway"):

View File

@ -56,13 +56,15 @@ class NodeManager:
self.testrunuid = self.config.getoption("testrunuid") self.testrunuid = self.config.getoption("testrunuid")
if self.testrunuid is None: if self.testrunuid is None:
self.testrunuid = uuid.uuid4().hex self.testrunuid = uuid.uuid4().hex
self.group = execnet.Group() self.group = execnet.Group(execmodel="main_thread_only")
if specs is None: if specs is None:
specs = self._getxspecs() specs = self._getxspecs()
self.specs: list[execnet.XSpec] = [] self.specs: list[execnet.XSpec] = []
for spec in specs: for spec in specs:
if not isinstance(spec, execnet.XSpec): if not isinstance(spec, execnet.XSpec):
spec = execnet.XSpec(spec) spec = execnet.XSpec(spec)
if getattr(spec, "execmodel", None) != "main_thread_only":
spec = execnet.XSpec(f"execmodel=main_thread_only//{spec}")
if not spec.chdir and not spec.popen: if not spec.chdir and not spec.popen:
spec.chdir = defaultchdir spec.chdir = defaultchdir
self.group.allocate_id(spec) self.group.allocate_id(spec)
@ -90,6 +92,8 @@ class NodeManager:
spec: execnet.XSpec, spec: execnet.XSpec,
putevent: Callable[[tuple[str, dict[str, Any]]], None], putevent: Callable[[tuple[str, dict[str, Any]]], None],
) -> WorkerController: ) -> WorkerController:
if getattr(spec, "execmodel", None) != "main_thread_only":
spec = execnet.XSpec(f"execmodel=main_thread_only//{spec}")
gw = self.group.makegateway(spec) gw = self.group.makegateway(spec)
self.config.hook.pytest_xdist_newgateway(gateway=gw) self.config.hook.pytest_xdist_newgateway(gateway=gw)
self.rsync_roots(gw) self.rsync_roots(gw)

View File

@ -49,7 +49,7 @@ class WorkerSetup:
def setup(self) -> None: def setup(self) -> None:
self.pytester.chdir() self.pytester.chdir()
# import os ; os.environ['EXECNET_DEBUG'] = "2" # import os ; os.environ['EXECNET_DEBUG'] = "2"
self.gateway = execnet.makegateway() self.gateway = execnet.makegateway("execmodel=main_thread_only//popen")
self.config = config = self.pytester.parseconfigure() self.config = config = self.pytester.parseconfigure()
putevent = self.events.put if self.use_callback else None putevent = self.events.put if self.use_callback else None

View File

@ -83,7 +83,7 @@ class TestNodeManagerPopen:
assert len(call.specs) == 2 assert len(call.specs) == 2
call = hookrecorder.popcall("pytest_xdist_newgateway") call = hookrecorder.popcall("pytest_xdist_newgateway")
assert call.gateway.spec == execnet.XSpec("popen") assert call.gateway.spec == execnet.XSpec("execmodel=main_thread_only//popen")
assert call.gateway.id == "gw0" assert call.gateway.id == "gw0"
call = hookrecorder.popcall("pytest_xdist_newgateway") call = hookrecorder.popcall("pytest_xdist_newgateway")
assert call.gateway.id == "gw1" assert call.gateway.id == "gw1"
@ -177,7 +177,7 @@ class TestHRSync:
assert names == {"dir", "file.txt", "somedir"} assert names == {"dir", "file.txt", "somedir"}
def test_hrsync_one_host(self, source: Path, dest: Path) -> None: def test_hrsync_one_host(self, source: Path, dest: Path) -> None:
gw = execnet.makegateway("popen//chdir=%s" % dest) gw = execnet.makegateway("execmodel=main_thread_only//popen//chdir=%s" % dest)
finished = [] finished = []
rsync = HostRSync(source) rsync = HostRSync(source)
rsync.add_target_host(gw, finished=lambda: finished.append(1)) rsync.add_target_host(gw, finished=lambda: finished.append(1))