mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 10:21:13 +00:00
7644b25e8c
Implements F1 pre-flight cache build orchestrator on the operator workstation. Composes C11 TileDownloader (AZ-316), C12 CompanionBringup (AZ-327), C12 FlightsApiClient (AZ-489), and the new RemoteCacheProvisionerInvoker into one sequenced flow guarded by a filelock-backed workstation-side lockfile. Architectural decisions: - Phase-0 flight-resolve runs BEFORE the lockfile (ADR-010): a flight that cannot be resolved is an operator-input error, not a contended- resource error. Enforced by AC-11 + AC-14. - Consumer-side cuts (AZ-507) for C11 + C10 types: local Protocols / mirror DTOs in tile_downloader_cut.py and _types.py; external errors matched by name-based whitelisting so unknown exceptions still propagate per AC-6. Cross-component type translation lives at the composition root (c12_factory). - Failure surfacing: recognised operational failures (download error, companion not ready, build error, flight-resolve error) return as CacheBuildReport(outcome=failure, failure_phase=...). Only lockfile contention raises (BuildLockHeldError) since no phase ever ran. - Workstation-side filelock library (project pin); no custom primitive. - Remote C10 stdout streamed line-by-line as DEBUG with api_key / auth_token redacted before logging (defence-in-depth). - CLI is now a thin adapter; all workflow logic lives in build_cache.py. operator-tool build-cache exit codes map per CacheBuildReport.failure_phase + failure_exception_type. Tests: 116 c12 unit tests pass (29 new for AZ-328 covering 15/15 ACs + NFR-perf-overhead microbench; 7 new for remote_c10_invoker; 3 new for file_lock; test_cli_build_cache rewritten for new orchestrator interface). Full repo suite: 1522 passed, 80 skipped. Also: replays Batch 42's ruff format leftover for c12 flights_api + test_az489 files (formatter ran over the c12 directory after new files were added). Pure whitespace; no behaviour change. Full report: _docs/03_implementation/batch_43_cycle1_report.md Co-authored-by: Cursor <cursoragent@cursor.com>
58 lines
2.2 KiB
Python
58 lines
2.2 KiB
Python
"""AZ-328 — ``FilelockFileLockFactory`` real-filelock smoke tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from gps_denied_onboard.components.c12_operator_tooling import (
|
|
FilelockFileLockFactory,
|
|
LockTimeout,
|
|
)
|
|
|
|
|
|
class TestFilelockFileLockFactory:
|
|
def test_acquire_and_release(self, tmp_path: Path) -> None:
|
|
factory = FilelockFileLockFactory()
|
|
lock_path = tmp_path / ".c12.lock"
|
|
|
|
with factory.try_lock(lock_path, timeout_s=1.0):
|
|
# Re-acquire from the same process with a tight timeout —
|
|
# filelock is reentrant by holder process, so this MAY succeed
|
|
# without raising; what we care about is that the basic
|
|
# acquire/release contract works.
|
|
assert lock_path.exists()
|
|
# Lock file may persist on POSIX (it's the rendezvous file)
|
|
# but it should now be released and re-acquirable.
|
|
with factory.try_lock(lock_path, timeout_s=1.0):
|
|
pass
|
|
|
|
def test_concurrent_lock_raises_lock_timeout(self, tmp_path: Path) -> None:
|
|
# filelock IS process-aware, so two SEPARATE FileLock objects
|
|
# against the same path from the same process WILL contend on
|
|
# POSIX — verify the timeout path raises our LockTimeout.
|
|
from filelock import FileLock as RealFileLock
|
|
|
|
lock_path = tmp_path / ".c12.lock"
|
|
held = RealFileLock(str(lock_path))
|
|
held.acquire(timeout=1.0)
|
|
try:
|
|
factory = FilelockFileLockFactory()
|
|
with pytest.raises(LockTimeout) as exc_info:
|
|
# Tight timeout — the held lock must NOT be released by
|
|
# this assertion path or the test loses meaning.
|
|
with factory.try_lock(lock_path, timeout_s=0.05):
|
|
pass # pragma: no cover
|
|
|
|
assert exc_info.value.path == lock_path
|
|
assert exc_info.value.timeout_s == 0.05
|
|
finally:
|
|
held.release()
|
|
|
|
def test_creates_parent_directory(self, tmp_path: Path) -> None:
|
|
factory = FilelockFileLockFactory()
|
|
nested = tmp_path / "nested" / "deeper" / ".c12.lock"
|
|
with factory.try_lock(nested, timeout_s=1.0):
|
|
assert nested.parent.is_dir()
|