feat(2-6): stateful Renderer, DisplayInterface, and pipeline smoke test

Implements story 2-6: Renderer class with per-aircraft trail history
(deque capped at TRAIL_MAX_DOTS=5), NullDisplay with DEBUG logging,
WaveshareDisplay stub, and end-to-end pipeline smoke test. All 96
tests pass; ruff check and format clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Edholm
2026-04-22 23:33:36 -04:00
parent 5d307c33b0
commit 25076dc1f3
6 changed files with 161 additions and 30 deletions
@@ -1,6 +1,6 @@
# Story 2.6: Stateful Renderer & Display Interface
Status: ready-for-dev
Status: review
## Story
@@ -22,22 +22,22 @@ AC5: **Given** the `test_pipeline.py` smoke test (`FileFixtureFetcher → Render
## Tasks / Subtasks
- [ ] Task 1: Update `NullDisplay` in `src/planemapper/display.py` (AC: #4)
- [ ] 1.1 Add `import logging` and `log = logging.getLogger(__name__)`
- [ ] 1.2 Update `NullDisplay.show()` to log at DEBUG level:
- [x] Task 1: Update `NullDisplay` in `src/planemapper/display.py` (AC: #4)
- [x] 1.1 Add `import logging` and `log = logging.getLogger(__name__)`
- [x] 1.2 Update `NullDisplay.show()` to log at DEBUG level:
```python
log.debug("NullDisplay.show: %dx%d", image.width, image.height)
```
- [ ] 1.3 Add `WaveshareDisplay` stub:
- [x] 1.3 Add `WaveshareDisplay` stub:
```python
class WaveshareDisplay:
def show(self, image: Image.Image) -> None:
raise NotImplementedError
```
- [ ] Task 2: Implement `Renderer` in `src/planemapper/renderer/renderer.py` (AC: #1, #2, #3)
- [ ] 2.1 Replace `# stub` with full implementation
- [ ] 2.2 Imports:
- [x] Task 2: Implement `Renderer` in `src/planemapper/renderer/renderer.py` (AC: #1, #2, #3)
- [x] 2.1 Replace `# stub` with full implementation
- [x] 2.2 Imports:
```python
import collections
from PIL import Image
@@ -48,7 +48,7 @@ AC5: **Given** the `test_pipeline.py` smoke test (`FileFixtureFetcher → Render
from planemapper.renderer.overlay import draw_home_marker
from planemapper.renderer.aircraft import draw_aircraft
```
- [ ] 2.3 Implement `__init__` with `base_map`, `bounds`, `_trails` dict:
- [x] 2.3 Implement `__init__` with `base_map`, `bounds`, `_trails` dict:
```python
def __init__(
self,
@@ -59,7 +59,7 @@ AC5: **Given** the `test_pipeline.py` smoke test (`FileFixtureFetcher → Render
self._bounds = bounds
self._trails: dict[str, collections.deque[tuple[int, int]]] = {}
```
- [ ] 2.4 Implement `render(aircraft_list)` with full pipeline:
- [x] 2.4 Implement `render(aircraft_list)` with full pipeline:
```python
def render(self, aircraft_list: list[Aircraft]) -> Image.Image:
image = self._base_map.copy()
@@ -76,14 +76,14 @@ AC5: **Given** the `test_pipeline.py` smoke test (`FileFixtureFetcher → Render
return image
```
- [ ] Task 3: Write tests in `tests/test_renderer.py` (AC: #1, #2, #3)
- [ ] 3.1 Replace placeholder with full test module
- [ ] 3.2 Test AC1: create white 800×480 RGB base map, call `render([])` with empty aircraft list, assert returned image is `PIL.Image.Image` with size `(800, 480)`
- [ ] 3.3 Test AC2: call `render()` twice with same aircraft; after second call, assert `renderer._trails` has an entry keyed on the aircraft's ICAO
- [ ] 3.4 Test AC3: call `render()` with one aircraft, then call `render([])` with empty list; assert `renderer._trails` still has the aircraft ICAO entry
- [x] Task 3: Write tests in `tests/test_renderer.py` (AC: #1, #2, #3)
- [x] 3.1 Replace placeholder with full test module
- [x] 3.2 Test AC1: create white 800×480 RGB base map, call `render([])` with empty aircraft list, assert returned image is `PIL.Image.Image` with size `(800, 480)`
- [x] 3.3 Test AC2: call `render()` twice with same aircraft; after second call, assert `renderer._trails` has an entry keyed on the aircraft's ICAO
- [x] 3.4 Test AC3: call `render()` with one aircraft, then call `render([])` with empty list; assert `renderer._trails` still has the aircraft ICAO entry
- [ ] Task 4: Write/update pipeline smoke test in `tests/test_pipeline.py` (AC: #5)
- [ ] 4.1 Replace placeholder with full smoke test:
- [x] Task 4: Write/update pipeline smoke test in `tests/test_pipeline.py` (AC: #5)
- [x] 4.1 Replace placeholder with full smoke test:
- Use `FileFixtureFetcher(Path("tests/fixtures/aircraft_sample.json"))`
- Create a fake 800×480 white RGB base map (`Image.new("RGB", (800, 480), "white")`)
- Create `Renderer(base_map, bounds)` where `bounds = MapBounds(53.0, -6.0, 100.0)`
@@ -91,12 +91,12 @@ AC5: **Given** the `test_pipeline.py` smoke test (`FileFixtureFetcher → Render
- Call `renderer.render(fetcher.fetch())`
- Assert returned image is `PIL.Image.Image` with size `(800, 480)`
- Call `NullDisplay().show(image)` — assert no exception
- [ ] 4.2 Ensure monkeypatch of `AIRSPACE_PATH` is applied before `render()` is called
- [x] 4.2 Ensure monkeypatch of `AIRSPACE_PATH` is applied before `render()` is called
- [ ] Task 5: Run quality gates
- [ ] 5.1 `python -m pytest tests/` — all tests pass
- [ ] 5.2 `python -m ruff check .` — zero violations
- [ ] 5.3 `python -m ruff format --check .` — no formatting issues
- [x] Task 5: Run quality gates
- [x] 5.1 `python -m pytest tests/` — all tests pass
- [x] 5.2 `python -m ruff check .` — zero violations
- [x] 5.3 `python -m ruff format --check .` — no formatting issues
## Implementation Notes
@@ -35,7 +35,7 @@
# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
generated: 2026-04-22
last_updated: 2026-04-22 # 2-1 done, 2-2 done, 2-3 done, 2-4 done, 2-5 done, 2-6 ready-for-dev, epic-2 in-progress
last_updated: 2026-04-22 # 2-1 done, 2-2 done, 2-3 done, 2-4 done, 2-5 done, 2-6 review, epic-2 in-progress
project: planeMapper
project_key: NOKEY
tracking_system: file-system
@@ -58,7 +58,7 @@ development_status:
2-3-home-marker-and-airspace-outlines: done
2-4-altitude-colour-bands-and-aircraft-type-icons: done
2-5-per-aircraft-drawing: done
2-6-stateful-renderer-and-display-interface: ready-for-dev
2-6-stateful-renderer-and-display-interface: review
2-7-operational-radar-loop-startup-screen-and-systemd-wiring: backlog
epic-2-retrospective: optional