"""Tests for scsi_device – SCSI frame chunking, header building, CRC. Tests use real ScsiDevice with an injected FakeScsiTransport — same DI flow as production. No patching of private methods, no subprocess mocks. The transport is the port; we test logic above it by controlling what the port returns. """ import binascii import struct import unittest from unittest.mock import MagicMock, patch import pytest from trcc.adapters.device.linux.detector import find_lcd_devices from trcc.adapters.device.scsi import ( _BOOT_MAX_RETRIES, _BOOT_SIGNATURE, _BOOT_WAIT_SECONDS, _CHUNK_SIZE_LARGE, _CHUNK_SIZE_SMALL, _FRAME_CMD_BASE, _POST_INIT_DELAY, ScsiDevice, ScsiTransport, ) from trcc.core.models import FBL_PROFILES _p320 = FBL_PROFILES[100] # 320×320 canonical profile # ========================================================================= # FakeScsiTransport — test double at the port boundary. # Scripts read responses + records writes. No subprocess, no USB. # ========================================================================= class FakeScsiTransport(ScsiTransport): """Test transport: scripted reads, recorded writes.""" def __init__(self, reads=None): self.reads = list(reads or []) self.sends: list[tuple[bytes, bytes]] = [] # (cdb, data) self.read_calls: list[tuple[bytes, int]] = [] # (cdb, length) self._open = False def open(self) -> bool: self._open = True return True def close(self) -> None: self._open = False def send_cdb(self, cdb: bytes, data: bytes) -> bool: self.sends.append((cdb, data)) return True def read_cdb(self, cdb: bytes, length: int) -> bytes: self.read_calls.append((cdb, length)) if self.reads: return self.reads.pop(0) return b'\x00' * length def _make_device(transport=None, width=320, height=320): """Build a ScsiDevice with the given transport (fake by default).""" return ScsiDevice( '/dev/sg0', transport or FakeScsiTransport(), width=width, height=height, vid=0x0402, pid=0x3922, ) class TestBootConstants(unittest.TestCase): """Verify boot state constants match USBLCD.exe protocol.""" def test_boot_signature(self): self.assertEqual(_BOOT_SIGNATURE, b'\xa1\xa2\xa3\xa4') def test_boot_wait_seconds(self): self.assertEqual(_BOOT_WAIT_SECONDS, 3.0) def test_boot_max_retries(self): self.assertGreaterEqual(_BOOT_MAX_RETRIES, 3) def test_post_init_delay(self): self.assertGreater(_POST_INIT_DELAY, 0) self.assertLessEqual(_POST_INIT_DELAY, 1.0) class TestCRC32(unittest.TestCase): """CRC32 against known values.""" def test_empty(self): self.assertEqual(ScsiDevice._crc32(b''), binascii.crc32(b'') & 0xFFFFFFFF) def test_known_value(self): self.assertEqual(ScsiDevice._crc32(b'hello'), binascii.crc32(b'hello') & 0xFFFFFFFF) def test_unsigned(self): """Result is always unsigned 32-bit.""" result = ScsiDevice._crc32(b'\xff' * 100) self.assertGreaterEqual(result, 0) self.assertLess(result, 2**32) class TestBuildHeader(unittest.TestCase): """20-byte SCSI command header: cmd(4) + zeros(8) + size(4) + crc32(4).""" def test_length(self): header = ScsiDevice._build_header(0xF5, 0xE100) self.assertEqual(len(header), 20) def test_cmd_field(self): header = ScsiDevice._build_header(0xF5, 0xE100) cmd = struct.unpack('