Replace Mock-based fixtures with real BLEInterface instances in stale identity check tests. This ensures coverage.py properly tracks execution of production code paths. The Mock approach with method binding executed the production code but coverage tracking was inconsistent. Using real instances guarantees proper coverage attribution. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
558 lines
24 KiB
Python
558 lines
24 KiB
Python
"""
|
|
Tests for zombie connection detection.
|
|
|
|
Zombie connections occur when the BLE link degrades - 1-byte keepalives still work
|
|
but larger data packets fail. Both sides think the connection is "alive" based on
|
|
keepalives, but data can't flow. This causes a deadlock where new connections are
|
|
rejected as "duplicates" even though the existing connection is non-functional.
|
|
|
|
The zombie detection feature tracks when real data (not keepalives) was last
|
|
received, and allows new connections if the existing connection has been a
|
|
"zombie" (only keepalives) for longer than the timeout.
|
|
"""
|
|
|
|
import pytest
|
|
import time
|
|
import threading
|
|
from unittest.mock import Mock, MagicMock, patch
|
|
|
|
|
|
class TestZombieConnectionDetection:
|
|
"""Test zombie connection detection in _check_duplicate_identity."""
|
|
|
|
@pytest.fixture
|
|
def mock_ble_interface(self):
|
|
"""Create a mock BLEInterface with real method bindings."""
|
|
try:
|
|
from ble_reticulum.BLEInterface import BLEInterface
|
|
except ImportError:
|
|
pytest.skip("BLEInterface not available")
|
|
|
|
interface = Mock(spec=BLEInterface)
|
|
interface.identity_to_address = {}
|
|
interface.address_to_identity = {}
|
|
interface.address_to_interface = {} # For _cleanup_stale_address
|
|
interface.pending_mtu = {} # For _cleanup_stale_address
|
|
interface.fragmenters = {} # For _cleanup_stale_address
|
|
interface.reassemblers = {} # For _cleanup_stale_address
|
|
interface.frag_lock = threading.RLock() # For _cleanup_stale_address
|
|
interface.peers = {}
|
|
interface._pending_detach = {}
|
|
interface._last_real_data = {}
|
|
interface._zombie_timeout = 30.0 # 30 seconds default
|
|
interface.driver = Mock()
|
|
interface.driver.connected_peers = []
|
|
interface.driver.disconnect = Mock()
|
|
|
|
# Import the actual methods we want to test
|
|
from ble_reticulum.BLEInterface import BLEInterface as RealInterface
|
|
|
|
interface._check_duplicate_identity = lambda addr, identity: RealInterface._check_duplicate_identity(interface, addr, identity)
|
|
interface._compute_identity_hash = lambda identity: RealInterface._compute_identity_hash(interface, identity)
|
|
interface._cleanup_stale_address = lambda ih, addr: RealInterface._cleanup_stale_address(interface, ih, addr)
|
|
interface._get_fragmenter_key = lambda identity, addr: RealInterface._get_fragmenter_key(interface, identity, addr)
|
|
interface.__str__ = Mock(return_value="BLEInterface[Test]")
|
|
|
|
return interface
|
|
|
|
def test_healthy_connection_rejects_duplicate(self, mock_ble_interface):
|
|
"""Test that a healthy connection (recent real data) rejects duplicates."""
|
|
interface = mock_ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
interface.driver.connected_peers.append(mac_old)
|
|
interface.peers[mac_old] = {"connected": True}
|
|
|
|
# Connection is healthy - real data received recently
|
|
interface._last_real_data[identity_hash] = time.time() - 10 # 10 seconds ago
|
|
|
|
# Should reject as duplicate
|
|
is_duplicate = interface._check_duplicate_identity(mac_new, identity)
|
|
assert is_duplicate, "Should reject duplicate when existing connection is healthy"
|
|
|
|
def test_zombie_connection_allows_reconnection(self, mock_ble_interface):
|
|
"""Test that a zombie connection (no real data for > timeout) allows reconnection."""
|
|
interface = mock_ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
interface.driver.connected_peers.append(mac_old)
|
|
interface.peers[mac_old] = {"connected": True}
|
|
|
|
# Connection is zombie - no real data for > timeout
|
|
interface._last_real_data[identity_hash] = time.time() - 60 # 60 seconds ago (> 30s timeout)
|
|
|
|
# Should allow reconnection
|
|
is_duplicate = interface._check_duplicate_identity(mac_new, identity)
|
|
assert not is_duplicate, "Should allow reconnection when existing connection is a zombie"
|
|
|
|
def test_zombie_disconnects_old_connection(self, mock_ble_interface):
|
|
"""Test that detecting a zombie triggers disconnect of the old connection."""
|
|
interface = mock_ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
interface.address_to_identity[mac_old] = identity
|
|
interface.driver.connected_peers.append(mac_old)
|
|
interface.peers[mac_old] = {"connected": True}
|
|
|
|
# Connection is zombie
|
|
interface._last_real_data[identity_hash] = time.time() - 60
|
|
|
|
# Check for duplicate
|
|
interface._check_duplicate_identity(mac_new, identity)
|
|
|
|
# Should have called disconnect on old address
|
|
interface.driver.disconnect.assert_called_once_with(mac_old)
|
|
|
|
def test_connection_at_zombie_threshold(self, mock_ble_interface):
|
|
"""Test behavior at the zombie timeout threshold."""
|
|
interface = mock_ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
interface.driver.connected_peers.append(mac_old)
|
|
interface.peers[mac_old] = {"connected": True}
|
|
|
|
# Just under the threshold - should still reject (needs to be > timeout)
|
|
# Use 29.5s to avoid timing flakiness (time passes between setting and checking)
|
|
interface._last_real_data[identity_hash] = time.time() - 29.5 # just under 30s
|
|
|
|
is_duplicate = interface._check_duplicate_identity(mac_new, identity)
|
|
# At under 30s, it should NOT be considered a zombie (needs to be > 30s)
|
|
assert is_duplicate, "Should reject duplicate when under threshold"
|
|
|
|
def test_no_last_data_timestamp_does_not_zombie(self, mock_ble_interface):
|
|
"""Test that missing last_real_data timestamp doesn't trigger zombie detection."""
|
|
interface = mock_ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
interface.driver.connected_peers.append(mac_old)
|
|
interface.peers[mac_old] = {"connected": True}
|
|
|
|
# No last_real_data entry - should not trigger zombie detection
|
|
# (This handles newly connected peers before any data is received)
|
|
|
|
is_duplicate = interface._check_duplicate_identity(mac_new, identity)
|
|
# Without timestamp, we can't determine zombie state - reject as duplicate
|
|
assert is_duplicate, "Should reject duplicate when no timestamp exists"
|
|
|
|
def test_custom_zombie_timeout(self, mock_ble_interface):
|
|
"""Test that custom zombie timeout is respected."""
|
|
interface = mock_ble_interface
|
|
interface._zombie_timeout = 10.0 # Custom 10 second timeout
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
interface.driver.connected_peers.append(mac_old)
|
|
interface.peers[mac_old] = {"connected": True}
|
|
|
|
# 15 seconds ago - should be zombie with 10s timeout
|
|
interface._last_real_data[identity_hash] = time.time() - 15
|
|
|
|
is_duplicate = interface._check_duplicate_identity(mac_new, identity)
|
|
assert not is_duplicate, "Should allow reconnection with custom zombie timeout"
|
|
|
|
|
|
class TestZombieTrackingOnDataReceive:
|
|
"""Test that real data updates the _last_real_data timestamp."""
|
|
|
|
@pytest.fixture
|
|
def mock_ble_interface(self):
|
|
"""Create a mock BLEInterface with real _handle_ble_data."""
|
|
try:
|
|
from ble_reticulum.BLEInterface import BLEInterface
|
|
except ImportError:
|
|
pytest.skip("BLEInterface not available")
|
|
|
|
interface = Mock(spec=BLEInterface)
|
|
interface.address_to_identity = {}
|
|
interface.identity_to_address = {}
|
|
interface._identity_cache = {}
|
|
interface._identity_cache_ttl = 60
|
|
interface._last_real_data = {}
|
|
interface._zombie_timeout = 30.0
|
|
interface.fragmenters = {}
|
|
interface.reassemblers = {}
|
|
interface.frag_lock = threading.RLock()
|
|
interface.driver = Mock()
|
|
interface.driver.request_identity_resync = Mock()
|
|
interface.__str__ = Mock(return_value="BLEInterface[Test]")
|
|
|
|
from ble_reticulum.BLEInterface import BLEInterface as RealInterface
|
|
|
|
interface._handle_ble_data = lambda addr, data: RealInterface._handle_ble_data(interface, addr, data)
|
|
interface._compute_identity_hash = lambda identity: RealInterface._compute_identity_hash(interface, identity)
|
|
interface._get_fragmenter_key = lambda identity, addr: RealInterface._get_fragmenter_key(interface, identity, addr)
|
|
|
|
return interface
|
|
|
|
def test_real_data_updates_timestamp(self, mock_ble_interface):
|
|
"""Test that receiving real data (not keepalive) updates the timestamp."""
|
|
interface = mock_ble_interface
|
|
|
|
address = "AA:BB:CC:DD:EE:01"
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
|
|
interface.address_to_identity[address] = identity
|
|
interface.identity_to_address[identity_hash] = address
|
|
|
|
# Setup reassembler
|
|
frag_key = interface._get_fragmenter_key(identity, address)
|
|
mock_reassembler = Mock()
|
|
mock_reassembler.add_fragment = Mock(return_value=None)
|
|
interface.reassemblers[frag_key] = mock_reassembler
|
|
|
|
# Clear any existing timestamp
|
|
interface._last_real_data.clear()
|
|
|
|
# Receive real data (10 bytes - not a keepalive)
|
|
real_data = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a'
|
|
before_time = time.time()
|
|
interface._handle_ble_data(address, real_data)
|
|
after_time = time.time()
|
|
|
|
# Timestamp should be updated
|
|
assert identity_hash in interface._last_real_data, "Should track real data timestamp"
|
|
timestamp = interface._last_real_data[identity_hash]
|
|
assert before_time <= timestamp <= after_time, "Timestamp should be within receive window"
|
|
|
|
def test_keepalive_does_not_update_timestamp(self, mock_ble_interface):
|
|
"""Test that receiving keepalive (1 byte 0x00) does NOT update timestamp."""
|
|
interface = mock_ble_interface
|
|
|
|
address = "AA:BB:CC:DD:EE:01"
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
|
|
interface.address_to_identity[address] = identity
|
|
interface.identity_to_address[identity_hash] = address
|
|
|
|
# Set an old timestamp
|
|
old_timestamp = time.time() - 100
|
|
interface._last_real_data[identity_hash] = old_timestamp
|
|
|
|
# Receive keepalive (1 byte 0x00)
|
|
keepalive = bytes([0x00])
|
|
interface._handle_ble_data(address, keepalive)
|
|
|
|
# Timestamp should NOT be updated
|
|
assert interface._last_real_data[identity_hash] == old_timestamp, \
|
|
"Keepalive should NOT update timestamp"
|
|
|
|
|
|
class TestZombieTrackingOnConnect:
|
|
"""Test that connection establishment initializes _last_real_data."""
|
|
|
|
@pytest.fixture
|
|
def mock_ble_interface(self):
|
|
"""Create a mock BLEInterface for spawn testing."""
|
|
try:
|
|
from ble_reticulum.BLEInterface import BLEInterface
|
|
except ImportError:
|
|
pytest.skip("BLEInterface not available")
|
|
|
|
interface = Mock(spec=BLEInterface)
|
|
interface.spawned_interfaces = {}
|
|
interface.address_to_interface = {}
|
|
interface._last_real_data = {}
|
|
interface._zombie_timeout = 30.0
|
|
interface.HW_MTU = 500
|
|
interface.bitrate = 700000
|
|
interface.mode = 0 # Interface.MODE_FULL
|
|
interface.ifac_size = 0
|
|
interface.ifac_netname = None
|
|
interface.ifac_netkey = None
|
|
interface.announce_cap = 1.0
|
|
interface.__str__ = Mock(return_value="BLEInterface[Test]")
|
|
|
|
return interface
|
|
|
|
def test_spawn_initializes_timestamp(self, mock_ble_interface):
|
|
"""Test that spawning a peer interface initializes the timestamp."""
|
|
interface = mock_ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
|
|
# Compute identity_hash the same way BLEInterface does
|
|
identity_hash = identity.hex()[:16]
|
|
|
|
# Clear timestamp
|
|
interface._last_real_data.clear()
|
|
|
|
# Simulate what _spawn_peer_interface does - it initializes the timestamp
|
|
before_time = time.time()
|
|
interface._last_real_data[identity_hash] = time.time()
|
|
after_time = time.time()
|
|
|
|
# Verify the timestamp was set
|
|
assert identity_hash in interface._last_real_data, \
|
|
"Spawning interface should initialize timestamp"
|
|
timestamp = interface._last_real_data[identity_hash]
|
|
assert before_time <= timestamp <= after_time, \
|
|
"Timestamp should be within expected range"
|
|
|
|
|
|
class TestPendingDetachAllowsReconnection:
|
|
"""Test that pending detach (Check 1) allows reconnection using real BLEInterface."""
|
|
|
|
@pytest.fixture
|
|
def ble_interface(self):
|
|
"""Create a real BLEInterface for testing."""
|
|
# Import test utilities
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
|
|
from tests.mock_ble_driver import MockBLEDriver
|
|
from ble_reticulum.BLEInterface import BLEInterface
|
|
|
|
driver = MockBLEDriver(local_address="AA:BB:CC:DD:EE:FF")
|
|
|
|
class MockOwner:
|
|
def inbound(self, data, interface):
|
|
pass
|
|
|
|
config = {"name": "TestInterface", "enable_peripheral": True}
|
|
interface = BLEInterface(MockOwner(), config)
|
|
interface.driver = driver
|
|
|
|
return interface
|
|
|
|
def test_pending_detach_allows_reconnection_from_new_mac(self, ble_interface):
|
|
"""Test that pending detach allows reconnection from new MAC (Check 1 path)."""
|
|
interface = ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
|
|
# Set up state: old connection exists in identity_to_address but has pending detach
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
interface._pending_detach[identity_hash] = time.time() # Pending detach exists
|
|
|
|
# Note: NOT in connected_peers or peers (connection is dead)
|
|
|
|
# Should allow reconnection because pending detach exists (Check 1)
|
|
is_duplicate = interface._check_duplicate_identity(mac_new, identity)
|
|
assert not is_duplicate, "Should allow reconnection when pending detach exists"
|
|
|
|
|
|
class TestNotConnectedAllowsReconnection:
|
|
"""Test that not-connected check (Check 2) allows reconnection using real BLEInterface."""
|
|
|
|
@pytest.fixture
|
|
def ble_interface(self):
|
|
"""Create a real BLEInterface for testing."""
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
|
|
from tests.mock_ble_driver import MockBLEDriver
|
|
from ble_reticulum.BLEInterface import BLEInterface
|
|
|
|
driver = MockBLEDriver(local_address="AA:BB:CC:DD:EE:FF")
|
|
|
|
class MockOwner:
|
|
def inbound(self, data, interface):
|
|
pass
|
|
|
|
config = {"name": "TestInterface", "enable_peripheral": True}
|
|
interface = BLEInterface(MockOwner(), config)
|
|
interface.driver = driver
|
|
|
|
return interface
|
|
|
|
def test_not_connected_allows_reconnection(self, ble_interface):
|
|
"""Test that stale entry without connection allows reconnection (Check 2 path)."""
|
|
interface = ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
|
|
# Set up state: old connection exists in identity_to_address
|
|
# but NOT in connected_peers, NOT in peers, and NO pending detach
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
# Note: _pending_detach is empty, driver.connected_peers is empty, peers is empty
|
|
|
|
# Should allow reconnection because old address is not connected (Check 2)
|
|
is_duplicate = interface._check_duplicate_identity(mac_new, identity)
|
|
assert not is_duplicate, "Should allow reconnection when old address is not connected"
|
|
|
|
def test_in_peers_but_not_connected_peers_still_rejects(self, ble_interface):
|
|
"""Test that being in peers dict still rejects (connection considered alive)."""
|
|
interface = ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
|
|
# Set up state: NOT in connected_peers but IS in peers
|
|
# This simulates a state where the driver doesn't know about the peer
|
|
# but our internal tracking does - we should still reject
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
interface.peers[mac_old] = {"connected": True}
|
|
# Note: driver.connected_peers is empty
|
|
|
|
# Should reject because old address is in peers dict
|
|
# The logic checks: if existing_address not in self.driver.connected_peers
|
|
# AND existing_address not in self.peers
|
|
# Here the second condition fails, so it falls through to zombie check
|
|
is_duplicate = interface._check_duplicate_identity(mac_new, identity)
|
|
# Since no timestamp exists and no pending detach, it should reject
|
|
assert is_duplicate, "Should reject when old address is still in peers"
|
|
|
|
|
|
class TestZombieDisconnectExceptionHandling:
|
|
"""Test exception handling when zombie disconnect fails using real BLEInterface."""
|
|
|
|
@pytest.fixture
|
|
def ble_interface(self):
|
|
"""Create a real BLEInterface for testing."""
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
|
|
from tests.mock_ble_driver import MockBLEDriver
|
|
from ble_reticulum.BLEInterface import BLEInterface
|
|
|
|
driver = MockBLEDriver(local_address="AA:BB:CC:DD:EE:FF")
|
|
|
|
class MockOwner:
|
|
def inbound(self, data, interface):
|
|
pass
|
|
|
|
config = {"name": "TestInterface", "enable_peripheral": True}
|
|
interface = BLEInterface(MockOwner(), config)
|
|
interface.driver = driver
|
|
|
|
return interface
|
|
|
|
def test_zombie_disconnect_exception_still_allows_reconnection(self, ble_interface):
|
|
"""Test that exception during zombie disconnect doesn't prevent reconnection."""
|
|
interface = ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
mac_old = "AA:BB:CC:DD:EE:01"
|
|
mac_new = "AA:BB:CC:DD:EE:02"
|
|
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
|
|
# Set up zombie state: old connection in maps, in connected_peers, timestamp is old
|
|
interface.identity_to_address[identity_hash] = mac_old
|
|
interface.address_to_identity[mac_old] = identity
|
|
interface.driver.connected_peers.append(mac_old)
|
|
interface.peers[mac_old] = {"connected": True}
|
|
interface._last_real_data[identity_hash] = time.time() - 60 # 60 seconds ago (zombie)
|
|
|
|
# Make disconnect raise an exception by patching the driver method
|
|
original_disconnect = interface.driver.disconnect
|
|
def raising_disconnect(addr):
|
|
raise Exception("BLE disconnect failed")
|
|
interface.driver.disconnect = raising_disconnect
|
|
|
|
# Should still allow reconnection despite exception
|
|
is_duplicate = interface._check_duplicate_identity(mac_new, identity)
|
|
assert not is_duplicate, "Should allow reconnection even when zombie disconnect fails"
|
|
|
|
# Restore original method
|
|
interface.driver.disconnect = original_disconnect
|
|
|
|
|
|
class TestZombieCleanupOnDetach:
|
|
"""Test that _last_real_data is cleaned up when interface is detached."""
|
|
|
|
@pytest.fixture
|
|
def mock_ble_interface(self):
|
|
"""Create a mock BLEInterface for detach testing."""
|
|
try:
|
|
from ble_reticulum.BLEInterface import BLEInterface
|
|
except ImportError:
|
|
pytest.skip("BLEInterface not available")
|
|
|
|
interface = Mock(spec=BLEInterface)
|
|
interface.spawned_interfaces = {}
|
|
interface.identity_to_address = {}
|
|
interface.address_to_identity = {}
|
|
interface._pending_detach = {}
|
|
interface._pending_detach_grace_period = 2.0
|
|
interface._last_real_data = {}
|
|
interface._zombie_timeout = 30.0
|
|
interface.fragmenters = {}
|
|
interface.reassemblers = {}
|
|
interface.frag_lock = threading.RLock()
|
|
interface.__str__ = Mock(return_value="BLEInterface[Test]")
|
|
|
|
from ble_reticulum.BLEInterface import BLEInterface as RealInterface
|
|
|
|
interface._process_pending_detaches = lambda: RealInterface._process_pending_detaches(interface)
|
|
interface._compute_identity_hash = lambda identity: RealInterface._compute_identity_hash(interface, identity)
|
|
interface._get_fragmenter_key = lambda identity, addr: RealInterface._get_fragmenter_key(interface, identity, addr)
|
|
|
|
return interface
|
|
|
|
def test_detach_cleans_up_timestamp(self, mock_ble_interface):
|
|
"""Test that detaching an interface cleans up the timestamp."""
|
|
interface = mock_ble_interface
|
|
|
|
identity = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
|
identity_hash = interface._compute_identity_hash(identity)
|
|
address = "AA:BB:CC:DD:EE:01"
|
|
|
|
# Create mock peer interface
|
|
mock_peer_if = Mock()
|
|
mock_peer_if.peer_identity = identity
|
|
mock_peer_if.detach = Mock()
|
|
|
|
interface.spawned_interfaces[identity_hash] = mock_peer_if
|
|
interface.identity_to_address[identity_hash] = address
|
|
interface._last_real_data[identity_hash] = time.time() - 100
|
|
|
|
# Schedule detach in the past
|
|
interface._pending_detach[identity_hash] = time.time() - 10 # 10 seconds ago
|
|
|
|
# Setup fragmenter key
|
|
frag_key = interface._get_fragmenter_key(identity, "")
|
|
interface.fragmenters[frag_key] = Mock()
|
|
interface.reassemblers[frag_key] = Mock()
|
|
|
|
# Process detaches
|
|
interface._process_pending_detaches()
|
|
|
|
# Timestamp should be cleaned up
|
|
assert identity_hash not in interface._last_real_data, \
|
|
"Detaching interface should clean up timestamp"
|