use uv to run test, more fixes

This commit is contained in:
2026-07-07 23:43:48 +08:00
parent f7d6a51220
commit 268483577e
16 changed files with 705 additions and 127 deletions

View File

@@ -1,4 +1,16 @@
"""pytest fixtures for gammaray-mcp bridge tests."""
"""pytest fixtures for gammaray-mcp bridge tests.
All paths are configurable via environment variables. run_tests.sh is the
canonical configuration point — it sets sensible defaults and exports them
before calling pytest. If you prefer to run pytest directly, set these:
GAMMARAY_BIN — path to the gammaray probe binary
GAMMARAY_LIB_DIR — directory containing GammaRay shared libraries
QML_RUNNER — path to the QML runtime executable
TEST_QML — path to the test QML application
BRIDGE_EXE — path to the bridge executable
BRIDGE_RUN_SCRIPT — path to bridge/run.sh
"""
import os
import subprocess
@@ -10,11 +22,33 @@ import pytest
from mcp_client import McpClient
PROJECT_ROOT = Path(__file__).resolve().parent.parent
BRIDGE_EXE = PROJECT_ROOT / "bridge" / "build" / "qml-sg-mcp-bridge"
RUN_SCRIPT = PROJECT_ROOT / "bridge" / "run.sh"
PROBE_BIN = PROJECT_ROOT / "install-prefix" / "bin" / "gammaray"
TEST_QML = Path(os.environ.get("TEST_QML", str(Path.home() / "Sources" / "testqml" / "relayout.qml")))
QML_RUNNER = "/usr/lib/qt6/bin/qml"
# All paths read from env vars, with sensible project-local defaults.
# Users only need to override these when GammaRay is installed elsewhere.
GAMMARAY_BIN = Path(os.environ.get(
"GAMMARAY_BIN",
str(PROJECT_ROOT / "install-prefix" / "bin" / "gammaray"),
))
GAMMARAY_LIB_DIR = os.environ.get(
"GAMMARAY_LIB_DIR",
str(PROJECT_ROOT / "install-prefix" / "lib"),
)
QML_RUNNER = Path(os.environ.get(
"QML_RUNNER",
"/usr/lib/qt6/bin/qml",
))
TEST_QML = Path(os.environ.get(
"TEST_QML",
str(PROJECT_ROOT / "tests" / "testapp" / "main.qml"),
))
BRIDGE_EXE = Path(os.environ.get(
"BRIDGE_EXE",
str(PROJECT_ROOT / "bridge" / "build" / "qml-sg-mcp-bridge"),
))
BRIDGE_RUN_SCRIPT = Path(os.environ.get(
"BRIDGE_RUN_SCRIPT",
str(PROJECT_ROOT / "bridge" / "run.sh"),
))
def pytest_addoption(parser):
@@ -42,7 +76,6 @@ def pytest_collection_modifyitems(config, items):
@pytest.fixture(scope="session")
def bridge_path() -> Path:
"""Path to the bridge executable."""
if not BRIDGE_EXE.exists():
pytest.skip(f"Bridge not built at {BRIDGE_EXE}")
return BRIDGE_EXE
@@ -50,20 +83,19 @@ def bridge_path() -> Path:
@pytest.fixture(scope="session")
def run_script_path() -> Path:
"""Path to the run.sh wrapper."""
if not RUN_SCRIPT.exists():
pytest.skip(f"run.sh not found at {RUN_SCRIPT}")
return RUN_SCRIPT
if not BRIDGE_RUN_SCRIPT.exists():
pytest.skip(f"run.sh not found at {BRIDGE_RUN_SCRIPT}")
return BRIDGE_RUN_SCRIPT
@pytest.fixture(scope="session")
@pytest.fixture(scope="module")
def probe_process(request) -> subprocess.Popen | None:
"""Start a GammaRay probe process (session-scoped, one per test run)."""
if request.config.getoption("--no-probe"):
return None
if not PROBE_BIN.exists():
pytest.skip(f"GammaRay probe binary not found at {PROBE_BIN}")
if not GAMMARAY_BIN.exists():
pytest.skip(f"GammaRay probe binary not found at {GAMMARAY_BIN}")
if not TEST_QML.exists():
pytest.skip(f"Test QML file not found at {TEST_QML}")
if not QML_RUNNER.exists():
@@ -71,7 +103,7 @@ def probe_process(request) -> subprocess.Popen | None:
env = os.environ.copy()
env["QT_QPA_PLATFORM"] = "offscreen"
env["LD_LIBRARY_PATH"] = str(PROJECT_ROOT / "install-prefix" / "lib")
env["LD_LIBRARY_PATH"] = GAMMARAY_LIB_DIR
proc = None
used_systemd = False
@@ -94,20 +126,21 @@ def probe_process(request) -> subprocess.Popen | None:
"--working-directory", str(TEST_QML.parent),
"-E", f"QT_QPA_PLATFORM={env['QT_QPA_PLATFORM']}",
"-E", f"LD_LIBRARY_PATH={env['LD_LIBRARY_PATH']}",
str(PROBE_BIN),
str(GAMMARAY_BIN),
"--inject-only", "--listen", "tcp://127.0.0.1:11732",
"--injector", "preload",
QML_RUNNER, str(TEST_QML),
str(QML_RUNNER), str(TEST_QML),
],
capture_output=True, text=True, timeout=10,
)
if result.returncode == 0:
used_systemd = True
proc = subprocess.Popen(["true"]) # sentinel: probe is external
else:
# Fallback: direct spawn
proc = subprocess.Popen(
[str(PROBE_BIN), "--inject-only", "--listen", "tcp://127.0.0.1:11732",
"--injector", "preload", QML_RUNNER, str(TEST_QML)],
[str(GAMMARAY_BIN), "--inject-only", "--listen", "tcp://127.0.0.1:11732",
"--injector", "preload", str(QML_RUNNER), str(TEST_QML)],
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
@@ -156,15 +189,35 @@ def bridge(run_script_path) -> McpClient:
c = McpClient(bridge_path=run_script_path)
c.initialize()
yield c
# Gracefully disconnect from the probe before killing the bridge.
# GammaRay's QTcpSocket::disconnectFromHost() is asynchronous — the
# Endpoint::disconnected signal (which clears m_socket) fires on a later
# event loop iteration. If we kill the bridge (SIGTERM) while m_socket is
# still set, the probe does not immediately detect the disconnection, and
# the next test's bridge cannot connect. Sending disconnectProbe() first
# triggers a clean GammaRay protocol-level shutdown that waits for the
# async disconnect to complete, releasing the probe for the next test.
try:
c.call_tool("disconnectProbe", timeout=5)
except Exception:
pass # bridge may have already crashed; best-effort
c.close()
# Allow the probe to detect the disconnection before the next test
# connects.
time.sleep(1)
@pytest.fixture
def connected_bridge(bridge, probe_process) -> McpClient:
"""Bridge connected to a running probe."""
"""Bridge connected to a running probe (retries on transient failures)."""
if probe_process is None:
pytest.skip("No probe available")
result = bridge.call_tool("connectProbeDefault")
assert isinstance(result, dict), f"connectProbeDefault failed: {result}"
assert result.get("connected"), f"Not connected: {result}"
return bridge
pytest.skip("No probe available (try --no-probe to skip probe-dependent tests)")
deadline = time.monotonic() + 30
last_error = None
while time.monotonic() < deadline:
result = bridge.call_tool("connectProbeDefault")
if isinstance(result, dict) and result.get("connected"):
return bridge
last_error = result
time.sleep(3)
raise AssertionError(f"Failed to connect to probe after 30s: {last_error}")