Initial commit - Phase 3/4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael
2026-01-06 13:45:29 -08:00
parent 1da6730735
commit 4f35df1781
323 changed files with 98287 additions and 1195 deletions

View File

@@ -0,0 +1,339 @@
"""Unit tests for PM3DeviceManager."""
import pytest
import asyncio
from unittest.mock import Mock, patch, AsyncMock, MagicMock
from datetime import datetime
from app.backend.managers.pm3_device_manager import (
PM3DeviceManager,
PM3Device,
DeviceStatus,
PM3FirmwareInfo
)
@pytest.fixture
def device_manager():
"""Create a PM3DeviceManager instance."""
return PM3DeviceManager()
@pytest.fixture
def mock_device():
"""Create a mock PM3Device."""
return PM3Device(
device_id="pm3_test123",
device_path="/dev/ttyACM0",
serial_number="ABC123",
friendly_name="Test PM3",
usb_vid="9ac4",
usb_pid="4b8f",
status=DeviceStatus.CONNECTED
)
class TestPM3DeviceManager:
"""Tests for PM3DeviceManager class."""
def test_initialization(self, device_manager):
"""Test device manager initialization."""
assert device_manager is not None
assert device_manager.auto_discover is True
assert device_manager.discovery_interval == 30
assert len(device_manager._devices) == 0
def test_generate_device_id(self, device_manager):
"""Test device ID generation."""
device_id = device_manager._generate_device_id("/dev/ttyACM0")
assert device_id.startswith("pm3_")
assert len(device_id) == 12 # pm3_ + 8 char hash
# Same path should generate same ID
device_id2 = device_manager._generate_device_id("/dev/ttyACM0")
assert device_id == device_id2
# Different path should generate different ID
device_id3 = device_manager._generate_device_id("/dev/ttyACM1")
assert device_id != device_id3
def test_generate_device_id_with_serial(self, device_manager):
"""Test device ID generation with serial number."""
device_id = device_manager._generate_device_id("/dev/ttyACM0", serial="ABC123")
assert device_id.startswith("pm3_")
# Same serial should generate same ID even with different path
device_id2 = device_manager._generate_device_id("/dev/ttyACM1", serial="ABC123")
assert device_id == device_id2
@pytest.mark.asyncio
async def test_get_device(self, device_manager, mock_device):
"""Test getting device by ID."""
# Add device to manager
device_manager._devices[mock_device.device_id] = mock_device
# Get existing device
device = await device_manager.get_device(mock_device.device_id)
assert device is not None
assert device.device_id == mock_device.device_id
# Get non-existent device
device = await device_manager.get_device("nonexistent")
assert device is None
@pytest.mark.asyncio
async def test_get_all_devices(self, device_manager, mock_device):
"""Test getting all devices."""
# Initially empty
devices = await device_manager.get_all_devices()
assert len(devices) == 0
# Add device
device_manager._devices[mock_device.device_id] = mock_device
# Get all devices
devices = await device_manager.get_all_devices()
assert len(devices) == 1
assert devices[0].device_id == mock_device.device_id
@pytest.mark.asyncio
async def test_get_available_devices(self, device_manager):
"""Test getting available (not in use) devices."""
# Create devices with different statuses
device1 = PM3Device(
device_id="pm3_1",
device_path="/dev/ttyACM0",
status=DeviceStatus.CONNECTED
)
device2 = PM3Device(
device_id="pm3_2",
device_path="/dev/ttyACM1",
status=DeviceStatus.IN_USE
)
device3 = PM3Device(
device_id="pm3_3",
device_path="/dev/ttyACM2",
status=DeviceStatus.DISCONNECTED
)
device_manager._devices = {
device1.device_id: device1,
device2.device_id: device2,
device3.device_id: device3,
}
# Get available devices (only CONNECTED)
available = await device_manager.get_available_devices()
assert len(available) == 1
assert available[0].device_id == device1.device_id
@pytest.mark.asyncio
async def test_get_connected_devices(self, device_manager):
"""Test getting connected devices."""
# Create devices with different statuses
device1 = PM3Device(
device_id="pm3_1",
device_path="/dev/ttyACM0",
status=DeviceStatus.CONNECTED
)
device2 = PM3Device(
device_id="pm3_2",
device_path="/dev/ttyACM1",
status=DeviceStatus.IN_USE
)
device3 = PM3Device(
device_id="pm3_3",
device_path="/dev/ttyACM2",
status=DeviceStatus.DISCONNECTED
)
device_manager._devices = {
device1.device_id: device1,
device2.device_id: device2,
device3.device_id: device3,
}
# Get connected devices (CONNECTED + IN_USE, not DISCONNECTED)
connected = await device_manager.get_connected_devices()
assert len(connected) == 2
assert device1 in connected
assert device2 in connected
assert device3 not in connected
@pytest.mark.asyncio
async def test_set_device_status(self, device_manager, mock_device):
"""Test setting device status."""
device_manager._devices[mock_device.device_id] = mock_device
# Set status
result = await device_manager.set_device_status(
mock_device.device_id,
DeviceStatus.IN_USE
)
assert result is True
assert mock_device.status == DeviceStatus.IN_USE
# Set status for non-existent device
result = await device_manager.set_device_status(
"nonexistent",
DeviceStatus.IN_USE
)
assert result is False
def test_parse_firmware_version(self, device_manager):
"""Test parsing firmware version from hw version output."""
output = """
Proxmark3 RFID instrument
bootrom: RRG/Iceman/master/v4.14831
os: RRG/Iceman/master/v4.14831
client: RRG/Iceman/master/v4.14831
"""
info = device_manager._parse_firmware_version(output)
assert info.bootrom_version == "v4.14831"
assert info.os_version == "v4.14831"
assert info.client_version == "v4.14831"
assert info.compatible is True
assert info.needs_upgrade is False
assert info.needs_downgrade is False
def test_parse_firmware_version_mismatch(self, device_manager):
"""Test parsing mismatched firmware versions."""
output = """
Proxmark3 RFID instrument
bootrom: RRG/Iceman/master/v4.14000
os: RRG/Iceman/master/v4.14000
client: RRG/Iceman/master/v4.14831
"""
info = device_manager._parse_firmware_version(output)
assert info.bootrom_version == "v4.14000"
assert info.os_version == "v4.14000"
assert info.client_version == "v4.14831"
assert info.compatible is False
assert info.needs_upgrade is True
@pytest.mark.asyncio
async def test_discover_via_dev(self, device_manager):
"""Test device discovery via /dev scanning."""
with patch('pathlib.Path.glob') as mock_glob:
# Mock /dev/ttyACM* files
mock_device1 = Mock()
mock_device1.is_char_device.return_value = True
mock_device1.__str__ = lambda x: "/dev/ttyACM0"
mock_device2 = Mock()
mock_device2.is_char_device.return_value = True
mock_device2.__str__ = lambda x: "/dev/ttyACM1"
mock_glob.return_value = [mock_device1, mock_device2]
devices = await device_manager._discover_via_dev()
assert len(devices) == 2
assert "/dev/ttyACM0" in devices
assert "/dev/ttyACM1" in devices
@pytest.mark.asyncio
async def test_identify_device_not_found(self, device_manager):
"""Test identifying a device that doesn't exist."""
with pytest.raises(ValueError, match="Device .* not found"):
await device_manager.identify_device("nonexistent")
@pytest.mark.asyncio
async def test_identify_device_no_worker(self, device_manager):
"""Test identifying a device without a worker."""
device = PM3Device(
device_id="pm3_test",
device_path="/dev/ttyACM0",
status=DeviceStatus.CONNECTED,
worker=None
)
device_manager._devices[device.device_id] = device
with pytest.raises(RuntimeError, match="has no worker"):
await device_manager.identify_device(device.device_id)
def test_device_to_dict(self, mock_device):
"""Test converting PM3Device to dictionary."""
data = mock_device.to_dict()
assert data["device_id"] == mock_device.device_id
assert data["device_path"] == mock_device.device_path
assert data["serial_number"] == mock_device.serial_number
assert data["friendly_name"] == mock_device.friendly_name
assert data["usb_vid"] == mock_device.usb_vid
assert data["usb_pid"] == mock_device.usb_pid
assert data["status"] == mock_device.status.value
assert data["connected"] is True
assert "firmware_info" in data
def test_device_status_enum(self):
"""Test DeviceStatus enum values."""
assert DeviceStatus.CONNECTED.value == "connected"
assert DeviceStatus.DISCONNECTED.value == "disconnected"
assert DeviceStatus.IN_USE.value == "in_use"
assert DeviceStatus.ERROR.value == "error"
assert DeviceStatus.VERSION_MISMATCH.value == "version_mismatch"
assert DeviceStatus.FLASHING.value == "flashing"
assert DeviceStatus.BOOTLOADER_MODE.value == "bootloader_mode"
assert DeviceStatus.DISABLED.value == "disabled"
class TestPM3FirmwareInfo:
"""Tests for PM3FirmwareInfo dataclass."""
def test_default_values(self):
"""Test default firmware info values."""
info = PM3FirmwareInfo()
assert info.bootrom_version == "unknown"
assert info.os_version == "unknown"
assert info.client_version == "unknown"
assert info.compatible is False
assert info.needs_upgrade is False
assert info.needs_downgrade is False
assert info.bootloader_outdated is False
def test_compatibility_check(self):
"""Test firmware compatibility logic."""
# Compatible versions
info1 = PM3FirmwareInfo(
bootrom_version="v4.14831",
os_version="v4.14831",
client_version="v4.14831"
)
info1.compatible = (
info1.os_version == info1.client_version and
info1.bootrom_version == info1.client_version
)
assert info1.compatible is True
# Incompatible versions
info2 = PM3FirmwareInfo(
bootrom_version="v4.14000",
os_version="v4.14000",
client_version="v4.14831"
)
info2.compatible = (
info2.os_version == info2.client_version and
info2.bootrom_version == info2.client_version
)
assert info2.compatible is False
@pytest.mark.asyncio
async def test_device_manager_lifecycle(device_manager):
"""Test device manager start/stop lifecycle."""
# Start manager
await device_manager.start()
assert device_manager._monitor_task is not None
# Stop manager
await device_manager.stop()
# Monitor task should be cancelled
if __name__ == "__main__":
pytest.main([__file__, "-v"])