8d05c67a48
Two-cycle stale→recovery tests confirm that a non-empty fetch after timeout or empty-list correctly resets is_stale=False; no changes to main.py or aircraft.py required as recovery logic was already correct from story 3-1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import pathlib
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
import requests
|
|
from PIL import Image
|
|
|
|
from planemapper.display import NullDisplay
|
|
from planemapper.main import _run_one_cycle
|
|
from planemapper.models import Aircraft
|
|
from planemapper.renderer.projection import MapBounds
|
|
from planemapper.renderer.renderer import Renderer
|
|
|
|
FIXTURE_DIR = pathlib.Path(__file__).parent / "fixtures"
|
|
|
|
|
|
@pytest.fixture
|
|
def renderer(monkeypatch: pytest.MonkeyPatch) -> Renderer:
|
|
monkeypatch.setattr(
|
|
"planemapper.renderer.airspace.AIRSPACE_PATH",
|
|
FIXTURE_DIR / "airspace_sample.geojson",
|
|
)
|
|
base_map = Image.new("RGB", (800, 480), color=(255, 255, 255))
|
|
bounds = MapBounds(home_lat=53.0, home_lon=-6.0, radius_nm=100.0)
|
|
return Renderer(base_map, bounds)
|
|
|
|
|
|
def _make_aircraft(icao: str = "ABC123") -> Aircraft:
|
|
return Aircraft(icao=icao, lat=53.1, lon=-6.1, altitude_ft=10000, callsign="TST1")
|
|
|
|
|
|
def test_timeout_returns_stale_last_aircraft(renderer: Renderer) -> None:
|
|
display = NullDisplay()
|
|
last = [_make_aircraft()]
|
|
mock_fetcher = MagicMock()
|
|
mock_fetcher.fetch.side_effect = requests.Timeout
|
|
result = _run_one_cycle(renderer, mock_fetcher, display, last)
|
|
assert len(result) == 1
|
|
assert result[0].is_stale is True
|
|
|
|
|
|
def test_empty_fetch_returns_stale_when_had_previous(renderer: Renderer) -> None:
|
|
display = NullDisplay()
|
|
last = [_make_aircraft()]
|
|
mock_fetcher = MagicMock()
|
|
mock_fetcher.fetch.return_value = []
|
|
result = _run_one_cycle(renderer, mock_fetcher, display, last)
|
|
assert len(result) == 1
|
|
assert result[0].is_stale is True
|
|
|
|
|
|
def test_stale_aircraft_rendered_without_exception(renderer: Renderer) -> None:
|
|
display = NullDisplay()
|
|
stale_aircraft = dataclasses.replace(_make_aircraft(), is_stale=True)
|
|
mock_fetcher = MagicMock()
|
|
mock_fetcher.fetch.side_effect = requests.Timeout
|
|
result = _run_one_cycle(renderer, mock_fetcher, display, [stale_aircraft])
|
|
assert result[0].is_stale is True
|
|
|
|
|
|
def test_stale_cycle_completes_without_crash(renderer: Renderer) -> None:
|
|
display = NullDisplay()
|
|
last = [_make_aircraft()]
|
|
mock_fetcher = MagicMock()
|
|
mock_fetcher.fetch.side_effect = requests.Timeout
|
|
# Should complete without any exception
|
|
result = _run_one_cycle(renderer, mock_fetcher, display, last)
|
|
assert isinstance(result, list)
|
|
|
|
|
|
def test_recovery_after_timeout_returns_fresh(renderer: Renderer) -> None:
|
|
display = NullDisplay()
|
|
last = [_make_aircraft()]
|
|
mock_fetcher = MagicMock()
|
|
# Cycle 1: timeout → stale
|
|
mock_fetcher.fetch.side_effect = requests.Timeout
|
|
stale_result = _run_one_cycle(renderer, mock_fetcher, display, last)
|
|
assert stale_result[0].is_stale is True
|
|
# Cycle 2: fresh fetch → recovery
|
|
fresh_aircraft = [_make_aircraft(icao="DEF456")]
|
|
mock_fetcher.fetch.side_effect = None
|
|
mock_fetcher.fetch.return_value = fresh_aircraft
|
|
recovered = _run_one_cycle(renderer, mock_fetcher, display, stale_result)
|
|
assert len(recovered) == 1
|
|
assert recovered[0].is_stale is False
|
|
assert recovered[0].icao == "DEF456"
|
|
|
|
|
|
def test_recovery_after_empty_returns_fresh(renderer: Renderer) -> None:
|
|
display = NullDisplay()
|
|
last = [_make_aircraft()]
|
|
mock_fetcher = MagicMock()
|
|
# Cycle 1: empty → stale
|
|
mock_fetcher.fetch.return_value = []
|
|
stale_result = _run_one_cycle(renderer, mock_fetcher, display, last)
|
|
assert stale_result[0].is_stale is True
|
|
# Cycle 2: non-empty → recovery
|
|
mock_fetcher.fetch.return_value = [_make_aircraft(icao="GHI789")]
|
|
recovered = _run_one_cycle(renderer, mock_fetcher, display, stale_result)
|
|
assert recovered[0].is_stale is False
|