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"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[dependency-groups]
|
[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]
|
[tool.ty.src]
|
||||||
# Only the package and its tests; agent worktrees under .claude/ would
|
# 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]
|
[tool.ruff.lint.isort]
|
||||||
known-first-party = ["pqcli"]
|
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]
|
[tool.coverage.run]
|
||||||
# Also measure the pqcli processes the tests start, such as the curses
|
# Also measure the pqcli processes the tests start, such as the curses
|
||||||
# UI driven through a pseudo-terminal.
|
# UI driven through a pseudo-terminal.
|
||||||
|
|||||||
+31
-16
@@ -1,14 +1,14 @@
|
|||||||
"""Run pqcli in a pseudo-terminal and read its screen through pyte.
|
"""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 fcntl
|
||||||
import os
|
import os
|
||||||
import pty
|
|
||||||
import select
|
import select
|
||||||
import signal
|
import signal
|
||||||
import struct
|
import struct
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import termios
|
import termios
|
||||||
import time
|
import time
|
||||||
@@ -75,18 +75,33 @@ class PtyApp:
|
|||||||
# LINES/COLUMNS would override the pty size seen by curses.
|
# LINES/COLUMNS would override the pty size seen by curses.
|
||||||
env.pop("LINES", None)
|
env.pop("LINES", None)
|
||||||
env.pop("COLUMNS", 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()
|
# Popen forks and execs in C, which is safe in the threaded
|
||||||
if pid == 0: # pragma: no cover - runs in the child
|
# pytest-xdist workers, unlike pty.fork().
|
||||||
try:
|
fd, child_fd = os.openpty()
|
||||||
_set_size(0, rows, cols)
|
_set_size(child_fd, rows, cols)
|
||||||
if cwd is not None:
|
self._process = subprocess.Popen(
|
||||||
os.chdir(cwd)
|
argv,
|
||||||
os.execve(sys.executable, argv, env)
|
stdin=child_fd,
|
||||||
finally:
|
stdout=child_fd,
|
||||||
os._exit(127)
|
stderr=child_fd,
|
||||||
self.pid = pid
|
env=env,
|
||||||
|
cwd=cwd,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
os.close(child_fd)
|
||||||
|
self.pid = self._process.pid
|
||||||
self.fd = fd
|
self.fd = fd
|
||||||
|
|
||||||
# reading ---------------------------------------------------------
|
# reading ---------------------------------------------------------
|
||||||
@@ -202,9 +217,9 @@ class PtyApp:
|
|||||||
while self.exit_status is None:
|
while self.exit_status is None:
|
||||||
# keep draining so the child never blocks on a full pty
|
# keep draining so the child never blocks on a full pty
|
||||||
self._pump(0.02)
|
self._pump(0.02)
|
||||||
pid, status = os.waitpid(self.pid, os.WNOHANG)
|
status = self._process.poll()
|
||||||
if pid:
|
if status is not None:
|
||||||
self.exit_status = os.waitstatus_to_exitcode(status)
|
self.exit_status = status
|
||||||
break
|
break
|
||||||
if time.monotonic() > deadline:
|
if time.monotonic() > deadline:
|
||||||
raise AssertionError(
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -68,6 +77,7 @@ dev = [
|
|||||||
{ name = "prek" },
|
{ name = "prek" },
|
||||||
{ name = "pyte" },
|
{ name = "pyte" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-xdist" },
|
||||||
{ name = "ty" },
|
{ name = "ty" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -83,6 +93,7 @@ dev = [
|
|||||||
{ name = "prek", specifier = ">=0.2" },
|
{ name = "prek", specifier = ">=0.2" },
|
||||||
{ name = "pyte", specifier = ">=0.8" },
|
{ name = "pyte", specifier = ">=0.8" },
|
||||||
{ name = "pytest", specifier = ">=8.0" },
|
{ name = "pytest", specifier = ">=8.0" },
|
||||||
|
{ name = "pytest-xdist", specifier = ">=3.0" },
|
||||||
{ name = "ty", specifier = ">=0.0.89" },
|
{ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|||||||
Reference in new issue
Block a user