|
| 1 | +""" |
| 2 | +Protocol-framing tests for rfunit.RfUnitI2C. |
| 3 | +
|
| 4 | +These pin down the exact wire format (command bytes, status-polling state |
| 5 | +machine, chunking math) so the TypeScript port in cmsis-dap-webusb/src/i2c/ |
| 6 | +can be checked byte-for-byte against the same fixtures. The golden vectors |
| 7 | +below (especially gen_challenge_response) are copied verbatim into |
| 8 | +src/i2c/rfunit-i2c.test.ts - if you change rfunit.py's protocol, update both. |
| 9 | +""" |
| 10 | + |
| 11 | +import struct |
| 12 | + |
| 13 | +import pytest |
| 14 | + |
| 15 | +import rfunit |
| 16 | + |
| 17 | + |
| 18 | +class RecordingI2CClient(rfunit.I2CClient): |
| 19 | + """Fake transport that records every write/transmit call and returns |
| 20 | + pre-programmed responses, queued in call order. Falls back to |
| 21 | + zero-filled responses once the queue is exhausted.""" |
| 22 | + |
| 23 | + def __init__(self, read_responses=None, transmit_responses=None): |
| 24 | + self.writes: list[list[int]] = [] |
| 25 | + self.transmits: list[tuple[list[int], int]] = [] |
| 26 | + self.read_lens: list[int] = [] |
| 27 | + self._read_responses = list(read_responses or []) |
| 28 | + self._transmit_responses = list(transmit_responses or []) |
| 29 | + |
| 30 | + def scan(self): |
| 31 | + return [rfunit.I2C_ADDR] |
| 32 | + |
| 33 | + def read(self, read_len: int): |
| 34 | + self.read_lens.append(read_len) |
| 35 | + if self._read_responses: |
| 36 | + return self._read_responses.pop(0) |
| 37 | + return [0] * read_len |
| 38 | + |
| 39 | + def write(self, data): |
| 40 | + self.writes.append(list(data)) |
| 41 | + |
| 42 | + def transmit(self, data, read_len: int): |
| 43 | + self.transmits.append((list(data), read_len)) |
| 44 | + if self._transmit_responses: |
| 45 | + return self._transmit_responses.pop(0) |
| 46 | + return [0] * read_len |
| 47 | + |
| 48 | + |
| 49 | +# --------------------------------------------------------------------------- |
| 50 | +# gen_challenge_response golden vectors |
| 51 | +# --------------------------------------------------------------------------- |
| 52 | + |
| 53 | +CHALLENGE_RESPONSE_VECTORS = [ |
| 54 | + ([0, 0, 0, 0], [0x85, 0x44, 0xE5, 0x2E]), |
| 55 | + ([3, 17, 250, 8], [0xC5, 0x53, 0x6F, 0x44]), |
| 56 | + ([255, 255, 255, 255], [0x15, 0x2A, 0x0F, 0x3A]), |
| 57 | + ([1, 2, 3, 4], [0x05, 0x1E, 0x40, 0x36]), |
| 58 | +] |
| 59 | + |
| 60 | + |
| 61 | +@pytest.mark.parametrize("challenge,expected", CHALLENGE_RESPONSE_VECTORS) |
| 62 | +def test_gen_challenge_response_golden_vectors(challenge, expected): |
| 63 | + assert rfunit.gen_challenge_response(challenge) == expected |
| 64 | + |
| 65 | + |
| 66 | +def test_gen_challenge_response_applies_modulo_11_to_each_byte(): |
| 67 | + # 11 (0xB) and 22 both reduce to 0 mod 11 - response must be identical. |
| 68 | + assert rfunit.gen_challenge_response([11, 11, 11, 11]) == rfunit.gen_challenge_response([22, 22, 22, 22]) |
| 69 | + assert rfunit.gen_challenge_response([0, 0, 0, 0]) == rfunit.gen_challenge_response([11, 22, 33, 44]) |
| 70 | + |
| 71 | + |
| 72 | +# --------------------------------------------------------------------------- |
| 73 | +# Command byte-framing |
| 74 | +# --------------------------------------------------------------------------- |
| 75 | + |
| 76 | + |
| 77 | +def test_read_register_frames_command_and_slices_status_bytes(): |
| 78 | + dev = RecordingI2CClient(transmit_responses=[[0xAA, 0xBB, 1, 2, 3, 4]]) |
| 79 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 80 | + |
| 81 | + result = rf.read_register(0x0C) |
| 82 | + |
| 83 | + assert dev.transmits == [([rfunit.CMD_REG_READ_xC1, 0x0C], 6)] |
| 84 | + assert result == [1, 2, 3, 4] |
| 85 | + |
| 86 | + |
| 87 | +def test_write_register_frames_command_register_and_data(): |
| 88 | + dev = RecordingI2CClient() |
| 89 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 90 | + |
| 91 | + rf.write_register(0x0C, [0x01, 0x02]) |
| 92 | + |
| 93 | + assert dev.writes == [[rfunit.CMD_REG_WRITE_x48, 0x0C, 0x01, 0x02]] |
| 94 | + |
| 95 | + |
| 96 | +def test_read_data_frames_address_as_little_endian_u32_and_slices_response(): |
| 97 | + dev = RecordingI2CClient(transmit_responses=[[0xAA, 0xBB, 1, 2, 3, 4, 5, 6, 0xFF]]) |
| 98 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 99 | + |
| 100 | + result = rf.read_data(0x00001234) |
| 101 | + |
| 102 | + expected_cmd = [rfunit.CMD_FLASH_READ_xC3, *struct.pack("<I", 0x00001234)] |
| 103 | + assert dev.transmits == [(expected_cmd, 8)] |
| 104 | + # read_data returns a bytes slice (rfunit.py:349), not a list. |
| 105 | + assert result == bytes([1, 2, 3, 4, 5, 6]) |
| 106 | + |
| 107 | + |
| 108 | +def test_erase_flash_frames_addr_and_count_as_little_endian_u32(monkeypatch): |
| 109 | + dev = RecordingI2CClient() |
| 110 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 111 | + monkeypatch.setattr(rf, "wait_busy", lambda: True) |
| 112 | + |
| 113 | + rf.erase_flash(0x1000, 0x24400) |
| 114 | + |
| 115 | + expected = [ |
| 116 | + rfunit.CMD_FLASH_ERASE_x95, |
| 117 | + *struct.pack("<I", 0x1000), |
| 118 | + *struct.pack("<I", 0x24400), |
| 119 | + ] |
| 120 | + assert dev.writes == [expected] |
| 121 | + |
| 122 | + |
| 123 | +def test_write_flash_sets_address_then_writes_data(monkeypatch): |
| 124 | + dev = RecordingI2CClient() |
| 125 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 126 | + monkeypatch.setattr(rf, "wait_busy", lambda: True) |
| 127 | + |
| 128 | + rf.write_flash(0x2000, b"\x01\x02\x03") |
| 129 | + |
| 130 | + assert dev.writes == [ |
| 131 | + [rfunit.CMD_FLASH_SET_WRITE_ADDR_x9B, *struct.pack("<I", 0x2000)], |
| 132 | + [rfunit.CMD_FLASH_WRITE_x9A, 1, 2, 3], |
| 133 | + ] |
| 134 | + |
| 135 | + |
| 136 | +def test_write_flash_aborts_before_data_write_if_set_address_wait_busy_fails(monkeypatch): |
| 137 | + dev = RecordingI2CClient() |
| 138 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 139 | + monkeypatch.setattr(rf, "wait_busy", lambda: False) |
| 140 | + |
| 141 | + result = rf.write_flash(0x2000, b"\x01\x02\x03") |
| 142 | + |
| 143 | + assert result is False |
| 144 | + # Only the "set address" write happened - not the data write. |
| 145 | + assert len(dev.writes) == 1 |
| 146 | + |
| 147 | + |
| 148 | +def test_boot_to_ldrom_sends_challenge_response_command(monkeypatch): |
| 149 | + dev = RecordingI2CClient() |
| 150 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 151 | + monkeypatch.setattr(rf, "get_timer_value", lambda: [0, 0, 0, 0]) |
| 152 | + monkeypatch.setattr(rf, "init", lambda: None) |
| 153 | + monkeypatch.setattr(rf, "stop", lambda: None) |
| 154 | + monkeypatch.setattr(rf, "wait_for_status", lambda target: True) |
| 155 | + |
| 156 | + # boot_to_ldrom busy-waits on a real 10-second time.time()-based clock |
| 157 | + # (rfunit.py:378-381 - needed on micropython to avoid a serial timeout). |
| 158 | + # Fake an advancing clock so the test doesn't block for 10 real seconds |
| 159 | + # (or hang forever if time.time() were naively frozen). |
| 160 | + fake_clock = [0.0] |
| 161 | + monkeypatch.setattr(rfunit.time, "time", lambda: fake_clock[0]) |
| 162 | + monkeypatch.setattr(rfunit.time, "sleep", lambda _s: fake_clock.__setitem__(0, fake_clock[0] + 1)) |
| 163 | + |
| 164 | + result = rf.boot_to_ldrom() |
| 165 | + |
| 166 | + expected = [rfunit.CMD_BOOT_LDROM_x4B, *rfunit.gen_challenge_response([0, 0, 0, 0])] |
| 167 | + assert dev.writes == [expected] |
| 168 | + assert result is True |
| 169 | + |
| 170 | + |
| 171 | +# --------------------------------------------------------------------------- |
| 172 | +# wait_busy / wait_for_status state machine |
| 173 | +# --------------------------------------------------------------------------- |
| 174 | + |
| 175 | + |
| 176 | +def _status_client(statuses): |
| 177 | + """A RecordingI2CClient whose read(2) calls step through `statuses` |
| 178 | + (each a full u16, little-endian-packed on read like the real status |
| 179 | + register), holding the last value once exhausted.""" |
| 180 | + |
| 181 | + class _StatusClient(RecordingI2CClient): |
| 182 | + def read(self, read_len: int): |
| 183 | + self.read_lens.append(read_len) |
| 184 | + value = statuses[min(len(self.read_lens) - 1, len(statuses) - 1)] |
| 185 | + return list(struct.pack("<H", value)) |
| 186 | + |
| 187 | + return _StatusClient() |
| 188 | + |
| 189 | + |
| 190 | +@pytest.fixture(autouse=True) |
| 191 | +def _no_real_sleep(monkeypatch): |
| 192 | + monkeypatch.setattr(rfunit.time, "sleep", lambda _s: None) |
| 193 | + |
| 194 | + |
| 195 | +def test_wait_busy_returns_true_once_status_leaves_busy(): |
| 196 | + dev = _status_client([rfunit.STATUS_BUSY, rfunit.STATUS_BUSY, rfunit.STATUS_READY]) |
| 197 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 198 | + |
| 199 | + assert rf.wait_busy() is True |
| 200 | + |
| 201 | + |
| 202 | +def test_wait_busy_returns_false_on_error_outside_ldrom(): |
| 203 | + dev = _status_client([rfunit.STATUS_ERROR]) |
| 204 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 205 | + rf.read_error_string = lambda: b"boom" |
| 206 | + |
| 207 | + assert rf.wait_busy() is False |
| 208 | + |
| 209 | + |
| 210 | +def test_wait_busy_ignores_error_flag_while_in_ldrom(): |
| 211 | + # STATUS_LDROM (0x0C) has the STATUS_ERROR (0x04) bit set too - the |
| 212 | + # real firmware always reports "error" while in LDROM, so wait_busy |
| 213 | + # must NOT treat this as a failure (rfunit.py's documented special case). |
| 214 | + dev = _status_client([rfunit.STATUS_LDROM]) |
| 215 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 216 | + |
| 217 | + assert rf.wait_busy() is True |
| 218 | + |
| 219 | + |
| 220 | +def test_wait_busy_passes_through_boot_ldrom_in_progress_then_succeeds(): |
| 221 | + dev = _status_client( |
| 222 | + [rfunit.STATUS_BOOT_LDROM_IN_PROGRESS, rfunit.STATUS_BOOT_LDROM_IN_PROGRESS, rfunit.STATUS_READY] |
| 223 | + ) |
| 224 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 225 | + |
| 226 | + assert rf.wait_busy() is True |
| 227 | + |
| 228 | + |
| 229 | +def test_wait_for_status_returns_true_when_target_bit_is_set(): |
| 230 | + dev = _status_client([rfunit.STATUS_BUSY, rfunit.STATUS_LDROM]) |
| 231 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 232 | + |
| 233 | + assert rf.wait_for_status(rfunit.STATUS_LDROM) is True |
| 234 | + |
| 235 | + |
| 236 | +def test_wait_for_status_returns_false_on_unknown_status(): |
| 237 | + dev = _status_client([0x40]) # not busy, not error, not the boot-in-progress sentinel |
| 238 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 239 | + |
| 240 | + assert rf.wait_for_status(rfunit.STATUS_READY) is False |
| 241 | + |
| 242 | + |
| 243 | +def test_wait_for_status_returns_false_on_error_outside_ldrom(): |
| 244 | + dev = _status_client([rfunit.STATUS_ERROR]) |
| 245 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 246 | + rf.read_error_string = lambda: b"boom" |
| 247 | + |
| 248 | + assert rf.wait_for_status(rfunit.STATUS_READY) is False |
| 249 | + |
| 250 | + |
| 251 | +# --------------------------------------------------------------------------- |
| 252 | +# dump_flash chunking |
| 253 | +# --------------------------------------------------------------------------- |
| 254 | + |
| 255 | + |
| 256 | +def test_dump_flash_yields_six_byte_chunks(): |
| 257 | + dev = RecordingI2CClient() |
| 258 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 259 | + rf.read_data = lambda addr: [addr & 0xFF] * 6 |
| 260 | + |
| 261 | + chunks = list(rf.dump_flash(0, 18)) |
| 262 | + |
| 263 | + assert len(chunks) == 3 |
| 264 | + assert all(len(c) == 6 for c in chunks) |
| 265 | + |
| 266 | + |
| 267 | +def test_dump_flash_truncates_final_partial_chunk(): |
| 268 | + dev = RecordingI2CClient() |
| 269 | + rf = rfunit.RfUnitI2C(dev, logger=lambda *_: None) |
| 270 | + rf.read_data = lambda addr: [1, 2, 3, 4, 5, 6] |
| 271 | + |
| 272 | + # 20 bytes total: two full 6-byte chunks + one 6-byte chunk truncated to 2. |
| 273 | + chunks = list(rf.dump_flash(0, 14)) |
| 274 | + |
| 275 | + assert [len(c) for c in chunks] == [6, 6, 2] |
| 276 | + assert chunks[-1] == [1, 2] |
0 commit comments