trainUs/tests/conftest.py
David 352a3d5e88
Some checks are pending
CI / Lint & Format (push) Waiting to run
CI / Build (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (chromium) (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (firefox) (push) Waiting to run
Initial commit
2026-06-18 13:06:57 +02:00

157 lines
5.4 KiB
Python

import urllib.request
import pytest
import subprocess
import time
import signal
import os
import fcntl
def _counter_add(delta: int, count_file: str, lock_file: str) -> int:
"""Atomically add `delta` to the worker counter and return the new value."""
with open(lock_file, "w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
try:
try:
with open(count_file) as f:
value = int(f.read().strip() or "0")
except FileNotFoundError:
value = 0
value += delta
with open(count_file, "w") as f:
f.write(str(value))
return value
finally:
fcntl.flock(lock, fcntl.LOCK_UN)
def _counter_read(count_file: str, lock_file: str) -> int:
"""Read the current worker counter value without modifying it."""
with open(lock_file, "w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
try:
try:
with open(count_file) as f:
return int(f.read().strip() or "0")
except FileNotFoundError:
return 0
finally:
fcntl.flock(lock, fcntl.LOCK_UN)
@pytest.fixture(scope="session")
def app_url(tmp_path_factory, worker_id):
"""Start the Vite dev server and return its URL.
When running under pytest-xdist, only the first worker (gw0) starts the
server and writes the URL to a shared temp file. All other workers wait
for that file to appear and then read the URL from it. The server is only
stopped once every worker has finished (tracked via a shared counter file).
When running without xdist (worker_id == "master") the behaviour is
identical to the original single-worker case.
Shared files are placed inside tmp_path_factory.getbasetemp() so that two
concurrent pytest invocations never collide (each invocation gets a unique
basetemp directory from pytest's per-run counter).
"""
basetemp = tmp_path_factory.getbasetemp()
# In xdist each worker gets its own subdirectory; step up to the shared parent
if worker_id != "master":
basetemp = basetemp.parent
server_url_file = str(basetemp / "trainus_server_url")
worker_count_file = str(basetemp / "trainus_worker_count")
worker_count_lock = str(basetemp / "trainus_worker_count.lock")
url = "http://localhost:5199"
is_controller = worker_id in ("master", "gw0")
# Register this worker in the shared counter
_counter_add(1, worker_count_file, worker_count_lock)
server_error_file = str(basetemp / "trainus_server_error")
if is_controller:
# Remove stale files from a previous run if present
for stale in (server_url_file, server_error_file):
try:
os.remove(stale)
except FileNotFoundError:
pass
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
process = subprocess.Popen(
["npm", "run", "dev", "--", "--port", "5199", "--strictPort"],
cwd=project_root,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
preexec_fn=os.setsid,
)
# Wait for the server to be ready (up to 60 s)
for _ in range(120):
try:
urllib.request.urlopen(url, timeout=1)
break
except Exception:
time.sleep(0.5)
else:
process.kill()
# Signal secondary workers to stop waiting
with open(server_error_file, "w") as f:
f.write("Vite dev server failed to start within 60s")
raise RuntimeError("Vite dev server failed to start within 60s")
# Publish the URL so other workers can pick it up
with open(server_url_file, "w") as f:
f.write(url)
yield url
# Mark the controller itself as done, then wait for all other workers
# to finish before killing the server. We decrement exactly once here
# and then only *read* the counter so we never accidentally modify it
# inside the polling loop.
_counter_add(-1, worker_count_file, worker_count_lock)
for _ in range(240):
if _counter_read(worker_count_file, worker_count_lock) <= 0:
break
time.sleep(0.5)
# Cleanup
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
else:
# Secondary workers: wait until gw0 has written the URL file (up to 90 s)
for _ in range(180):
if os.path.exists(server_url_file):
break
if os.path.exists(server_error_file):
with open(server_error_file) as f:
msg = f.read().strip()
raise RuntimeError(f"gw0 failed to start the Vite dev server: {msg}")
time.sleep(0.5)
else:
raise RuntimeError("Timed out waiting for Vite dev server URL from gw0")
with open(server_url_file) as f:
server_url = f.read().strip()
yield server_url
# Signal this worker is done
_counter_add(-1, worker_count_file, worker_count_lock)
@pytest.fixture(scope="session")
def browser_type_launch_args():
"""Configure browser launch args."""
return {}
@pytest.fixture(scope="session")
def browser_context_args():
"""Configure browser context."""
return {
"viewport": {"width": 1280, "height": 720},
}