feat: Complete Rust port of WiFi-DensePose with modular crates
Major changes: - Organized Python v1 implementation into v1/ subdirectory - Created Rust workspace with 9 modular crates: - wifi-densepose-core: Core types, traits, errors - wifi-densepose-signal: CSI processing, phase sanitization, FFT - wifi-densepose-nn: Neural network inference (ONNX/Candle/tch) - wifi-densepose-api: Axum-based REST/WebSocket API - wifi-densepose-db: SQLx database layer - wifi-densepose-config: Configuration management - wifi-densepose-hardware: Hardware abstraction - wifi-densepose-wasm: WebAssembly bindings - wifi-densepose-cli: Command-line interface Documentation: - ADR-001: Workspace structure - ADR-002: Signal processing library selection - ADR-003: Neural network inference strategy - DDD domain model with bounded contexts Testing: - 69 tests passing across all crates - Signal processing: 45 tests - Neural networks: 21 tests - Core: 3 doc tests Performance targets: - 10x faster CSI processing (~0.5ms vs ~5ms) - 5x lower memory usage (~100MB vs ~500MB) - WASM support for browser deployment
This commit is contained in:
410
v1/tests/unit/test_router_interface_tdd.py
Normal file
410
v1/tests/unit/test_router_interface_tdd.py
Normal file
@@ -0,0 +1,410 @@
|
||||
"""TDD tests for router interface following London School approach."""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import Mock, patch, AsyncMock, MagicMock
|
||||
from datetime import datetime, timezone
|
||||
import importlib.util
|
||||
|
||||
# Import the router interface module directly
|
||||
import unittest.mock
|
||||
|
||||
# Mock asyncssh before importing
|
||||
with unittest.mock.patch.dict('sys.modules', {'asyncssh': unittest.mock.MagicMock()}):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'router_interface',
|
||||
'/workspaces/wifi-densepose/src/hardware/router_interface.py'
|
||||
)
|
||||
router_module = importlib.util.module_from_spec(spec)
|
||||
|
||||
# Import CSI extractor for dependency
|
||||
csi_spec = importlib.util.spec_from_file_location(
|
||||
'csi_extractor',
|
||||
'/workspaces/wifi-densepose/src/hardware/csi_extractor.py'
|
||||
)
|
||||
csi_module = importlib.util.module_from_spec(csi_spec)
|
||||
csi_spec.loader.exec_module(csi_module)
|
||||
|
||||
# Now load the router interface
|
||||
router_module.CSIData = csi_module.CSIData # Make CSIData available
|
||||
spec.loader.exec_module(router_module)
|
||||
|
||||
# Get classes from modules
|
||||
RouterInterface = router_module.RouterInterface
|
||||
RouterConnectionError = router_module.RouterConnectionError
|
||||
CSIData = csi_module.CSIData
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.tdd
|
||||
@pytest.mark.london
|
||||
class TestRouterInterface:
|
||||
"""Test router interface using London School TDD."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_logger(self):
|
||||
"""Mock logger for testing."""
|
||||
return Mock()
|
||||
|
||||
@pytest.fixture
|
||||
def router_config(self):
|
||||
"""Router configuration for testing."""
|
||||
return {
|
||||
'host': '192.168.1.1',
|
||||
'port': 22,
|
||||
'username': 'admin',
|
||||
'password': 'password',
|
||||
'command_timeout': 30,
|
||||
'connection_timeout': 10,
|
||||
'max_retries': 3,
|
||||
'retry_delay': 1.0
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def router_interface(self, router_config, mock_logger):
|
||||
"""Create router interface for testing."""
|
||||
return RouterInterface(config=router_config, logger=mock_logger)
|
||||
|
||||
# Initialization tests
|
||||
def test_should_initialize_with_valid_config(self, router_config, mock_logger):
|
||||
"""Should initialize router interface with valid configuration."""
|
||||
interface = RouterInterface(config=router_config, logger=mock_logger)
|
||||
|
||||
assert interface.host == '192.168.1.1'
|
||||
assert interface.port == 22
|
||||
assert interface.username == 'admin'
|
||||
assert interface.password == 'password'
|
||||
assert interface.command_timeout == 30
|
||||
assert interface.connection_timeout == 10
|
||||
assert interface.max_retries == 3
|
||||
assert interface.retry_delay == 1.0
|
||||
assert interface.is_connected == False
|
||||
assert interface.logger == mock_logger
|
||||
|
||||
def test_should_raise_error_with_invalid_config(self, mock_logger):
|
||||
"""Should raise error when initialized with invalid configuration."""
|
||||
invalid_config = {'invalid': 'config'}
|
||||
|
||||
with pytest.raises(ValueError, match="Missing required configuration"):
|
||||
RouterInterface(config=invalid_config, logger=mock_logger)
|
||||
|
||||
def test_should_validate_required_fields(self, mock_logger):
|
||||
"""Should validate all required configuration fields."""
|
||||
required_fields = ['host', 'port', 'username', 'password']
|
||||
base_config = {
|
||||
'host': '192.168.1.1',
|
||||
'port': 22,
|
||||
'username': 'admin',
|
||||
'password': 'password'
|
||||
}
|
||||
|
||||
for field in required_fields:
|
||||
config = base_config.copy()
|
||||
del config[field]
|
||||
|
||||
with pytest.raises(ValueError, match="Missing required configuration"):
|
||||
RouterInterface(config=config, logger=mock_logger)
|
||||
|
||||
def test_should_use_default_values(self, mock_logger):
|
||||
"""Should use default values for optional parameters."""
|
||||
minimal_config = {
|
||||
'host': '192.168.1.1',
|
||||
'port': 22,
|
||||
'username': 'admin',
|
||||
'password': 'password'
|
||||
}
|
||||
|
||||
interface = RouterInterface(config=minimal_config, logger=mock_logger)
|
||||
|
||||
assert interface.command_timeout == 30 # default
|
||||
assert interface.connection_timeout == 10 # default
|
||||
assert interface.max_retries == 3 # default
|
||||
assert interface.retry_delay == 1.0 # default
|
||||
|
||||
def test_should_initialize_without_logger(self, router_config):
|
||||
"""Should initialize without logger provided."""
|
||||
interface = RouterInterface(config=router_config)
|
||||
|
||||
assert interface.logger is not None # Should create default logger
|
||||
|
||||
# Connection tests
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_connect_successfully(self, router_interface):
|
||||
"""Should establish SSH connection successfully."""
|
||||
mock_ssh_client = Mock()
|
||||
|
||||
with patch('src.hardware.router_interface.asyncssh.connect', new_callable=AsyncMock) as mock_connect:
|
||||
mock_connect.return_value = mock_ssh_client
|
||||
|
||||
result = await router_interface.connect()
|
||||
|
||||
assert result == True
|
||||
assert router_interface.is_connected == True
|
||||
assert router_interface.ssh_client == mock_ssh_client
|
||||
mock_connect.assert_called_once_with(
|
||||
'192.168.1.1',
|
||||
port=22,
|
||||
username='admin',
|
||||
password='password',
|
||||
connect_timeout=10
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_handle_connection_failure(self, router_interface):
|
||||
"""Should handle SSH connection failure gracefully."""
|
||||
with patch('src.hardware.router_interface.asyncssh.connect', new_callable=AsyncMock) as mock_connect:
|
||||
mock_connect.side_effect = ConnectionError("Connection failed")
|
||||
|
||||
result = await router_interface.connect()
|
||||
|
||||
assert result == False
|
||||
assert router_interface.is_connected == False
|
||||
assert router_interface.ssh_client is None
|
||||
router_interface.logger.error.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_disconnect_when_connected(self, router_interface):
|
||||
"""Should disconnect SSH connection when connected."""
|
||||
mock_ssh_client = Mock()
|
||||
router_interface.is_connected = True
|
||||
router_interface.ssh_client = mock_ssh_client
|
||||
|
||||
await router_interface.disconnect()
|
||||
|
||||
assert router_interface.is_connected == False
|
||||
assert router_interface.ssh_client is None
|
||||
mock_ssh_client.close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_handle_disconnect_when_not_connected(self, router_interface):
|
||||
"""Should handle disconnect when not connected."""
|
||||
router_interface.is_connected = False
|
||||
router_interface.ssh_client = None
|
||||
|
||||
await router_interface.disconnect()
|
||||
|
||||
# Should not raise any exception
|
||||
assert router_interface.is_connected == False
|
||||
|
||||
# Command execution tests
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_execute_command_successfully(self, router_interface):
|
||||
"""Should execute SSH command successfully."""
|
||||
mock_ssh_client = Mock()
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "command output"
|
||||
mock_result.stderr = ""
|
||||
mock_result.returncode = 0
|
||||
|
||||
router_interface.is_connected = True
|
||||
router_interface.ssh_client = mock_ssh_client
|
||||
|
||||
with patch.object(mock_ssh_client, 'run', new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
result = await router_interface.execute_command("test command")
|
||||
|
||||
assert result == "command output"
|
||||
mock_run.assert_called_once_with("test command", timeout=30)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_handle_command_execution_when_not_connected(self, router_interface):
|
||||
"""Should handle command execution when not connected."""
|
||||
router_interface.is_connected = False
|
||||
|
||||
with pytest.raises(RouterConnectionError, match="Not connected to router"):
|
||||
await router_interface.execute_command("test command")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_handle_command_execution_error(self, router_interface):
|
||||
"""Should handle command execution errors."""
|
||||
mock_ssh_client = Mock()
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = ""
|
||||
mock_result.stderr = "command error"
|
||||
mock_result.returncode = 1
|
||||
|
||||
router_interface.is_connected = True
|
||||
router_interface.ssh_client = mock_ssh_client
|
||||
|
||||
with patch.object(mock_ssh_client, 'run', new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
with pytest.raises(RouterConnectionError, match="Command failed"):
|
||||
await router_interface.execute_command("test command")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_retry_command_execution_on_failure(self, router_interface):
|
||||
"""Should retry command execution on temporary failure."""
|
||||
mock_ssh_client = Mock()
|
||||
mock_success_result = Mock()
|
||||
mock_success_result.stdout = "success output"
|
||||
mock_success_result.stderr = ""
|
||||
mock_success_result.returncode = 0
|
||||
|
||||
router_interface.is_connected = True
|
||||
router_interface.ssh_client = mock_ssh_client
|
||||
|
||||
with patch.object(mock_ssh_client, 'run', new_callable=AsyncMock) as mock_run:
|
||||
# First two calls fail, third succeeds
|
||||
mock_run.side_effect = [
|
||||
ConnectionError("Network error"),
|
||||
ConnectionError("Network error"),
|
||||
mock_success_result
|
||||
]
|
||||
|
||||
result = await router_interface.execute_command("test command")
|
||||
|
||||
assert result == "success output"
|
||||
assert mock_run.call_count == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_fail_after_max_retries(self, router_interface):
|
||||
"""Should fail after maximum retries exceeded."""
|
||||
mock_ssh_client = Mock()
|
||||
|
||||
router_interface.is_connected = True
|
||||
router_interface.ssh_client = mock_ssh_client
|
||||
|
||||
with patch.object(mock_ssh_client, 'run', new_callable=AsyncMock) as mock_run:
|
||||
mock_run.side_effect = ConnectionError("Network error")
|
||||
|
||||
with pytest.raises(RouterConnectionError, match="Command execution failed after 3 retries"):
|
||||
await router_interface.execute_command("test command")
|
||||
|
||||
assert mock_run.call_count == 3
|
||||
|
||||
# CSI data retrieval tests
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_get_csi_data_successfully(self, router_interface):
|
||||
"""Should retrieve CSI data successfully."""
|
||||
expected_csi_data = Mock(spec=CSIData)
|
||||
|
||||
with patch.object(router_interface, 'execute_command', new_callable=AsyncMock) as mock_execute:
|
||||
with patch.object(router_interface, '_parse_csi_response', return_value=expected_csi_data) as mock_parse:
|
||||
mock_execute.return_value = "csi data response"
|
||||
|
||||
result = await router_interface.get_csi_data()
|
||||
|
||||
assert result == expected_csi_data
|
||||
mock_execute.assert_called_once_with("iwlist scan | grep CSI")
|
||||
mock_parse.assert_called_once_with("csi data response")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_handle_csi_data_retrieval_failure(self, router_interface):
|
||||
"""Should handle CSI data retrieval failure."""
|
||||
with patch.object(router_interface, 'execute_command', new_callable=AsyncMock) as mock_execute:
|
||||
mock_execute.side_effect = RouterConnectionError("Command failed")
|
||||
|
||||
with pytest.raises(RouterConnectionError):
|
||||
await router_interface.get_csi_data()
|
||||
|
||||
# Router status tests
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_get_router_status_successfully(self, router_interface):
|
||||
"""Should get router status successfully."""
|
||||
expected_status = {
|
||||
'cpu_usage': 25.5,
|
||||
'memory_usage': 60.2,
|
||||
'wifi_status': 'active',
|
||||
'uptime': '5 days, 3 hours'
|
||||
}
|
||||
|
||||
with patch.object(router_interface, 'execute_command', new_callable=AsyncMock) as mock_execute:
|
||||
with patch.object(router_interface, '_parse_status_response', return_value=expected_status) as mock_parse:
|
||||
mock_execute.return_value = "status response"
|
||||
|
||||
result = await router_interface.get_router_status()
|
||||
|
||||
assert result == expected_status
|
||||
mock_execute.assert_called_once_with("cat /proc/stat && free && iwconfig")
|
||||
mock_parse.assert_called_once_with("status response")
|
||||
|
||||
# Configuration tests
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_configure_csi_monitoring_successfully(self, router_interface):
|
||||
"""Should configure CSI monitoring successfully."""
|
||||
config = {
|
||||
'channel': 6,
|
||||
'bandwidth': 20,
|
||||
'sample_rate': 100
|
||||
}
|
||||
|
||||
with patch.object(router_interface, 'execute_command', new_callable=AsyncMock) as mock_execute:
|
||||
mock_execute.return_value = "Configuration applied"
|
||||
|
||||
result = await router_interface.configure_csi_monitoring(config)
|
||||
|
||||
assert result == True
|
||||
mock_execute.assert_called_once_with(
|
||||
"iwconfig wlan0 channel 6 && echo 'CSI monitoring configured'"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_handle_csi_monitoring_configuration_failure(self, router_interface):
|
||||
"""Should handle CSI monitoring configuration failure."""
|
||||
config = {
|
||||
'channel': 6,
|
||||
'bandwidth': 20,
|
||||
'sample_rate': 100
|
||||
}
|
||||
|
||||
with patch.object(router_interface, 'execute_command', new_callable=AsyncMock) as mock_execute:
|
||||
mock_execute.side_effect = RouterConnectionError("Command failed")
|
||||
|
||||
result = await router_interface.configure_csi_monitoring(config)
|
||||
|
||||
assert result == False
|
||||
|
||||
# Health check tests
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_perform_health_check_successfully(self, router_interface):
|
||||
"""Should perform health check successfully."""
|
||||
with patch.object(router_interface, 'execute_command', new_callable=AsyncMock) as mock_execute:
|
||||
mock_execute.return_value = "pong"
|
||||
|
||||
result = await router_interface.health_check()
|
||||
|
||||
assert result == True
|
||||
mock_execute.assert_called_once_with("echo 'ping' && echo 'pong'")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_should_handle_health_check_failure(self, router_interface):
|
||||
"""Should handle health check failure."""
|
||||
with patch.object(router_interface, 'execute_command', new_callable=AsyncMock) as mock_execute:
|
||||
mock_execute.side_effect = RouterConnectionError("Command failed")
|
||||
|
||||
result = await router_interface.health_check()
|
||||
|
||||
assert result == False
|
||||
|
||||
# Parsing method tests
|
||||
def test_should_parse_csi_response(self, router_interface):
|
||||
"""Should parse CSI response data."""
|
||||
mock_response = "CSI_DATA:timestamp,antennas,subcarriers,frequency,bandwidth"
|
||||
|
||||
with patch('src.hardware.router_interface.CSIData') as mock_csi_data:
|
||||
expected_data = Mock(spec=CSIData)
|
||||
mock_csi_data.return_value = expected_data
|
||||
|
||||
result = router_interface._parse_csi_response(mock_response)
|
||||
|
||||
assert result == expected_data
|
||||
|
||||
def test_should_parse_status_response(self, router_interface):
|
||||
"""Should parse router status response."""
|
||||
mock_response = """
|
||||
cpu 123456 0 78901 234567 0 0 0 0 0 0
|
||||
MemTotal: 1024000 kB
|
||||
MemFree: 512000 kB
|
||||
wlan0 IEEE 802.11 ESSID:"TestNetwork"
|
||||
"""
|
||||
|
||||
result = router_interface._parse_status_response(mock_response)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert 'cpu_usage' in result
|
||||
assert 'memory_usage' in result
|
||||
assert 'wifi_status' in result
|
||||
Reference in New Issue
Block a user