Files
geointel/backend/tests/test_sprint13_yolo_preflight.py
T
Codex 3f1a686bef
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled
Harden YOLO compatibility preflight
2026-06-17 01:59:00 +02:00

189 lines
6.2 KiB
Python

from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
from app.core.config import Settings
from app.services.yolo_preflight_service import YoloPreflightService
ROOT = Path(__file__).resolve().parents[2]
class AvailableAdapter:
@staticmethod
def dependencies_available() -> bool:
return True
def __init__(self, settings: Settings) -> None:
self.settings = settings
def load_model(self, model_path: Path) -> object:
return {"model_path": str(model_path)}
class MissingDependencyAdapter:
@staticmethod
def dependencies_available() -> bool:
return False
class FailingLoadAdapter(AvailableAdapter):
def load_model(self, model_path: Path) -> object:
raise RuntimeError(f"cannot load {model_path}")
def _manifest(tmp_path: Path, tile_count: int = 1) -> Path:
tiles = []
for index in range(tile_count):
tile_path = tmp_path / f"tile_{index:04d}.tif"
tile_path.write_bytes(b"tile")
tiles.append(
{
"path": str(tile_path),
"pixel_window": [0, 0, 100, 100],
"bounds": [4.0, 51.0, 5.0, 52.0],
"transform": [4.0, 0.01, 0.0, 52.0, 0.0, -0.01],
"index": index,
}
)
manifest_path = tmp_path / "manifest.json"
manifest_path.write_text(json.dumps({"tiles": tiles, "count": tile_count}), encoding="utf-8")
return manifest_path
def test_yolo_preflight_reports_disabled_without_loading_model(tmp_path: Path) -> None:
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=False, yolo_model_path=str(tmp_path / "missing.pt")),
tile_manifest_path=str(tmp_path / "missing-manifest.json"),
yolo_adapter_class=AvailableAdapter,
)
assert result["status"] == "not_configured"
assert result["checks"]["enabled"] is False
assert result["checks"]["dependencies_available"] is None
assert result["checks"]["model_file_exists"] is None
assert result["checks"]["manifest_valid"] is None
def test_yolo_preflight_distinguishes_missing_dependencies_from_missing_model(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path)),
tile_manifest_path=str(_manifest(tmp_path)),
yolo_adapter_class=MissingDependencyAdapter,
)
assert result["status"] == "dependency_unavailable"
assert result["checks"]["dependencies_available"] is False
assert result["checks"]["model_file_exists"] is None
assert result["checks"]["manifest_valid"] is None
def test_yolo_preflight_validates_model_and_manifest_without_importing_yolo(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
manifest_path = _manifest(tmp_path, tile_count=2)
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4),
tile_manifest_path=str(manifest_path),
yolo_adapter_class=AvailableAdapter,
)
assert result["status"] == "ready"
assert result["checks"]["dependencies_available"] is True
assert result["checks"]["model_file_exists"] is True
assert result["checks"]["manifest_valid"] is True
assert result["tile_count"] == 2
assert result["will_download_models"] is False
assert result["will_run_inference"] is False
def test_yolo_preflight_can_explicitly_smoke_load_local_model(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
manifest_path = _manifest(tmp_path)
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4),
tile_manifest_path=str(manifest_path),
yolo_adapter_class=AvailableAdapter,
check_model_load=True,
)
assert result["status"] == "ready"
assert result["checks"]["model_load_requested"] is True
assert result["checks"]["model_load_ok"] is True
assert result["will_download_models"] is False
assert result["will_run_inference"] is False
def test_yolo_preflight_reports_explicit_model_load_failure(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4),
tile_manifest_path=str(_manifest(tmp_path)),
yolo_adapter_class=FailingLoadAdapter,
check_model_load=True,
)
assert result["status"] == "model_load_failed"
assert result["error_code"] == "DETECTION_MODEL_LOAD_FAILED"
assert result["checks"]["model_load_ok"] is False
def test_yolo_preflight_script_outputs_json(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
manifest_path = _manifest(tmp_path)
result = subprocess.run(
[
sys.executable,
str(ROOT / "scripts" / "yolo_preflight.py"),
"--model-path",
str(model_path),
"--tile-manifest-path",
str(manifest_path),
"--assume-dependencies",
"--json",
],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
payload = json.loads(result.stdout)
assert payload["status"] == "ready"
assert payload["model_path"] == str(model_path)
assert payload["tile_manifest_path"] == str(manifest_path)
def test_yolo_preflight_script_rejects_assumed_dependencies_for_model_load(tmp_path: Path) -> None:
result = subprocess.run(
[
sys.executable,
str(ROOT / "scripts" / "yolo_preflight.py"),
"--model-path",
str(tmp_path / "model.pt"),
"--assume-dependencies",
"--check-model-load",
"--json",
],
cwd=ROOT,
check=False,
capture_output=True,
text=True,
)
assert result.returncode != 0
assert "--check-model-load cannot be combined with --assume-dependencies" in result.stderr