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:
339
tests/unit/managers/test_pm3_device_manager.py
Normal file
339
tests/unit/managers/test_pm3_device_manager.py
Normal 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"])
|
||||
Reference in New Issue
Block a user