Files
vt1200/harness/badapple/tests/test_rdebug_load.py
T
arti d211ff0770 fix(badapple): --no-load must not wait for ICMP; survive a slow readback
Two faults the 60 fps work exposed, both in the delivery tooling:

* `demo.sh --no-load` waited for the terminal to answer ping.  A loaded payload
  owns the LANCE and stops the OS's stack with it, so that gate can never pass
  once the payload is running -- the recovery path documented for "already
  jumped" deadlocked for its full 180 s.  The gate now runs only when this
  invocation is going to load the payload.

* `rdebug_load.py` used a 5 s socket timeout and no retry, so a readback peek
  that timed out on a freshly booted OS failed a load whose pokes had already
  been acknowledged.  Timeout is 15 s (--timeout) and the readback retries once
  before it is believed.

Tests: the retry path is pinned with a fake remote that times out on the first
peek (one timeout, then the successful pass over every chunk) and one that never
answers (it must raise rather than loop).  9 tests in harness/badapple pass.
2026-10-31 22:20:58 +02:00

111 lines
3.9 KiB
Python

"""Tests for the rdebug loader and the clip builder.
The pieces that touch a live terminal cannot be tested here; what is tested is
everything that decides *what* gets written where, because those are the parts a
wrong assumption silently corrupts: the chunking arithmetic, the linkage the
entry point is read from, and the 1-bpp packing the terminal actually reads.
"""
import struct
import pytest
from harness.badapple import clip_from_video
from harness.badapple import rdebug_load
# --- chunking -------------------------------------------------------------
def test_chunks_are_transfer_sized_and_cover_the_image_once():
data = bytes(range(200)) * 23 # 4,600 B: not a multiple of 512
parts = rdebug_load.chunks(data, 512)
assert [len(p) for p in parts] == [512] * 8 + [504]
assert b"".join(parts) == data
def test_chunks_refuse_a_size_the_firmware_cannot_take():
# 1024 is rejected by the firmware: it reads the body's first word as the
# next opcode ("Invalid command 0xA5A5A5A5" on a 0xA5-filled chunk).
with pytest.raises(ValueError):
rdebug_load.chunks(b"x", 1024)
with pytest.raises(ValueError):
rdebug_load.chunks(b"x", 0)
def test_addresses_advance_by_the_chunk_length_in_bits():
base = 0x018C0000
assert rdebug_load.chunk_addr(base, 0) == base
assert rdebug_load.chunk_addr(base, 1) == base + 512 * 8
# --- linkage --------------------------------------------------------------
def test_env_file_gives_the_entry_and_the_pool_size(tmp_path):
env = tmp_path / "vidstream.env"
env.write_text("POOL_BYTES=249856\nSTATIC_PTR=0x01C00080\n"
"ENTRY_PTR=0x018D0270\n")
got = rdebug_load.read_env(env)
assert got["ENTRY_PTR"] == 0x018D0270
assert got["POOL_BYTES"] == 249856
def test_missing_env_file_says_so_instead_of_guessing(tmp_path, capsys):
rc = rdebug_load.main(["--image", "x.bin", "--env", str(tmp_path / "no.env")])
assert rc == 2
err = capsys.readouterr().err
assert "no build environment" in err and "build-stream.sh" in err
# --- readback ------------------------------------------------------------
class _Flaky:
"""A remote end whose first peek times out, as a busy OS does."""
def __init__(self, transfers, fail_times=1):
self.mem = dict(transfers)
self.fail = fail_times
self.calls = 0
def peek(self, addr, length):
self.calls += 1
if self.fail:
self.fail -= 1
raise TimeoutError("timed out")
return self.mem[addr][:length]
def test_readback_retries_a_stalled_peek_instead_of_failing_the_load():
transfers = rdebug_load.plan(b"0123456789", 0x018C0000, 4)
rd = _Flaky(transfers, fail_times=1)
assert rdebug_load.readback(rd, transfers) == b"0123456789"
# The retry re-reads the whole image: one timed-out chunk plus the three
# chunks of the successful pass.
assert rd.calls == len(transfers) + 1
def test_readback_gives_up_after_the_retry():
transfers = rdebug_load.plan(b"0123", 0x018C0000, 4)
rd = _Flaky(transfers, fail_times=9)
with pytest.raises(TimeoutError):
rdebug_load.readback(rd, transfers)
# --- the clip format ------------------------------------------------------
def test_packed_rows_put_pixel_zero_on_bit_zero_of_byte_zero():
"""The terminal reads bit 0 of a byte as the leftmost pixel.
Same rule the payload's encoder uses (`vidsrc.Rows.pack_row`), and the one
an MSB-first packer gets wrong in a way that only shows on a display.
"""
pixels = [[0] * 16]
pixels[0][0] = 1 # leftmost pixel of the row
pixels[0][15] = 1 # last pixel of the second byte
row = clip_from_video.pack_row(pixels[0])
assert len(row) == 2
assert row[0] == 0x01
assert row[1] == 0x80
def test_pack_row_rejects_a_short_row():
with pytest.raises(ValueError):
clip_from_video.pack_row([0] * 15)