mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
tests: run in parallel with pytest-xdist
pytest now runs with -n auto; pass -n0 to run tests one by one. The pty harness starts pqcli through subprocess instead of pty.fork(), which is unsafe in the threaded xdist workers, and the child claims the pty as its controlling terminal so ^C still raises SIGINT.
This commit is contained in:
3 files changed
+66
-17
No files matched your search
+11
-1
@@ -37,7 +37,13 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["prek>=0.2", "pyte>=0.8", "pytest>=8.0", "ty>=0.0.89"]
|
||||
dev = [
|
||||
"prek>=0.2",
|
||||
"pyte>=0.8",
|
||||
"pytest>=8.0",
|
||||
"pytest-xdist>=3.0",
|
||||
"ty>=0.0.89",
|
||||
]
|
||||
|
||||
[tool.ty.src]
|
||||
# Only the package and its tests; agent worktrees under .claude/ would
|
||||
@@ -53,6 +59,10 @@ select = ["E", "F", "W", "I", "UP"]
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["pqcli"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
# Run tests in parallel; pass -n0 to run them one by one.
|
||||
addopts = "-n auto"
|
||||
|
||||
[tool.coverage.run]
|
||||
# Also measure the pqcli processes the tests start, such as the curses
|
||||
# UI driven through a pseudo-terminal.
|
||||
|
||||
+31
-16
@@ -1,14 +1,14 @@
|
||||
"""Run pqcli in a pseudo-terminal and read its screen through pyte.
|
||||
|
||||
Only works on POSIX: it needs pty.fork and the termios ioctls.
|
||||
Only works on POSIX: it needs a pty and the termios ioctls.
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
@@ -75,18 +75,33 @@ class PtyApp:
|
||||
# LINES/COLUMNS would override the pty size seen by curses.
|
||||
env.pop("LINES", None)
|
||||
env.pop("COLUMNS", None)
|
||||
argv = [sys.executable, "-m", "pqcli", *args]
|
||||
# The child makes the pty its controlling terminal, so that ^C
|
||||
# raises SIGINT as in a real terminal, then becomes pqcli.
|
||||
argv = [
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import fcntl, sys, termios, runpy; "
|
||||
"fcntl.ioctl(0, termios.TIOCSCTTY, 0); "
|
||||
"sys.argv[0] = 'pqcli'; "
|
||||
"runpy.run_module('pqcli', run_name='__main__')",
|
||||
*args,
|
||||
]
|
||||
|
||||
pid, fd = pty.fork()
|
||||
if pid == 0: # pragma: no cover - runs in the child
|
||||
try:
|
||||
_set_size(0, rows, cols)
|
||||
if cwd is not None:
|
||||
os.chdir(cwd)
|
||||
os.execve(sys.executable, argv, env)
|
||||
finally:
|
||||
os._exit(127)
|
||||
self.pid = pid
|
||||
# Popen forks and execs in C, which is safe in the threaded
|
||||
# pytest-xdist workers, unlike pty.fork().
|
||||
fd, child_fd = os.openpty()
|
||||
_set_size(child_fd, rows, cols)
|
||||
self._process = subprocess.Popen(
|
||||
argv,
|
||||
stdin=child_fd,
|
||||
stdout=child_fd,
|
||||
stderr=child_fd,
|
||||
env=env,
|
||||
cwd=cwd,
|
||||
start_new_session=True,
|
||||
)
|
||||
os.close(child_fd)
|
||||
self.pid = self._process.pid
|
||||
self.fd = fd
|
||||
|
||||
# reading ---------------------------------------------------------
|
||||
@@ -202,9 +217,9 @@ class PtyApp:
|
||||
while self.exit_status is None:
|
||||
# keep draining so the child never blocks on a full pty
|
||||
self._pump(0.02)
|
||||
pid, status = os.waitpid(self.pid, os.WNOHANG)
|
||||
if pid:
|
||||
self.exit_status = os.waitstatus_to_exitcode(status)
|
||||
status = self._process.poll()
|
||||
if status is not None:
|
||||
self.exit_status = status
|
||||
break
|
||||
if time.monotonic() > deadline:
|
||||
raise AssertionError(
|
||||
|
||||
@@ -23,6 +23,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "2.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.1"
|
||||
@@ -68,6 +77,7 @@ dev = [
|
||||
{ name = "prek" },
|
||||
{ name = "pyte" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-xdist" },
|
||||
{ name = "ty" },
|
||||
]
|
||||
|
||||
@@ -83,6 +93,7 @@ dev = [
|
||||
{ name = "prek", specifier = ">=0.2" },
|
||||
{ name = "pyte", specifier = ">=0.8" },
|
||||
{ name = "pytest", specifier = ">=8.0" },
|
||||
{ name = "pytest-xdist", specifier = ">=3.0" },
|
||||
{ name = "ty", specifier = ">=0.0.89" },
|
||||
]
|
||||
|
||||
@@ -142,6 +153,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-xdist"
|
||||
version = "3.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "execnet" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.5.0"
|
||||
|
||||
Reference in new issue
Block a user