All checks were successful
continuous-integration/drone/push Build is passing
tests/concurrency/ — NOT in the default `pytest tests/unit` gate; run explicitly with `pytest tests/concurrency -q`. flock/prctl/alarm are never mocked: helper subprocesses (helpers.py) hold real locks and install the real lifetime guards; locks live in a per-test tmp dir via CCCI_APP_LOCK_DIR; every helper (and recorded grandchild) is reaped by fixture cleanup. - test_locks.py (cases 1-4): SIGKILL auto-release; LOCK_NB held/unheld semantics; PEP 446 fd-not-inherited (holder's child survives, lock still releases); same-domain second acquire blocks until first holder exits. - test_janitor.py (cases 5-12): orphan reaped once + lockfile unlinked; live holder never reaped + logged; new-run acquire blocks until a slow reap completes (reap-under-probe-lock); two overlapping janitors -> exactly one reaps (flock arbitration); reboot sim (no lockfile) reaps immediately with no age wait; >120min-held lock flagged 'possible leaked run' and NOT stolen; warm/canonical names never probed (no lockfile even created); directory-as-lockfile and missing lock dir degrade to skip+log, never crash. - test_lifetime.py (cases 13-16): PDEATHSIG (wrapper parent SIGKILL'd -> guarded child TERM'd, teardown marker, lock released); already-orphaned helper REFUSES to run (ppid race); 2s deadline alarm -> teardown + exit 142 + lock released; SIGTERM -> teardown + exit 143 + lock released. - test_abra_dir.py (cases 17-19 + 18b): per-run dir built + $ABRA_DIR exported before the first abra call (recording stub abra on PATH); two CONCURRENT same-recipe fetch+checkout flows into different ABRA_DIRs -> divergent correct trees, canonical staged clone untouched; .env written through the servers/ symlink lands in the canonical path (env_get/env_set agree); manual runs get pid-suffixed dirs. On cc-ci: pytest tests/concurrency -q -> 20 passed; tests/unit -> 138 passed; lint PASS.
121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Subprocess helpers for tests/concurrency — REAL kernel locks and the REAL lifetime guards in
|
|
separate processes (flock/prctl are never mocked; tests assert on actual kernel behavior).
|
|
|
|
Invoked as: python3 helpers.py <command> <args...>
|
|
|
|
Env contract (set by the spawning test):
|
|
CCCI_APP_LOCK_DIR sandbox lock dir (never /run/lock in tests)
|
|
CCCI_HELPER_OUT marker file this helper APPENDS progress lines to (ACQUIRED/READY/...)
|
|
|
|
Commands:
|
|
hold <domain> acquire the app lock, mark `ACQUIRED <ts>`, sleep forever
|
|
hold-with-child <domain> acquire the lock, spawn a plain sleeping subprocess child, mark
|
|
`ACQUIRED <ts>` + `CHILD <pid>` (PEP 446: the child must NOT
|
|
inherit the lock fd), sleep forever
|
|
guarded <domain> <deadline> install the REAL lifetime guards (alarm=<deadline>s), acquire the
|
|
lock, mark `READY`; when the teardown funnel runs (`finally:`),
|
|
mark `TEARDOWN` before exiting
|
|
wrapper <domain> spawn `guarded <domain> 3600` as MY child, mark `WRAPPED <pid>`,
|
|
sleep — the test kills me to prove PDEATHSIG TERMs the child
|
|
orphan-probe wait (bounded) until reparented (ppid==1), then install the
|
|
guards; mark `REFUSED` if they exit (expected) or `GUARDS_OK`
|
|
fetch-checkout <recipe> <ref> run run_recipe_ci.fetch_recipe (the test sets CCCI_SKIP_FETCH=1
|
|
+ a per-"run" ABRA_DIR), git-checkout <ref>, mark
|
|
`RESULT <head> <data.txt content>`
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "runner"))
|
|
from harness import abra, lifecycle, lifetime # noqa: E402
|
|
|
|
OUT = os.environ.get("CCCI_HELPER_OUT")
|
|
|
|
|
|
def mark(line: str) -> None:
|
|
if OUT:
|
|
with open(OUT, "a") as f:
|
|
f.write(line + "\n")
|
|
f.flush()
|
|
print(line, flush=True)
|
|
|
|
|
|
def cmd_hold(domain: str) -> None:
|
|
lifecycle.acquire_app_lock(domain)
|
|
mark(f"ACQUIRED {time.time()}")
|
|
time.sleep(3600)
|
|
|
|
|
|
def cmd_hold_with_child(domain: str) -> None:
|
|
lifecycle.acquire_app_lock(domain)
|
|
child = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(3600)"])
|
|
mark(f"ACQUIRED {time.time()}")
|
|
mark(f"CHILD {child.pid}")
|
|
time.sleep(3600)
|
|
|
|
|
|
def cmd_guarded(domain: str, deadline: str) -> None:
|
|
lifetime.install_lifetime_guards(deadline_seconds=int(deadline))
|
|
lifecycle.acquire_app_lock(domain)
|
|
mark("READY")
|
|
try:
|
|
time.sleep(3600)
|
|
finally:
|
|
mark("TEARDOWN")
|
|
|
|
|
|
def cmd_wrapper(domain: str) -> None:
|
|
p = subprocess.Popen( # noqa: S603
|
|
[sys.executable, os.path.abspath(__file__), "guarded", domain, "3600"],
|
|
env=os.environ.copy(),
|
|
)
|
|
mark(f"WRAPPED {p.pid}")
|
|
time.sleep(3600)
|
|
|
|
|
|
def cmd_orphan_probe() -> None:
|
|
# Our spawner exits immediately after fork; wait (bounded) until we are reparented so the
|
|
# prctl is installed with the parent ALREADY dead — the exact race the ppid check closes.
|
|
for _ in range(200):
|
|
if os.getppid() == 1:
|
|
break
|
|
time.sleep(0.05)
|
|
else:
|
|
mark("NEVER_REPARENTED") # e.g. a subreaper environment — test will fail visibly
|
|
return
|
|
try:
|
|
lifetime.install_lifetime_guards()
|
|
except SystemExit:
|
|
mark("REFUSED")
|
|
raise
|
|
mark("GUARDS_OK")
|
|
|
|
|
|
def cmd_fetch_checkout(recipe: str, ref: str) -> None:
|
|
import run_recipe_ci
|
|
|
|
run_recipe_ci.fetch_recipe(recipe, None, None)
|
|
abra.recipe_checkout(recipe, ref)
|
|
head = abra.recipe_head_commit(recipe)
|
|
with open(os.path.join(abra.recipe_dir(recipe), "data.txt")) as f:
|
|
content = f.read().strip()
|
|
mark(f"RESULT {head} {content}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cmd, *args = sys.argv[1:]
|
|
{
|
|
"hold": cmd_hold,
|
|
"hold-with-child": cmd_hold_with_child,
|
|
"guarded": cmd_guarded,
|
|
"wrapper": cmd_wrapper,
|
|
"orphan-probe": cmd_orphan_probe,
|
|
"fetch-checkout": cmd_fetch_checkout,
|
|
}[cmd](*args)
|