mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 21:31:13 +00:00
5dfd9a577e
Closes cumulative review 46-48 F1 (Medium) + F3 (Low). Adds iso_ts_from_clock(clock) alongside iso_ts_now() in the Layer-1 helper; migrates four duplicate definitions in c2_vpr (net_vlad, ultra_vpr, _faiss_bridge) and c12_operator_orchestrator (operator_reloc_service). Output format flipped +00:00 -> Z to align with iso_ts_now() and the canonical FDR _TS fixture (FDR schema test passes unmodified). 18 helper AC tests + 186 sibling tests pass; ruff clean. Co-authored-by: Cursor <cursoragent@cursor.com>
390 lines
12 KiB
Python
390 lines
12 KiB
Python
"""AC tests for AZ-508 + AZ-526: ISO-timestamp helper consolidation.
|
|
|
|
Verifies the `iso_timestamps` helper module exposed at
|
|
`gps_denied_onboard.helpers.iso_timestamps` — the single Layer-1 source
|
|
for FDR record envelope timestamps. Two surfaces:
|
|
|
|
- ``iso_ts_now()`` (AZ-508) — wall-clock variant, microsecond precision.
|
|
- ``iso_ts_from_clock(clock)`` (AZ-526) — Clock-injected variant,
|
|
nanosecond precision.
|
|
|
|
Both produce RFC 3339 UTC with the canonical ``Z`` suffix, matching the
|
|
FDR ``_TS`` fixture in ``tests/unit/test_az272_fdr_record_schema.py``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import re
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from gps_denied_onboard.helpers import iso_ts_from_clock, iso_ts_now
|
|
from gps_denied_onboard.helpers.iso_timestamps import (
|
|
iso_ts_from_clock as iso_ts_from_clock_direct,
|
|
)
|
|
from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now as iso_ts_now_direct
|
|
|
|
_TS_REGEX: re.Pattern[str] = re.compile(
|
|
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$"
|
|
)
|
|
_TS_NS_REGEX: re.Pattern[str] = re.compile(
|
|
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{9}Z$"
|
|
)
|
|
|
|
_REPO_ROOT: Path = Path(__file__).resolve().parents[2]
|
|
_HELPER_PATH: Path = (
|
|
_REPO_ROOT / "src" / "gps_denied_onboard" / "helpers" / "iso_timestamps.py"
|
|
)
|
|
_C6_DIR: Path = (
|
|
_REPO_ROOT / "src" / "gps_denied_onboard" / "components" / "c6_tile_cache"
|
|
)
|
|
_C7_DIR: Path = (
|
|
_REPO_ROOT / "src" / "gps_denied_onboard" / "components" / "c7_inference"
|
|
)
|
|
_C2_DIR: Path = (
|
|
_REPO_ROOT / "src" / "gps_denied_onboard" / "components" / "c2_vpr"
|
|
)
|
|
_C12_DIR: Path = (
|
|
_REPO_ROOT / "src"
|
|
/ "gps_denied_onboard"
|
|
/ "components"
|
|
/ "c12_operator_orchestrator"
|
|
)
|
|
|
|
|
|
class _StubClock:
|
|
"""Minimal :class:`Clock`-shaped stub for AC-3 nanosecond verification."""
|
|
|
|
def __init__(self, ns: int) -> None:
|
|
self._ns = ns
|
|
|
|
def time_ns(self) -> int:
|
|
return self._ns
|
|
|
|
|
|
def test_ac1_import_and_call_returns_str() -> None:
|
|
# Act
|
|
value = iso_ts_now()
|
|
# Assert
|
|
assert isinstance(value, str)
|
|
assert value, "iso_ts_now() returned an empty string"
|
|
# Both the package-level and module-level imports must resolve to the
|
|
# same callable so consumers can reach it either way.
|
|
assert iso_ts_now is iso_ts_now_direct
|
|
|
|
|
|
def test_ac2_format_matches_canonical_regex() -> None:
|
|
# Act
|
|
value = iso_ts_now()
|
|
# Assert
|
|
assert _TS_REGEX.fullmatch(value), (
|
|
f"{value!r} does not match the canonical FDR ts format "
|
|
f"YYYY-MM-DDTHH:MM:SS.ffffffZ"
|
|
)
|
|
|
|
|
|
def test_ac2_fromisoformat_roundtrip_yields_utc_aware_datetime() -> None:
|
|
# Arrange
|
|
value = iso_ts_now()
|
|
iso_with_offset = value.replace("Z", "+00:00")
|
|
|
|
# Act
|
|
parsed = datetime.fromisoformat(iso_with_offset)
|
|
|
|
# Assert
|
|
assert parsed.tzinfo is not None
|
|
assert parsed.utcoffset() == timezone.utc.utcoffset(parsed)
|
|
|
|
|
|
def test_ac3_two_successive_calls_are_non_decreasing() -> None:
|
|
# Arrange / Act
|
|
a = iso_ts_now()
|
|
time.sleep(0.000_005)
|
|
b = iso_ts_now()
|
|
|
|
# Assert (lexicographic comparison is correct for the fixed-width format)
|
|
assert b >= a, f"expected {b!r} >= {a!r}"
|
|
|
|
|
|
def test_ac4_no_other_iso_ts_now_definition_exists_in_src() -> None:
|
|
"""AC-4 (AZ-508): a `def _iso_ts_now` / `def iso_ts_now` MUST exist
|
|
only inside `helpers/iso_timestamps.py`. Any other definition under
|
|
`src/` means a consumer slipped a copy back in.
|
|
"""
|
|
# Arrange
|
|
src_root = _REPO_ROOT / "src"
|
|
offenders: list[tuple[Path, str]] = []
|
|
|
|
# Act
|
|
for path in src_root.rglob("*.py"):
|
|
if path == _HELPER_PATH:
|
|
continue
|
|
try:
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
except SyntaxError:
|
|
continue
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.FunctionDef) and node.name in {
|
|
"iso_ts_now",
|
|
"_iso_ts_now",
|
|
}:
|
|
offenders.append((path.relative_to(_REPO_ROOT), node.name))
|
|
|
|
# Assert
|
|
assert offenders == [], (
|
|
f"Found stray `iso_ts_now` definitions outside the helper: {offenders}"
|
|
)
|
|
|
|
|
|
def test_ac4_az526_no_module_level_iso_ts_from_clock_outside_helper() -> None:
|
|
"""AC-4 (AZ-526): a module-level `def iso_ts_from_clock` /
|
|
`def _iso_ts_from_clock` MUST exist only inside
|
|
`helpers/iso_timestamps.py`. The two instance-method delegations
|
|
(`_faiss_bridge.FaissBridge._iso_ts_from_clock`,
|
|
`operator_reloc_service.OperatorReLocService._iso_ts_from_clock`)
|
|
live inside a `ClassDef`, so we only scan top-level function defs.
|
|
"""
|
|
# Arrange
|
|
src_root = _REPO_ROOT / "src"
|
|
offenders: list[tuple[Path, str]] = []
|
|
|
|
# Act
|
|
for path in src_root.rglob("*.py"):
|
|
if path == _HELPER_PATH:
|
|
continue
|
|
try:
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
except SyntaxError:
|
|
continue
|
|
for node in tree.body:
|
|
if isinstance(node, ast.FunctionDef) and node.name in {
|
|
"iso_ts_from_clock",
|
|
"_iso_ts_from_clock",
|
|
}:
|
|
offenders.append((path.relative_to(_REPO_ROOT), node.name))
|
|
|
|
# Assert
|
|
assert offenders == [], (
|
|
"Found stray module-level `iso_ts_from_clock` definitions outside "
|
|
f"the helper: {offenders}"
|
|
)
|
|
|
|
|
|
def test_ac4_c6_and_c7_callers_import_from_helpers() -> None:
|
|
"""The 3 migrated call-sites must import from `helpers.iso_timestamps`
|
|
(directly or via the `helpers` package facade) so future hygiene
|
|
cycles can rely on the single source of truth.
|
|
"""
|
|
# Arrange
|
|
callers = [
|
|
_C6_DIR / "cache_budget_enforcer.py",
|
|
_C6_DIR / "postgres_filesystem_store.py",
|
|
_C6_DIR / "freshness_gate.py",
|
|
_C7_DIR / "onnx_trt_ep_runtime.py",
|
|
_C7_DIR / "thermal_publisher.py",
|
|
]
|
|
expected_token = "gps_denied_onboard.helpers.iso_timestamps"
|
|
|
|
# Act / Assert
|
|
for caller in callers:
|
|
text = caller.read_text(encoding="utf-8")
|
|
assert expected_token in text, (
|
|
f"{caller.relative_to(_REPO_ROOT)} does not import "
|
|
f"`iso_ts_now` from `{expected_token}`"
|
|
)
|
|
assert "def _iso_ts_now" not in text, (
|
|
f"{caller.relative_to(_REPO_ROOT)} still defines a local "
|
|
"_iso_ts_now (consolidation incomplete)"
|
|
)
|
|
|
|
|
|
def test_ac6_helper_uses_stdlib_or_layer1_clock_only() -> None:
|
|
"""AC-6: the helper module's imports MUST be stdlib OR the Layer-1
|
|
`gps_denied_onboard.clock` package (needed by `iso_ts_from_clock`
|
|
for its `Clock` type annotation, imported under `TYPE_CHECKING`).
|
|
"""
|
|
# Arrange
|
|
tree = ast.parse(_HELPER_PATH.read_text(encoding="utf-8"))
|
|
allowed_stdlib = {"datetime", "__future__", "typing"}
|
|
allowed_layer1 = {"gps_denied_onboard.clock"}
|
|
|
|
# Act
|
|
imports: list[str] = []
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
imports.extend(alias.name for alias in node.names)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
if node.module is not None:
|
|
imports.append(node.module)
|
|
|
|
# Assert
|
|
for name in imports:
|
|
top = name.split(".")[0]
|
|
assert top in allowed_stdlib or name in allowed_layer1, (
|
|
f"helpers/iso_timestamps.py imports `{name}`; only stdlib "
|
|
f"({sorted(allowed_stdlib)}) or {sorted(allowed_layer1)} is "
|
|
"allowed by AC-6"
|
|
)
|
|
|
|
|
|
def test_helper_is_layer_1_no_component_imports() -> None:
|
|
"""Layer-1 discipline: the helper MUST NOT import from any component.
|
|
(Constraint § Layer-1 discipline in the AZ-508 / AZ-526 task specs.)
|
|
"""
|
|
# Arrange
|
|
text = _HELPER_PATH.read_text(encoding="utf-8")
|
|
|
|
# Assert
|
|
assert "gps_denied_onboard.components" not in text, (
|
|
"helpers/iso_timestamps.py imports from a component — Layer-1 "
|
|
"discipline violated"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"expected_field", ["iso_ts_now", "iso_ts_from_clock"]
|
|
)
|
|
def test_helper_public_surface_is_minimal(expected_field: str) -> None:
|
|
"""Defensive: only the two consolidated helpers are re-exported."""
|
|
# Arrange
|
|
import gps_denied_onboard.helpers.iso_timestamps as module
|
|
|
|
# Assert
|
|
assert expected_field in module.__all__
|
|
assert module.__all__ == ["iso_ts_from_clock", "iso_ts_now"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AZ-526 — `iso_ts_from_clock(clock)` AC tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_az526_ac1_import_and_call_returns_str() -> None:
|
|
# Arrange
|
|
clock = _StubClock(ns=1_700_000_000_000_000_000)
|
|
|
|
# Act
|
|
value = iso_ts_from_clock(clock)
|
|
|
|
# Assert
|
|
assert isinstance(value, str)
|
|
assert value, "iso_ts_from_clock() returned an empty string"
|
|
assert iso_ts_from_clock is iso_ts_from_clock_direct
|
|
|
|
|
|
def test_az526_ac2_format_matches_canonical_ns_regex() -> None:
|
|
# Arrange
|
|
clock = _StubClock(ns=1_734_567_890_123_456_789)
|
|
|
|
# Act
|
|
value = iso_ts_from_clock(clock)
|
|
|
|
# Assert
|
|
assert _TS_NS_REGEX.fullmatch(value), (
|
|
f"{value!r} does not match the canonical clock-driven format "
|
|
f"YYYY-MM-DDTHH:MM:SS.fffffffffZ"
|
|
)
|
|
|
|
|
|
def test_az526_ac2_fromisoformat_truncated_roundtrip_yields_utc() -> None:
|
|
# Arrange
|
|
clock = _StubClock(ns=1_700_000_000_000_000_000)
|
|
value = iso_ts_from_clock(clock)
|
|
# stdlib fromisoformat (3.11+) accepts up to 6 fractional digits;
|
|
# truncate the trailing 3 ns digits and the Z, then append `+00:00`.
|
|
iso_with_offset = value[:26] + "+00:00"
|
|
|
|
# Act
|
|
parsed = datetime.fromisoformat(iso_with_offset)
|
|
|
|
# Assert
|
|
assert parsed.tzinfo is not None
|
|
assert parsed.utcoffset() == timezone.utc.utcoffset(parsed)
|
|
|
|
|
|
def test_az526_ac3_nanosecond_fraction_preserved_verbatim() -> None:
|
|
"""The AC-3 fixture from the AZ-526 task spec."""
|
|
# Arrange
|
|
clock = _StubClock(ns=1_234_567_890_123_456_789)
|
|
|
|
# Act
|
|
value = iso_ts_from_clock(clock)
|
|
|
|
# Assert
|
|
assert value.endswith(".123456789Z"), (
|
|
f"expected ns suffix `.123456789Z`, got {value!r}"
|
|
)
|
|
expected_prefix = (
|
|
datetime.fromtimestamp(1_234_567_890, tz=timezone.utc)
|
|
.strftime("%Y-%m-%dT%H:%M:%S")
|
|
)
|
|
assert value == f"{expected_prefix}.123456789Z"
|
|
|
|
|
|
def test_az526_ac3_sub_second_distinguishability() -> None:
|
|
"""Two clock instances 1 ns apart MUST yield distinct outputs."""
|
|
# Arrange
|
|
base_ns = 1_700_000_000_000_000_000
|
|
|
|
# Act
|
|
a = iso_ts_from_clock(_StubClock(ns=base_ns))
|
|
b = iso_ts_from_clock(_StubClock(ns=base_ns + 1))
|
|
|
|
# Assert
|
|
assert a != b, "nanosecond precision lost (1-ns advance produced same output)"
|
|
assert b > a, "lexicographic ordering broken at the ns boundary"
|
|
|
|
|
|
def test_az526_ac4_all_four_call_sites_import_from_helper() -> None:
|
|
"""AC-4: the four call sites import `iso_ts_from_clock` from the
|
|
helper module. Module-level callers import as `_iso_ts_from_clock`
|
|
(preserving their local symbol); instance-method callers import the
|
|
canonical name and delegate.
|
|
"""
|
|
# Arrange
|
|
callers = [
|
|
_C2_DIR / "net_vlad.py",
|
|
_C2_DIR / "ultra_vpr.py",
|
|
_C2_DIR / "_faiss_bridge.py",
|
|
_C12_DIR / "operator_reloc_service.py",
|
|
]
|
|
expected_token = (
|
|
"from gps_denied_onboard.helpers.iso_timestamps import "
|
|
)
|
|
|
|
# Act / Assert
|
|
for caller in callers:
|
|
text = caller.read_text(encoding="utf-8")
|
|
assert expected_token in text and "iso_ts_from_clock" in text, (
|
|
f"{caller.relative_to(_REPO_ROOT)} does not import "
|
|
f"`iso_ts_from_clock` from the helper"
|
|
)
|
|
|
|
|
|
def test_az526_ac4_no_module_level_local_iso_ts_from_clock_body_in_callers() -> None:
|
|
"""AC-4 follow-on: the four migrated callers must not contain the
|
|
legacy multi-line body (`divmod(ns, 1_000_000_000)` + `+00:00`
|
|
suffix). Catches a future contributor who deletes the import but
|
|
forgets the helper exists.
|
|
"""
|
|
# Arrange
|
|
callers = [
|
|
_C2_DIR / "net_vlad.py",
|
|
_C2_DIR / "ultra_vpr.py",
|
|
_C2_DIR / "_faiss_bridge.py",
|
|
_C12_DIR / "operator_reloc_service.py",
|
|
]
|
|
forbidden_format_marker = "+00:00\""
|
|
|
|
# Act / Assert
|
|
for caller in callers:
|
|
text = caller.read_text(encoding="utf-8")
|
|
assert forbidden_format_marker not in text, (
|
|
f"{caller.relative_to(_REPO_ROOT)} still emits a `+00:00`-suffix "
|
|
"timestamp literal — the legacy ts body wasn't fully migrated"
|
|
)
|