140 lines
5.7 KiB
Python
140 lines
5.7 KiB
Python
"""
|
|
Layer 1 + 2 — static fixtures and frontend metadata.
|
|
|
|
These two layers always run (no backend, no compile, no QEMU). They
|
|
catch regressions where:
|
|
|
|
- The reproducer sketch is edited away from the upstream API surface
|
|
(e.g. someone changes `esp_camera_fb_get` to a custom name to make
|
|
the test pass — that defeats the regression).
|
|
- The frontend silently drops `esp32-cam` from BoardKind, the FQBN
|
|
table, or the rendered Web Component's pin list.
|
|
|
|
Mirrors the layout of test/esp32_cam/test_esp32_cam_blink.py so anyone
|
|
familiar with the issue-#129 regression suite reads this fluently.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import re
|
|
import unittest
|
|
|
|
|
|
_THIS_DIR = pathlib.Path(__file__).resolve().parent
|
|
_TEST_ROOT = _THIS_DIR.parent
|
|
_REPO = _TEST_ROOT.parent.parent
|
|
_FRONTEND = _REPO / "frontend" / "src"
|
|
_SKETCH = _TEST_ROOT / "sketches" / "camera_init" / "camera_init.ino"
|
|
|
|
|
|
# ── Layer 1 — static reproducer ─────────────────────────────────────────────
|
|
|
|
|
|
class TestCameraSketchSurface(unittest.TestCase):
|
|
"""The sketch that drives the live layer must call the upstream API
|
|
verbatim — `esp_camera_init`, `esp_camera_fb_get`, `esp_camera_fb_return`.
|
|
If a contributor "fixes" the test by inlining a different name, the
|
|
regression we care about (the upstream library not working under
|
|
Velxio) is no longer exercised."""
|
|
|
|
def test_sketch_exists(self):
|
|
self.assertTrue(
|
|
_SKETCH.is_file(), f"reproducer sketch missing: {_SKETCH}"
|
|
)
|
|
|
|
def test_sketch_includes_upstream_header(self):
|
|
text = _SKETCH.read_text(encoding="utf-8")
|
|
self.assertIn(
|
|
'#include "esp_camera.h"', text,
|
|
"sketch must include the upstream header — the whole point of the "
|
|
"shim work is that this exact include keeps working.",
|
|
)
|
|
|
|
def test_sketch_calls_init_and_fb_get(self):
|
|
text = _SKETCH.read_text(encoding="utf-8")
|
|
self.assertIn("esp_camera_init(&cfg)", text)
|
|
self.assertIn("esp_camera_fb_get()", text)
|
|
self.assertIn("esp_camera_fb_return(fb)", text)
|
|
|
|
def test_sketch_uses_jpeg_qvga_defaults(self):
|
|
"""QVGA + JPEG keeps the WS bandwidth in the few-KB-per-frame range
|
|
documented in autosearch/03. If a future change bumps this to VGA
|
|
without updating the bandwidth math, fail loudly."""
|
|
text = _SKETCH.read_text(encoding="utf-8")
|
|
self.assertIn("PIXFORMAT_JPEG", text)
|
|
self.assertIn("FRAMESIZE_QVGA", text)
|
|
|
|
|
|
# ── Layer 2 — frontend metadata ─────────────────────────────────────────────
|
|
|
|
|
|
class TestEsp32CamFrontendMetadata(unittest.TestCase):
|
|
"""ESP32-CAM is registered with the right FQBN and the rendered
|
|
Web Component exposes the SCCB pins our sketch relies on. Same idea
|
|
as the equivalent class in test_esp32_cam_blink.py, scoped to camera
|
|
pins (SIOD=26, SIOC=27, XCLK=0, PCLK=22, VSYNC=25, HREF=23)."""
|
|
|
|
BOARD_TS = _FRONTEND / "types" / "board.ts"
|
|
ELEMENT = _FRONTEND / "components" / "velxio-components" / "Esp32Element.ts"
|
|
|
|
def test_board_kind_present(self):
|
|
text = self.BOARD_TS.read_text(encoding="utf-8")
|
|
self.assertIn("'esp32-cam'", text, "BoardKind union missing 'esp32-cam'")
|
|
|
|
def test_fqbn_is_esp32cam_variant(self):
|
|
text = self.BOARD_TS.read_text(encoding="utf-8")
|
|
self.assertRegex(
|
|
text,
|
|
r"'esp32-cam'\s*:\s*'esp32:esp32:esp32cam'",
|
|
"BOARD_KIND_FQBN must point esp32-cam at esp32:esp32:esp32cam",
|
|
)
|
|
|
|
def _pin_block(self) -> str:
|
|
text = self.ELEMENT.read_text(encoding="utf-8")
|
|
match = re.search(r"PINS_ESP32_CAM\s*=\s*\[(.*?)\];", text, re.DOTALL)
|
|
self.assertIsNotNone(match, "PINS_ESP32_CAM array not found in element")
|
|
assert match is not None
|
|
return match.group(1)
|
|
|
|
def test_element_exposes_already_wired_pins(self):
|
|
"""GPIOs already broken-out on the rendered board (existing wiring
|
|
works for these — these are the user-facing GPIOs labelled on
|
|
the AI-Thinker silkscreen)."""
|
|
block = self._pin_block()
|
|
for pin in ("0", "2", "4", "12", "13", "14", "15", "16"):
|
|
self.assertRegex(
|
|
block,
|
|
r"\{\s*name:\s*'%s'" % pin,
|
|
f"ESP32-CAM element missing pin {pin!r}",
|
|
)
|
|
|
|
@unittest.expectedFailure
|
|
def test_element_exposes_camera_internal_pins(self):
|
|
"""The OV2640 is wired to internal GPIOs that DON'T appear on the
|
|
AI-Thinker silkscreen (they go under-board straight to the
|
|
camera socket): SIOD=26, SIOC=27, PCLK=22, VSYNC=25, HREF=23,
|
|
Y2..Y9 = 5,18,19,21,36,39,34,35.
|
|
|
|
For the shim path proposed in autosearch/04, **the user's sketch
|
|
does NOT need these pins to exist on the canvas** — the shim
|
|
skips the SCCB dance and synthesises the frame in firmware-space.
|
|
BUT: a future Path-B implementation that emulates real DVP would
|
|
require these pins to be present so wires can attach.
|
|
|
|
Marked expected-failure to document the gap. When/if Path B is
|
|
attempted, flip this to a passing assertion as the first PR step
|
|
— the rendered element must learn these pins before the QEMU
|
|
peripheral has anything to drive."""
|
|
block = self._pin_block()
|
|
for pin in ("22", "23", "25", "26", "27"):
|
|
self.assertRegex(
|
|
block,
|
|
r"\{\s*name:\s*'%s'" % pin,
|
|
f"ESP32-CAM element missing camera-internal pin {pin!r}",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|