Files
gps-denied-onboard/tests/unit/c12_operator_tooling/test_file_lock.py
T
Oleksandr Bezdieniezhnykh 7644b25e8c [AZ-328] C12 BuildCacheOrchestrator + remote C10 invoker (Batch 43)
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>
2026-05-13 11:03:46 +03:00

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()