ci: harden contributor checks (#254)

* ci: harden contributor checks

* ci: pin setup-uv action release

* ci: split workflow checks

* docs: clarify required checks
This commit is contained in:
Elliot Chen
2026-06-06 10:47:16 +08:00
committed by GitHub
parent 3527ea3eb2
commit 873e7535fb
12 changed files with 529 additions and 225 deletions

View File

@ -2,7 +2,8 @@
from __future__ import annotations
import multiprocessing
import subprocess
import sys
import time
from pathlib import Path
@ -11,6 +12,88 @@ import pytest
from everos.core.persistence import LockError, MemoryRoot, memory_root_lock
_LOCK_HOLDER_SCRIPT = """
import fcntl
import os
import sys
import time
from pathlib import Path
lock_path, ready_path, release_path = sys.argv[1:]
Path(lock_path).parent.mkdir(parents=True, exist_ok=True)
fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o644)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
Path(ready_path).write_text("ready")
while not Path(release_path).exists():
time.sleep(0.05)
finally:
try:
fcntl.flock(fd, fcntl.LOCK_UN)
finally:
os.close(fd)
"""
LOCK_HOLDER_READY_TIMEOUT = 5.0
async def _assert_subprocess_ready(
ready_path: Path,
proc: subprocess.Popen[str],
) -> None:
deadline = time.monotonic() + LOCK_HOLDER_READY_TIMEOUT
while time.monotonic() < deadline:
if await anyio.to_thread.run_sync(ready_path.exists):
return
if proc.poll() is not None:
stdout, stderr = proc.communicate()
raise AssertionError(
"subprocess exited before acquiring lock "
f"(exitcode={proc.returncode}, stdout={stdout!r}, stderr={stderr!r})"
)
await anyio.sleep(0.05)
proc.terminate()
stdout, stderr = proc.communicate(timeout=1)
raise AssertionError(
"subprocess failed to acquire lock "
f"(exitcode={proc.returncode}, stdout={stdout!r}, stderr={stderr!r})"
)
def _spawn_lock_holder(mr: MemoryRoot) -> tuple[subprocess.Popen[str], Path, Path]:
ready_path = mr.root / ".test-lock-ready"
release_path = mr.root / ".test-lock-release"
proc = subprocess.Popen(
[
sys.executable,
"-c",
_LOCK_HOLDER_SCRIPT,
str(mr.lock_file),
str(ready_path),
str(release_path),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
return proc, ready_path, release_path
async def _start_lock_holder(mr: MemoryRoot) -> tuple[subprocess.Popen[str], Path]:
proc, ready_path, release_path = _spawn_lock_holder(mr)
await _assert_subprocess_ready(ready_path, proc)
return proc, release_path
def _stop_lock_holder(proc: subprocess.Popen[str], release_path: Path) -> None:
release_path.write_text("release")
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.terminate()
proc.wait(timeout=5)
async def test_lock_creates_anchor_file(tmp_path: Path) -> None:
mr = MemoryRoot(tmp_path)
@ -27,60 +110,30 @@ async def test_lock_acquire_release_acquire(tmp_path: Path) -> None:
pass
def _hold_lock(memory_root_path: str, ready: object, release: object) -> None:
"""Subprocess helper: acquire blocking lock, signal, wait, release.
The subprocess runs its own event loop via :func:`anyio.run` since
:func:`memory_root_lock` is now async.
"""
async def _run() -> None:
mr = MemoryRoot(memory_root_path)
async with memory_root_lock(mr, blocking=True):
ready.set()
# Use a thread-offloaded wait so we don't block the event loop.
await anyio.to_thread.run_sync(release.wait, 5)
anyio.run(_run)
async def test_nonblocking_raises_when_held_by_other_process(tmp_path: Path) -> None:
"""Different process holding the lock → blocking=False raises LockError."""
mr = MemoryRoot(tmp_path)
ctx = multiprocessing.get_context("spawn")
ready = ctx.Event()
release = ctx.Event()
proc = ctx.Process(target=_hold_lock, args=(str(mr.root), ready, release))
proc.start()
proc, release_path = await _start_lock_holder(mr)
try:
assert ready.wait(timeout=5), "subprocess failed to acquire lock"
with pytest.raises(LockError):
async with memory_root_lock(mr, blocking=False):
pass
finally:
release.set()
proc.join(timeout=5)
if proc.is_alive():
proc.terminate()
_stop_lock_holder(proc, release_path)
async def test_blocking_waits_for_release(tmp_path: Path) -> None:
"""Different process holding lock + main process blocking=True waits."""
mr = MemoryRoot(tmp_path)
ctx = multiprocessing.get_context("spawn")
ready = ctx.Event()
release = ctx.Event()
proc = ctx.Process(target=_hold_lock, args=(str(mr.root), ready, release))
proc.start()
proc, release_path = await _start_lock_holder(mr)
try:
assert ready.wait(timeout=5)
# Schedule the subprocess to release shortly; main process should
# acquire the lock after that.
release_started = time.monotonic()
def release_after_short_delay() -> None:
time.sleep(0.2)
release.set()
release_path.write_text("release")
import threading
@ -90,7 +143,4 @@ async def test_blocking_waits_for_release(tmp_path: Path) -> None:
# Should have waited at least roughly the delay.
assert elapsed >= 0.1
finally:
release.set()
proc.join(timeout=5)
if proc.is_alive():
proc.terminate()
_stop_lock_holder(proc, release_path)