mirror of
https://github.com/ratspeak/C6-Reticulum-ASM
synced 2026-08-12 18:07:18 -04:00
Closes the milestone-1 §harness deliverable's third target (the abstract
spec listed Emu/Hw/Oracle; only the Oracle and Emu targets had
implementations until now).
- HwTarget.start/stop/write/read against pyserial + esptool. Class-level
flash cache keyed on (port, image-bin path, mtime) so back-to-back
tests share the ~2 s reflash; per-test cost is just the DTR/RTS reset
pulse and a boot-drain to the `boot\tready` marker.
- BuildArtifacts grew an `image_bin` field; build('c6') now also runs
`make image` so the .image.bin and .elf stay in lockstep when a test
forces a rebuild.
- TargetConfig grew image_bin / flash_chip / auto_flash / boot_ready_timeout
fields; defaults match the Adafruit Feather (chip=esp32c6, port comes
from --hardware-port / RATSPEAK_HW_PORT, default /dev/cu.usbmodem4101).
- conftest.py adds --hardware (and RATSPEAK_HW=1 env) opt-in; tests
marked @pytest.mark.hardware skip cleanly without the flag.
- tests/hardware/test_basic_demo.py — three round-trip tests on real
silicon: HEADER_1 packet → packet.parsed; short payload → packet.rejected;
'S'+'abc' → SHA-256 KAT bit-for-bit equal to FIPS 180-4 §B.1.
- 442 pytest pass without --hardware (3 hardware skipped); 445 pass
with --hardware in ~28 s.
87 lines
3.5 KiB
Python
87 lines
3.5 KiB
Python
"""End-to-end demo on the physical Adafruit ESP32-C6 Feather (ADR-0010).
|
|
|
|
These tests are the hardware mirror of the qemu-virt path exercised by
|
|
tests/packet/test_packet_parse_header.py and the SHA-256 KAT bridge in
|
|
tests/crypto/sha256/test_sha256_compress.py. They flash the firmware,
|
|
reset the chip, and assert the structured-log lines our `_main` emits
|
|
when fed (a) a well-formed Reticulum HEADER_1 packet, (b) a too-short
|
|
packet, and (c) the SHA-256 KAT trigger byte (`'S'` + `"abc"`).
|
|
|
|
Skipped by default. Run with `pytest --hardware tests/hardware/` to opt
|
|
in (or `RATSPEAK_HW=1`). Conftest plumbing lives in `tests/conftest.py`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from harness import build, log_parser, oracle, target
|
|
|
|
pytestmark = pytest.mark.hardware
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def artifacts() -> build.BuildArtifacts:
|
|
"""Build TARGET=c6 once per module and reuse the .image.bin across tests."""
|
|
return build.build("c6")
|
|
|
|
|
|
@pytest.fixture
|
|
def hw(request: pytest.FixtureRequest, artifacts: build.BuildArtifacts) -> target.HwTarget:
|
|
"""Open a fresh HwTarget per test. The class-level flash cache means
|
|
the actual esptool reflash happens once per pytest session; reset +
|
|
boot drain is per-test."""
|
|
cfg = target.TargetConfig(
|
|
binary=artifacts.elf,
|
|
image_bin=artifacts.image_bin,
|
|
serial_port=request.config.getoption("--hardware-port"),
|
|
)
|
|
t = target.HwTarget(cfg)
|
|
if not t.is_available():
|
|
pytest.skip("HwTarget unavailable (missing esptool/pyserial/port)")
|
|
t.start()
|
|
yield t
|
|
t.stop()
|
|
|
|
|
|
def _send(t: target.HwTarget, payload: bytes) -> list[log_parser.LogEvent]:
|
|
framed = oracle.kiss_encode(payload)
|
|
t.write(framed)
|
|
lines = t.read_lines(timeout=1.5)
|
|
return log_parser.parse_lines(line + "\r\n" for line in lines)
|
|
|
|
|
|
def _minimal_h1_packet(packet_type: int = 0x01) -> bytes:
|
|
"""A 19-byte HEADER_1 packet matching test_packet_parse_header.py."""
|
|
flags = packet_type & 0x03
|
|
return bytes([flags, 0x00]) + b"h" * 16 + bytes([0x00])
|
|
|
|
|
|
def test_minimum_h1_packet_is_parsed_on_hardware(hw: target.HwTarget) -> None:
|
|
events = _send(hw, _minimal_h1_packet())
|
|
assert log_parser.find_event(events, module="kiss", event="rx_frame")
|
|
assert log_parser.find_event(events, module="packet", event="parsed"), \
|
|
[e.raw for e in events]
|
|
|
|
|
|
def test_short_packet_is_rejected_on_hardware(hw: target.HwTarget) -> None:
|
|
"""Same payload as the qemu-virt sibling test — first byte is 0x01
|
|
(not a KAT dispatcher tag) so the frame falls through to
|
|
packet_parse_header where the short length triggers rejection."""
|
|
events = _send(hw, b"\x01hrt!")
|
|
assert log_parser.find_event(events, module="kiss", event="rx_frame")
|
|
assert log_parser.find_event(events, module="packet", event="rejected"), \
|
|
[e.raw for e in events]
|
|
|
|
|
|
def test_sha256_kat_on_hardware(hw: target.HwTarget) -> None:
|
|
"""SHA-256 KAT bridge: 'S' || 'abc' must produce FIPS 180-4 §B.1
|
|
digest ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
|
|
on the wire. This proves the entire compress/update/final stack runs
|
|
correctly on the real RV32IMAC core — bit-for-bit equivalent to the
|
|
qemu-virt run and the Cryptol/SAW algorithmic spec."""
|
|
events = _send(hw, b"Sabc")
|
|
sha_event = log_parser.find_event(events, module="sha256", event="digest")
|
|
assert sha_event is not None, [e.raw for e in events]
|
|
expected = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
|
assert sha_event.fields.get("hex") == expected, sha_event.raw
|