From cc5f566bccdacdc4efcf635d344926e550fa84f1 Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Fri, 13 Mar 2026 13:04:59 -0300 Subject: [PATCH] Speed up workspace tests and parallelize make test make test was dominated by teardown-heavy workspace integration tests, not by coverage overhead. Service shutdown was treating zombie processes as live, which forced repeated timeout waits, and one shell test was leaving killpg monkeypatched during cleanup, which made shell close paths burn the full wait budget.\n\nTreat Linux zombie pids as stopped in the workspace manager so service teardown completes promptly. Restore the real killpg implementation before shell test cleanup so the shell close path no longer pays the artificial timeout. Also isolate sys.argv in the runtime-network-check main() test so parallel pytest flags do not leak into argparse-based tests.\n\nAdd pytest-xdist to the dev environment and run make test with pytest -n auto by default so available cores are used automatically during local iteration.\n\nValidation:\n- uv lock\n- targeted hot-spot pytest rerun after the fix dropped the worst tests from roughly 10-21s each to sub-second timings\n- UV_CACHE_DIR=.uv-cache make check\n- UV_CACHE_DIR=.uv-cache make dist-check --- Makefile | 5 +++-- pyproject.toml | 1 + src/pyro_mcp/vm_manager.py | 22 ++++++++++++++++++++++ tests/test_runtime_network_check.py | 3 +++ tests/test_vm_manager.py | 5 +++++ tests/test_workspace_shells.py | 2 ++ uv.lock | 24 ++++++++++++++++++++++++ 7 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1b8ffa1..8c71e5b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ PYTHON ?= uv run python UV_CACHE_DIR ?= .uv-cache +PYTEST_FLAGS ?= -n auto OLLAMA_BASE_URL ?= http://localhost:11434/v1 OLLAMA_MODEL ?= llama3.2:3b OLLAMA_DEMO_FLAGS ?= @@ -27,7 +28,7 @@ help: ' lint Run Ruff lint checks' \ ' format Run Ruff formatter' \ ' typecheck Run mypy' \ - ' test Run pytest' \ + ' test Run pytest in parallel when multiple cores are available' \ ' check Run lint, typecheck, and tests' \ ' dist-check Smoke-test the installed pyro CLI and environment UX' \ ' pypi-publish Build, validate, and upload the package to PyPI' \ @@ -76,7 +77,7 @@ typecheck: uv run mypy test: - uv run pytest + uv run pytest $(PYTEST_FLAGS) check: lint typecheck test diff --git a/pyproject.toml b/pyproject.toml index 09f08ca..044f601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ dev = [ "pre-commit>=4.5.1", "pytest>=9.0.2", "pytest-cov>=7.0.0", + "pytest-xdist>=3.8.0", "ruff>=0.15.4", ] diff --git a/src/pyro_mcp/vm_manager.py b/src/pyro_mcp/vm_manager.py index eb90462..e25be5f 100644 --- a/src/pyro_mcp/vm_manager.py +++ b/src/pyro_mcp/vm_manager.py @@ -1589,6 +1589,23 @@ def _stop_process_group(pid: int, *, wait_seconds: int = 5) -> tuple[bool, bool] return True, True +def _linux_process_state(pid: int) -> str | None: + stat_path = Path("/proc") / str(pid) / "stat" + try: + raw_stat = stat_path.read_text(encoding="utf-8", errors="replace").strip() + except OSError: + return None + if raw_stat == "": + return None + closing_paren = raw_stat.rfind(")") + if closing_paren == -1: + return None + suffix = raw_stat[closing_paren + 2 :] + if suffix == "": + return None + return suffix.split(" ", 1)[0] + + def _run_service_probe_command( cwd: Path, command: str, @@ -1962,6 +1979,11 @@ def _ensure_no_symlink_parents(root: Path, target_path: Path, member_name: str) def _pid_is_running(pid: int | None) -> bool: if pid is None: return False + process_state = _linux_process_state(pid) + if process_state == "Z": + return False + if process_state is not None: + return True try: os.kill(pid, 0) except ProcessLookupError: diff --git a/tests/test_runtime_network_check.py b/tests/test_runtime_network_check.py index 35b5a87..0e26b5d 100644 --- a/tests/test_runtime_network_check.py +++ b/tests/test_runtime_network_check.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys + import pytest import pyro_mcp.runtime_network_check as runtime_network_check @@ -43,6 +45,7 @@ def test_network_check_uses_network_enabled_manager(monkeypatch: pytest.MonkeyPa def test_network_check_main_fails_on_unsuccessful_command( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: + monkeypatch.setattr(sys, "argv", ["runtime-network-check"]) monkeypatch.setattr( runtime_network_check, "run_network_check", diff --git a/tests/test_vm_manager.py b/tests/test_vm_manager.py index f8010d3..22e953c 100644 --- a/tests/test_vm_manager.py +++ b/tests/test_vm_manager.py @@ -2937,6 +2937,11 @@ def test_workspace_service_process_group_helpers(monkeypatch: pytest.MonkeyPatch assert kill_calls == [signal.SIGTERM, signal.SIGKILL] +def test_pid_is_running_treats_zombies_as_stopped(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(vm_manager_module, "_linux_process_state", lambda _pid: "Z") + assert vm_manager_module._pid_is_running(123) is False # noqa: SLF001 + + def test_workspace_service_probe_and_refresh_helpers( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_workspace_shells.py b/tests/test_workspace_shells.py index 4f4f206..b0efcd9 100644 --- a/tests/test_workspace_shells.py +++ b/tests/test_workspace_shells.py @@ -154,6 +154,7 @@ def test_workspace_shells_write_and_signal_runtime_errors( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: + real_killpg = os.killpg session = create_local_shell( workspace_id="workspace-6", shell_id="shell-6", @@ -189,6 +190,7 @@ def test_workspace_shells_write_and_signal_runtime_errors( monkeypatch.setattr("pyro_mcp.workspace_shells.os.killpg", _raise_killpg) with pytest.raises(RuntimeError, match="not running"): session.send_signal("INT") + monkeypatch.setattr("pyro_mcp.workspace_shells.os.killpg", real_killpg) finally: try: session.close() diff --git a/uv.lock b/uv.lock index 9e194db..d688e08 100644 --- a/uv.lock +++ b/uv.lock @@ -275,6 +275,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[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 = "filelock" version = "3.25.0" @@ -718,6 +727,7 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "pytest-xdist" }, { name = "ruff" }, ] @@ -730,6 +740,7 @@ dev = [ { name = "pre-commit", specifier = ">=4.5.1" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "ruff", specifier = ">=0.15.4" }, ] @@ -763,6 +774,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[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 = "python-discovery" version = "1.1.0"