1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-06-09 09:34:42 -04:00

Reworked the entire client and drivers

This patch includes a completely reworked client structure.
There is now only 1 client object, that loads the appropriate
drivers.  The drivers are fake, aprsis and tcpkiss.

The TCPKISS client was written from scratch to avoid using asyncio.
Asyncion is nothing but a pain in the ass.
This commit is contained in:
2025-03-28 09:16:06 -04:00
parent 8f471c229c
commit 1c39546bb9
39 changed files with 2672 additions and 1493 deletions
View File
+440
View File
@@ -0,0 +1,440 @@
import datetime
import unittest
from unittest import mock
from aprslib.exceptions import LoginError
from aprsd import exception
from aprsd.client.drivers.aprsis import APRSISDriver
from aprsd.client.drivers.registry import ClientDriver
from aprsd.packets import core
class TestAPRSISDriver(unittest.TestCase):
"""Unit tests for the APRSISDriver class."""
def setUp(self):
# Mock configuration
self.conf_patcher = mock.patch('aprsd.client.drivers.aprsis.CONF')
self.mock_conf = self.conf_patcher.start()
# Configure APRS-IS settings
self.mock_conf.aprs_network.enabled = True
self.mock_conf.aprs_network.login = 'TEST'
self.mock_conf.aprs_network.password = '12345'
self.mock_conf.aprs_network.host = 'rotate.aprs.net'
self.mock_conf.aprs_network.port = 14580
# Mock APRS Lib Client
self.aprslib_patcher = mock.patch('aprsd.client.drivers.aprsis.APRSLibClient')
self.mock_aprslib = self.aprslib_patcher.start()
self.mock_client = mock.MagicMock()
self.mock_aprslib.return_value = self.mock_client
# Create an instance of the driver
self.driver = APRSISDriver()
def tearDown(self):
self.conf_patcher.stop()
self.aprslib_patcher.stop()
def test_implements_client_driver_protocol(self):
"""Test that APRSISDriver implements the ClientDriver Protocol."""
# Verify the instance is recognized as implementing the Protocol
self.assertIsInstance(self.driver, ClientDriver)
# Verify all required methods are present with correct signatures
required_methods = [
'is_enabled',
'is_configured',
'is_alive',
'close',
'send',
'setup_connection',
'set_filter',
'login_success',
'login_failure',
'consumer',
'decode_packet',
'stats',
]
for method_name in required_methods:
self.assertTrue(
hasattr(self.driver, method_name),
f'Missing required method: {method_name}',
)
def test_init(self):
"""Test initialization sets default values."""
self.assertIsInstance(self.driver.max_delta, datetime.timedelta)
self.assertEqual(self.driver.max_delta, datetime.timedelta(minutes=2))
self.assertFalse(self.driver.login_status['success'])
self.assertIsNone(self.driver.login_status['message'])
self.assertIsNone(self.driver._client)
def test_is_enabled_true(self):
"""Test is_enabled returns True when APRS-IS is enabled."""
self.mock_conf.aprs_network.enabled = True
self.assertTrue(APRSISDriver.is_enabled())
def test_is_enabled_false(self):
"""Test is_enabled returns False when APRS-IS is disabled."""
self.mock_conf.aprs_network.enabled = False
self.assertFalse(APRSISDriver.is_enabled())
def test_is_enabled_key_error(self):
"""Test is_enabled returns False when enabled flag doesn't exist."""
self.mock_conf.aprs_network = mock.MagicMock()
type(self.mock_conf.aprs_network).enabled = mock.PropertyMock(
side_effect=KeyError
)
self.assertFalse(APRSISDriver.is_enabled())
def test_is_configured_true(self):
"""Test is_configured returns True when properly configured."""
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=True):
self.mock_conf.aprs_network.login = 'TEST'
self.mock_conf.aprs_network.password = '12345'
self.mock_conf.aprs_network.host = 'rotate.aprs.net'
self.assertTrue(APRSISDriver.is_configured())
def test_is_configured_no_login(self):
"""Test is_configured raises exception when login not set."""
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=True):
self.mock_conf.aprs_network.login = None
with self.assertRaises(exception.MissingConfigOptionException):
APRSISDriver.is_configured()
def test_is_configured_no_password(self):
"""Test is_configured raises exception when password not set."""
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=True):
self.mock_conf.aprs_network.login = 'TEST'
self.mock_conf.aprs_network.password = None
with self.assertRaises(exception.MissingConfigOptionException):
APRSISDriver.is_configured()
def test_is_configured_no_host(self):
"""Test is_configured raises exception when host not set."""
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=True):
self.mock_conf.aprs_network.login = 'TEST'
self.mock_conf.aprs_network.password = '12345'
self.mock_conf.aprs_network.host = None
with self.assertRaises(exception.MissingConfigOptionException):
APRSISDriver.is_configured()
def test_is_configured_disabled(self):
"""Test is_configured returns True when not enabled."""
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=False):
self.assertTrue(APRSISDriver.is_configured())
def test_is_alive_no_client(self):
"""Test is_alive returns False when no client."""
self.driver._client = None
self.assertFalse(self.driver.is_alive)
def test_is_alive_true(self):
"""Test is_alive returns True when client is alive and connection is not stale."""
self.driver._client = self.mock_client
self.mock_client.is_alive.return_value = True
with mock.patch.object(self.driver, '_is_stale_connection', return_value=False):
self.assertTrue(self.driver.is_alive)
def test_is_alive_client_not_alive(self):
"""Test is_alive returns False when client is not alive."""
self.driver._client = self.mock_client
self.mock_client.is_alive.return_value = False
with mock.patch.object(self.driver, '_is_stale_connection', return_value=False):
self.assertFalse(self.driver.is_alive)
def test_is_alive_stale_connection(self):
"""Test is_alive returns False when connection is stale."""
self.driver._client = self.mock_client
self.mock_client.is_alive.return_value = True
with mock.patch.object(self.driver, '_is_stale_connection', return_value=True):
self.assertFalse(self.driver.is_alive)
def test_close(self):
"""Test close method stops and closes the client."""
self.driver._client = self.mock_client
self.driver.close()
self.mock_client.stop.assert_called_once()
self.mock_client.close.assert_called_once()
def test_close_no_client(self):
"""Test close method handles no client gracefully."""
self.driver._client = None
# Should not raise exception
self.driver.close()
def test_send(self):
"""Test send passes packet to client."""
self.driver._client = self.mock_client
mock_packet = mock.MagicMock(spec=core.Packet)
self.driver.send(mock_packet)
self.mock_client.send.assert_called_once_with(mock_packet)
@mock.patch('aprsd.client.drivers.aprsis.LOG')
def test_setup_connection_success(self, mock_log):
"""Test setup_connection successfully connects."""
# Configure successful connection
self.mock_client.server_string = 'Test APRS-IS Server'
self.driver.setup_connection()
# Check client created with correct parameters
self.mock_aprslib.assert_called_once_with(
self.mock_conf.aprs_network.login,
passwd=self.mock_conf.aprs_network.password,
host=self.mock_conf.aprs_network.host,
port=self.mock_conf.aprs_network.port,
)
# Check logger set and connection initialized
self.assertEqual(self.mock_client.logger, mock_log)
self.mock_client.connect.assert_called_once()
# Check status updated
self.assertTrue(self.driver.connected)
self.assertTrue(self.driver.login_status['success'])
self.assertEqual(self.driver.login_status['message'], 'Test APRS-IS Server')
@mock.patch('aprsd.client.drivers.aprsis.LOG')
@mock.patch('aprsd.client.drivers.aprsis.time.sleep')
def test_setup_connection_login_error(self, mock_sleep, mock_log):
"""Test setup_connection handles login error."""
# Configure login error
login_error = LoginError('Bad login')
login_error.message = 'Invalid login credentials'
self.mock_client.connect.side_effect = login_error
self.driver.setup_connection()
# Check error logged
mock_log.error.assert_any_call("Failed to login to APRS-IS Server 'Bad login'")
mock_log.error.assert_any_call('Invalid login credentials')
# Check status updated
self.assertFalse(self.driver.connected)
self.assertFalse(self.driver.login_status['success'])
self.assertEqual(
self.driver.login_status['message'], 'Invalid login credentials'
)
# Check backoff used
mock_sleep.assert_called()
@mock.patch('aprsd.client.drivers.aprsis.LOG')
@mock.patch('aprsd.client.drivers.aprsis.time.sleep')
def test_setup_connection_general_error(self, mock_sleep, mock_log):
"""Test setup_connection handles general error."""
# Configure general exception
error_message = 'Connection error'
error = Exception(error_message)
# Standard exceptions don't have a message attribute
self.mock_client.connect.side_effect = error
self.driver.setup_connection()
# Check error logged
mock_log.error.assert_any_call(
f"Unable to connect to APRS-IS server. '{error_message}' "
)
# Check status updated
self.assertFalse(self.driver.connected)
self.assertFalse(self.driver.login_status['success'])
# Check login message contains the error message (more flexible than exact equality)
self.assertIn(error_message, self.driver.login_status['message'])
# Check backoff used
mock_sleep.assert_called()
def test_set_filter(self):
"""Test set_filter passes filter to client."""
self.driver._client = self.mock_client
test_filter = 'm/50'
self.driver.set_filter(test_filter)
self.mock_client.set_filter.assert_called_once_with(test_filter)
def test_login_success(self):
"""Test login_success returns login status."""
self.driver.login_status['success'] = True
self.assertTrue(self.driver.login_success())
self.driver.login_status['success'] = False
self.assertFalse(self.driver.login_success())
def test_login_failure(self):
"""Test login_failure returns error message."""
self.driver.login_status['message'] = None
self.assertIsNone(self.driver.login_failure())
self.driver.login_status['message'] = 'Test error'
self.assertEqual(self.driver.login_failure(), 'Test error')
def test_filter_property(self):
"""Test filter property returns client filter."""
self.driver._client = self.mock_client
test_filter = 'm/50'
self.mock_client.filter = test_filter
self.assertEqual(self.driver.filter, test_filter)
def test_server_string_property(self):
"""Test server_string property returns client server string."""
self.driver._client = self.mock_client
test_string = 'Test APRS-IS Server'
self.mock_client.server_string = test_string
self.assertEqual(self.driver.server_string, test_string)
def test_keepalive_property(self):
"""Test keepalive property returns client keepalive."""
self.driver._client = self.mock_client
test_time = datetime.datetime.now()
self.mock_client.aprsd_keepalive = test_time
self.assertEqual(self.driver.keepalive, test_time)
@mock.patch('aprsd.client.drivers.aprsis.LOG')
def test_is_stale_connection_true(self, mock_log):
"""Test _is_stale_connection returns True when connection is stale."""
self.driver._client = self.mock_client
# Set keepalive to 3 minutes ago (exceeds max_delta of 2 minutes)
self.mock_client.aprsd_keepalive = datetime.datetime.now() - datetime.timedelta(
minutes=3
)
result = self.driver._is_stale_connection()
self.assertTrue(result)
mock_log.error.assert_called_once()
def test_is_stale_connection_false(self):
"""Test _is_stale_connection returns False when connection is not stale."""
self.driver._client = self.mock_client
# Set keepalive to 1 minute ago (within max_delta of 2 minutes)
self.mock_client.aprsd_keepalive = datetime.datetime.now() - datetime.timedelta(
minutes=1
)
result = self.driver._is_stale_connection()
self.assertFalse(result)
def test_transport(self):
"""Test transport returns appropriate transport type."""
self.assertEqual(APRSISDriver.transport(), 'aprsis')
def test_decode_packet(self):
"""Test decode_packet uses core.factory."""
with mock.patch('aprsd.client.drivers.aprsis.core.factory') as mock_factory:
raw_packet = {'from': 'TEST', 'to': 'APRS'}
self.driver.decode_packet(raw_packet)
mock_factory.assert_called_once_with(raw_packet)
@mock.patch('aprsd.client.drivers.aprsis.LOG')
def test_consumer_success(self, mock_log):
"""Test consumer forwards callback to client."""
self.driver._client = self.mock_client
mock_callback = mock.MagicMock()
self.driver.consumer(mock_callback, raw=True)
self.mock_client.consumer.assert_called_once_with(
mock_callback, blocking=False, immortal=False, raw=True
)
@mock.patch('aprsd.client.drivers.aprsis.LOG')
def test_consumer_exception(self, mock_log):
"""Test consumer handles exceptions."""
self.driver._client = self.mock_client
mock_callback = mock.MagicMock()
test_error = Exception('Test error')
self.mock_client.consumer.side_effect = test_error
with self.assertRaises(Exception): # noqa: B017
self.driver.consumer(mock_callback)
mock_log.error.assert_called_with(test_error)
@mock.patch('aprsd.client.drivers.aprsis.LOG')
def test_consumer_no_client(self, mock_log):
"""Test consumer handles no client gracefully."""
self.driver._client = None
mock_callback = mock.MagicMock()
self.driver.consumer(mock_callback)
mock_log.warning.assert_called_once()
self.assertFalse(self.driver.connected)
def test_stats_configured_with_client(self):
"""Test stats returns correct data when configured with client."""
# Configure driver
with mock.patch.object(self.driver, 'is_configured', return_value=True):
self.driver._client = self.mock_client
self.mock_client.aprsd_keepalive = datetime.datetime.now()
self.mock_client.server_string = 'Test Server'
self.mock_client.filter = 'm/50'
stats = self.driver.stats()
self.assertEqual(stats['connected'], True)
self.assertEqual(stats['filter'], 'm/50')
self.assertEqual(stats['server_string'], 'Test Server')
self.assertEqual(stats['transport'], 'aprsis')
def test_stats_serializable(self):
"""Test stats with serializable=True converts datetime to ISO format."""
# Configure driver
with mock.patch.object(self.driver, 'is_configured', return_value=True):
self.driver._client = self.mock_client
test_time = datetime.datetime.now()
self.mock_client.aprsd_keepalive = test_time
stats = self.driver.stats(serializable=True)
# Check keepalive is a string in ISO format
self.assertIsInstance(stats['connection_keepalive'], str)
# Try parsing it to verify it's a valid ISO format
try:
datetime.datetime.fromisoformat(stats['connection_keepalive'])
except ValueError:
self.fail('keepalive is not in valid ISO format')
def test_stats_no_client(self):
"""Test stats with no client."""
with mock.patch.object(self.driver, 'is_configured', return_value=True):
self.driver._client = None
stats = self.driver.stats()
self.assertEqual(stats['connection_keepalive'], 'None')
self.assertEqual(stats['server_string'], 'None')
def test_stats_not_configured(self):
"""Test stats when not configured returns empty dict."""
with mock.patch.object(self.driver, 'is_configured', return_value=False):
stats = self.driver.stats()
self.assertEqual(stats, {})
if __name__ == '__main__':
unittest.main()
+191
View File
@@ -0,0 +1,191 @@
import unittest
from unittest import mock
from aprsd.client.drivers.fake import APRSDFakeDriver
from aprsd.packets import core
class TestAPRSDFakeDriver(unittest.TestCase):
"""Unit tests for the APRSDFakeDriver class."""
def setUp(self):
# Mock CONF for testing
self.conf_patcher = mock.patch('aprsd.client.drivers.fake.CONF')
self.mock_conf = self.conf_patcher.start()
# Configure fake_client.enabled
self.mock_conf.fake_client.enabled = True
# Create an instance of the driver
self.driver = APRSDFakeDriver()
def tearDown(self):
self.conf_patcher.stop()
def test_init(self):
"""Test initialization sets default values."""
self.assertEqual(self.driver.path, ['WIDE1-1', 'WIDE2-1'])
self.assertFalse(self.driver.thread_stop)
def test_is_enabled_true(self):
"""Test is_enabled returns True when configured."""
self.mock_conf.fake_client.enabled = True
self.assertTrue(APRSDFakeDriver.is_enabled())
def test_is_enabled_false(self):
"""Test is_enabled returns False when not configured."""
self.mock_conf.fake_client.enabled = False
self.assertFalse(APRSDFakeDriver.is_enabled())
def test_is_alive(self):
"""Test is_alive returns True when thread_stop is False."""
self.driver.thread_stop = False
self.assertTrue(self.driver.is_alive())
self.driver.thread_stop = True
self.assertFalse(self.driver.is_alive())
def test_close(self):
"""Test close sets thread_stop to True."""
self.driver.thread_stop = False
self.driver.close()
self.assertTrue(self.driver.thread_stop)
@mock.patch('aprsd.client.drivers.fake.LOG')
def test_setup_connection(self, mock_log):
"""Test setup_connection does nothing (it's fake)."""
self.driver.setup_connection()
# Method doesn't do anything, so just verify it doesn't crash
def test_set_filter(self):
"""Test set_filter method does nothing (it's fake)."""
# Just test it doesn't fail
self.driver.set_filter('test/filter')
def test_login_success(self):
"""Test login_success always returns True."""
self.assertTrue(self.driver.login_success())
def test_login_failure(self):
"""Test login_failure always returns None."""
self.assertIsNone(self.driver.login_failure())
@mock.patch('aprsd.client.drivers.fake.LOG')
def test_send_with_packet_object(self, mock_log):
"""Test send with a Packet object."""
mock_packet = mock.MagicMock(spec=core.Packet)
mock_packet.payload = 'Test payload'
mock_packet.to_call = 'TEST'
mock_packet.from_call = 'FAKE'
self.driver.send(mock_packet)
mock_log.info.assert_called_once()
mock_packet.prepare.assert_called_once()
@mock.patch('aprsd.client.drivers.fake.LOG')
def test_send_with_non_packet_object(self, mock_log):
"""Test send with a non-Packet object."""
# Create a mock message-like object
mock_msg = mock.MagicMock()
mock_msg.raw = 'Test'
mock_msg.msgNo = '123'
mock_msg.to_call = 'TEST'
mock_msg.from_call = 'FAKE'
self.driver.send(mock_msg)
mock_log.info.assert_called_once()
mock_log.debug.assert_called_once()
@mock.patch('aprsd.client.drivers.fake.LOG')
@mock.patch('aprsd.client.drivers.fake.time.sleep')
def test_consumer_with_raw_true(self, mock_sleep, mock_log):
"""Test consumer with raw=True."""
mock_callback = mock.MagicMock()
self.driver.consumer(mock_callback, raw=True)
# Verify callback was called with raw data
mock_callback.assert_called_once()
call_args = mock_callback.call_args[1]
self.assertIn('raw', call_args)
mock_sleep.assert_called_once_with(1)
@mock.patch('aprsd.client.drivers.fake.LOG')
@mock.patch('aprsd.client.drivers.fake.aprslib.parse')
@mock.patch('aprsd.client.drivers.fake.core.factory')
@mock.patch('aprsd.client.drivers.fake.time.sleep')
def test_consumer_with_raw_false(
self, mock_sleep, mock_factory, mock_parse, mock_log
):
"""Test consumer with raw=False."""
mock_callback = mock.MagicMock()
mock_packet = mock.MagicMock(spec=core.Packet)
mock_factory.return_value = mock_packet
self.driver.consumer(mock_callback, raw=False)
# Verify the packet was created and passed to callback
mock_parse.assert_called_once()
mock_factory.assert_called_once()
mock_callback.assert_called_once_with(packet=mock_packet)
mock_sleep.assert_called_once_with(1)
def test_consumer_updates_keepalive(self):
"""Test consumer updates keepalive timestamp."""
mock_callback = mock.MagicMock()
old_keepalive = self.driver.aprsd_keepalive
# Force a small delay to ensure timestamp changes
import time
time.sleep(0.01)
with mock.patch('aprsd.client.drivers.fake.time.sleep'):
self.driver.consumer(mock_callback)
self.assertNotEqual(old_keepalive, self.driver.aprsd_keepalive)
self.assertGreater(self.driver.aprsd_keepalive, old_keepalive)
def test_decode_packet_with_empty_kwargs(self):
"""Test decode_packet with empty kwargs."""
result = self.driver.decode_packet()
self.assertIsNone(result)
def test_decode_packet_with_packet(self):
"""Test decode_packet with packet in kwargs."""
mock_packet = mock.MagicMock(spec=core.Packet)
result = self.driver.decode_packet(packet=mock_packet)
self.assertEqual(result, mock_packet)
@mock.patch('aprsd.client.drivers.fake.aprslib.parse')
@mock.patch('aprsd.client.drivers.fake.core.factory')
def test_decode_packet_with_raw(self, mock_factory, mock_parse):
"""Test decode_packet with raw in kwargs."""
mock_packet = mock.MagicMock(spec=core.Packet)
mock_factory.return_value = mock_packet
raw_data = 'raw packet data'
result = self.driver.decode_packet(raw=raw_data)
mock_parse.assert_called_once_with(raw_data)
mock_factory.assert_called_once_with(mock_parse.return_value)
self.assertEqual(result, mock_packet)
def test_stats(self):
"""Test stats returns correct information."""
self.driver.thread_stop = False
result = self.driver.stats()
self.assertEqual(result['driver'], 'APRSDFakeDriver')
self.assertTrue(result['is_alive'])
# Test with serializable parameter
result_serializable = self.driver.stats(serializable=True)
self.assertEqual(result_serializable['driver'], 'APRSDFakeDriver')
self.assertTrue(result_serializable['is_alive'])
if __name__ == '__main__':
unittest.main()
+498
View File
@@ -0,0 +1,498 @@
import datetime
import socket
import unittest
from unittest import mock
import aprslib
from aprsd import exception
from aprsd.client.drivers.registry import ClientDriver
from aprsd.client.drivers.tcpkiss import TCPKISSDriver
from aprsd.packets import core
class TestTCPKISSDriver(unittest.TestCase):
"""Unit tests for the TCPKISSDriver class."""
def setUp(self):
# Mock configuration
self.conf_patcher = mock.patch('aprsd.client.drivers.tcpkiss.CONF')
self.mock_conf = self.conf_patcher.start()
# Configure KISS settings
self.mock_conf.kiss_tcp.enabled = True
self.mock_conf.kiss_tcp.host = '127.0.0.1'
self.mock_conf.kiss_tcp.port = 8001
self.mock_conf.kiss_tcp.path = ['WIDE1-1', 'WIDE2-1']
# Mock socket
self.socket_patcher = mock.patch('aprsd.client.drivers.tcpkiss.socket')
self.mock_socket_module = self.socket_patcher.start()
self.mock_socket = mock.MagicMock()
self.mock_socket_module.socket.return_value = self.mock_socket
# Mock select
self.select_patcher = mock.patch('aprsd.client.drivers.tcpkiss.select')
self.mock_select = self.select_patcher.start()
# Create an instance of the driver
self.driver = TCPKISSDriver()
def tearDown(self):
self.conf_patcher.stop()
self.socket_patcher.stop()
self.select_patcher.stop()
def test_implements_client_driver_protocol(self):
"""Test that TCPKISSDriver implements the ClientDriver Protocol."""
# Verify the instance is recognized as implementing the Protocol
self.assertIsInstance(self.driver, ClientDriver)
# Verify all required methods are present with correct signatures
required_methods = [
'is_enabled',
'is_configured',
'is_alive',
'close',
'send',
'setup_connection',
'set_filter',
'login_success',
'login_failure',
'consumer',
'decode_packet',
'stats',
]
for method_name in required_methods:
self.assertTrue(
hasattr(self.driver, method_name),
f'Missing required method: {method_name}',
)
def test_init(self):
"""Test initialization sets default values."""
self.assertFalse(self.driver._connected)
self.assertIsInstance(self.driver.keepalive, datetime.datetime)
self.assertFalse(self.driver._running)
def test_transport_property(self):
"""Test transport property returns correct value."""
self.assertEqual(self.driver.transport, 'tcpkiss')
def test_is_enabled_true(self):
"""Test is_enabled returns True when KISS TCP is enabled."""
self.mock_conf.kiss_tcp.enabled = True
self.assertTrue(TCPKISSDriver.is_enabled())
def test_is_enabled_false(self):
"""Test is_enabled returns False when KISS TCP is disabled."""
self.mock_conf.kiss_tcp.enabled = False
self.assertFalse(TCPKISSDriver.is_enabled())
def test_is_configured_true(self):
"""Test is_configured returns True when properly configured."""
with mock.patch.object(TCPKISSDriver, 'is_enabled', return_value=True):
self.mock_conf.kiss_tcp.host = '127.0.0.1'
self.assertTrue(TCPKISSDriver.is_configured())
def test_is_configured_false_no_host(self):
"""Test is_configured returns False when host not set."""
with mock.patch.object(TCPKISSDriver, 'is_enabled', return_value=True):
self.mock_conf.kiss_tcp.host = None
with self.assertRaises(exception.MissingConfigOptionException):
TCPKISSDriver.is_configured()
def test_is_configured_false_not_enabled(self):
"""Test is_configured returns False when not enabled."""
with mock.patch.object(TCPKISSDriver, 'is_enabled', return_value=False):
self.assertFalse(TCPKISSDriver.is_configured())
def test_is_alive(self):
"""Test is_alive property returns connection state."""
self.driver._connected = True
self.assertTrue(self.driver.is_alive)
self.driver._connected = False
self.assertFalse(self.driver.is_alive)
def test_close(self):
"""Test close method calls stop."""
with mock.patch.object(self.driver, 'stop') as mock_stop:
self.driver.close()
mock_stop.assert_called_once()
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_setup_connection_success(self, mock_log):
"""Test setup_connection successfully connects."""
# Mock the connect method to succeed
is_en = self.driver.is_enabled
is_con = self.driver.is_configured
self.driver.is_enabled = mock.MagicMock(return_value=True)
self.driver.is_configured = mock.MagicMock(return_value=True)
with mock.patch.object(
self.driver, 'connect', return_value=True
) as mock_connect:
self.driver.setup_connection()
mock_connect.assert_called_once()
mock_log.info.assert_called_with('KISS TCP Connection to 127.0.0.1:8001')
self.driver.is_enabled = is_en
self.driver.is_configured = is_con
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_setup_connection_failure(self, mock_log):
"""Test setup_connection handles connection failure."""
# Mock the connect method to fail
with mock.patch.object(
self.driver, 'connect', return_value=False
) as mock_connect:
self.driver.setup_connection()
mock_connect.assert_called_once()
mock_log.error.assert_called_with('Failed to connect to KISS interface')
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_setup_connection_exception(self, mock_log):
"""Test setup_connection handles exceptions."""
# Mock the connect method to raise an exception
with mock.patch.object(
self.driver, 'connect', side_effect=Exception('Test error')
) as mock_connect:
self.driver.setup_connection()
mock_connect.assert_called_once()
mock_log.error.assert_any_call('Failed to initialize KISS interface')
mock_log.exception.assert_called_once()
self.assertFalse(self.driver._connected)
def test_set_filter(self):
"""Test set_filter does nothing for KISS."""
# Just ensure it doesn't fail
self.driver.set_filter('test/filter')
def test_login_success_when_connected(self):
"""Test login_success returns True when connected."""
self.driver._connected = True
self.assertTrue(self.driver.login_success())
def test_login_success_when_not_connected(self):
"""Test login_success returns False when not connected."""
self.driver._connected = False
self.assertFalse(self.driver.login_success())
def test_login_failure(self):
"""Test login_failure returns success message."""
self.assertEqual(self.driver.login_failure(), 'Login successful')
@mock.patch('aprsd.client.drivers.tcpkiss.ax25frame.Frame.ui')
def test_send_packet(self, mock_frame_ui):
"""Test sending an APRS packet."""
# Create a mock frame
mock_frame = mock.MagicMock()
mock_frame_bytes = b'mock_frame_data'
mock_frame.__bytes__ = mock.MagicMock(return_value=mock_frame_bytes)
mock_frame_ui.return_value = mock_frame
# Set up the driver
self.driver.socket = self.mock_socket
self.driver.path = ['WIDE1-1', 'WIDE2-1']
# Create a mock packet
mock_packet = mock.MagicMock(spec=core.Packet)
mock_bytes = b'Test packet data'
mock_packet.__bytes__ = mock.MagicMock(return_value=mock_bytes)
# Add path attribute to the mock packet
mock_packet.path = None
# Send the packet
self.driver.send(mock_packet)
# Check that frame was created correctly
mock_frame_ui.assert_called_once_with(
destination='APZ100',
source=mock_packet.from_call,
path=self.driver.path,
info=mock_packet.payload.encode('US-ASCII'),
)
# Check that socket send was called
self.mock_socket.send.assert_called_once()
# Verify packet counters updated
self.assertEqual(self.driver.packets_sent, 1)
self.assertIsNotNone(self.driver.last_packet_sent)
def test_send_with_no_socket(self):
"""Test send raises exception when socket not initialized."""
self.driver.socket = None
mock_packet = mock.MagicMock(spec=core.Packet)
with self.assertRaises(Exception) as context:
self.driver.send(mock_packet)
self.assertIn('KISS interface not initialized', str(context.exception))
def test_stop(self):
"""Test stop method cleans up properly."""
self.driver._running = True
self.driver._connected = True
self.driver.socket = self.mock_socket
self.driver.stop()
self.assertFalse(self.driver._running)
self.assertFalse(self.driver._connected)
self.mock_socket.close.assert_called_once()
def test_stats(self):
"""Test stats method returns correct data."""
# Set up test data
self.driver._connected = True
self.driver.path = ['WIDE1-1', 'WIDE2-1']
self.driver.packets_sent = 5
self.driver.packets_received = 3
self.driver.last_packet_sent = datetime.datetime.now()
self.driver.last_packet_received = datetime.datetime.now()
# Get stats
stats = self.driver.stats()
# Check stats contains expected keys
expected_keys = [
'client',
'transport',
'connected',
'path',
'packets_sent',
'packets_received',
'last_packet_sent',
'last_packet_received',
'connection_keepalive',
'host',
'port',
]
for key in expected_keys:
self.assertIn(key, stats)
# Check some specific values
self.assertEqual(stats['client'], 'TCPKISSDriver')
self.assertEqual(stats['transport'], 'tcpkiss')
self.assertEqual(stats['connected'], True)
self.assertEqual(stats['packets_sent'], 5)
self.assertEqual(stats['packets_received'], 3)
def test_stats_serializable(self):
"""Test stats with serializable=True converts datetime to ISO format."""
self.driver.keepalive = datetime.datetime.now()
stats = self.driver.stats(serializable=True)
# Check keepalive is a string in ISO format
self.assertIsInstance(stats['connection_keepalive'], str)
# Try parsing it to verify it's a valid ISO format
try:
datetime.datetime.fromisoformat(stats['connection_keepalive'])
except ValueError:
self.fail('keepalive is not in valid ISO format')
def test_connect_success(self):
"""Test successful connection."""
result = self.driver.connect()
self.assertTrue(result)
self.assertTrue(self.driver._connected)
self.mock_socket.connect.assert_called_once_with(
(self.mock_conf.kiss_tcp.host, self.mock_conf.kiss_tcp.port)
)
self.mock_socket.settimeout.assert_any_call(5.0)
self.mock_socket.settimeout.assert_any_call(0.1)
def test_connect_failure_socket_error(self):
"""Test connection failure due to socket error."""
self.mock_socket.connect.side_effect = socket.error('Test socket error')
result = self.driver.connect()
self.assertFalse(result)
self.assertFalse(self.driver._connected)
def test_connect_failure_timeout(self):
"""Test connection failure due to timeout."""
self.mock_socket.connect.side_effect = socket.timeout('Test timeout')
result = self.driver.connect()
self.assertFalse(result)
self.assertFalse(self.driver._connected)
def test_fix_raw_frame(self):
"""Test fix_raw_frame removes KISS markers and handles FEND."""
# Create a test frame with KISS markers
with mock.patch(
'aprsd.client.drivers.tcpkiss.handle_fend', return_value=b'fixed_frame'
) as mock_handle_fend:
raw_frame = b'\xc0\x00some_frame_data\xc0' # \xc0 is FEND
result = self.driver.fix_raw_frame(raw_frame)
mock_handle_fend.assert_called_once_with(b'some_frame_data')
self.assertEqual(result, b'fixed_frame')
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_decode_packet_success(self, mock_log):
"""Test successful packet decoding."""
mock_frame = 'test frame data'
mock_aprs_data = {'from': 'TEST-1', 'to': 'APRS'}
mock_packet = mock.MagicMock(spec=core.Packet)
with mock.patch(
'aprsd.client.drivers.tcpkiss.aprslib.parse', return_value=mock_aprs_data
) as mock_parse:
with mock.patch(
'aprsd.client.drivers.tcpkiss.core.factory', return_value=mock_packet
) as mock_factory:
result = self.driver.decode_packet(frame=mock_frame)
mock_parse.assert_called_once_with(str(mock_frame))
mock_factory.assert_called_once_with(mock_aprs_data)
self.assertEqual(result, mock_packet)
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_decode_packet_no_frame(self, mock_log):
"""Test decode_packet with no frame returns None."""
result = self.driver.decode_packet()
self.assertIsNone(result)
mock_log.warning.assert_called_once()
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_decode_packet_exception(self, mock_log):
"""Test decode_packet handles exceptions."""
mock_frame = 'invalid frame'
with mock.patch(
'aprsd.client.drivers.tcpkiss.aprslib.parse',
side_effect=Exception('Test error'),
) as mock_parse:
result = self.driver.decode_packet(frame=mock_frame)
mock_parse.assert_called_once()
self.assertIsNone(result)
mock_log.error.assert_called_once()
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_consumer_with_frame(self, mock_log):
"""Test consumer processes frames and calls callback."""
mock_callback = mock.MagicMock()
mock_frame = mock.MagicMock()
# Configure driver for test
self.driver._connected = True
self.driver._running = True
# Set up read_frame to return one frame then stop
def side_effect():
self.driver._running = False
return mock_frame
with mock.patch.object(
self.driver, 'read_frame', side_effect=side_effect
) as mock_read_frame:
self.driver.consumer(mock_callback)
mock_read_frame.assert_called_once()
mock_callback.assert_called_once_with(frame=mock_frame)
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_consumer_with_connect_reconnect(self, mock_log):
"""Test consumer tries to reconnect when not connected."""
mock_callback = mock.MagicMock()
# Configure driver for test
self.driver._connected = False
# Setup to run once then stop
call_count = 0
def connect_side_effect():
nonlocal call_count
call_count += 1
# On second call, connect successfully
if call_count == 2:
self.driver._running = False
self.driver.socket = self.mock_socket
return True
return False
with mock.patch.object(
self.driver, 'connect', side_effect=connect_side_effect
) as mock_connect:
with mock.patch('aprsd.client.drivers.tcpkiss.time.sleep') as mock_sleep:
self.driver.consumer(mock_callback)
self.assertEqual(mock_connect.call_count, 2)
mock_sleep.assert_called_once_with(1)
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_read_frame_success(self, mock_log):
"""Test read_frame successfully reads a frame."""
# Set up driver
self.driver.socket = self.mock_socket
self.driver._running = True
# Mock socket recv to return data
raw_data = b'\xc0\x00test_frame\xc0'
self.mock_socket.recv.return_value = raw_data
# Mock select to indicate socket is readable
self.mock_select.select.return_value = ([self.mock_socket], [], [])
# Mock fix_raw_frame and Frame.from_bytes
mock_fixed_frame = b'fixed_frame'
mock_ax25_frame = mock.MagicMock()
with mock.patch.object(
self.driver, 'fix_raw_frame', return_value=mock_fixed_frame
) as mock_fix:
with mock.patch(
'aprsd.client.drivers.tcpkiss.ax25frame.Frame.from_bytes',
return_value=mock_ax25_frame,
) as mock_from_bytes:
result = self.driver.read_frame()
self.mock_socket.setblocking.assert_called_once_with(0)
self.mock_select.select.assert_called_once()
self.mock_socket.recv.assert_called_once()
mock_fix.assert_called_once_with(raw_data)
mock_from_bytes.assert_called_once_with(mock_fixed_frame)
self.assertEqual(result, mock_ax25_frame)
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_read_frame_select_timeout(self, mock_log):
"""Test read_frame handles select timeout."""
# Set up driver
self.driver.socket = self.mock_socket
self.driver._running = True
# Mock select to indicate no readable sockets
self.mock_select.select.return_value = ([], [], [])
result = self.driver.read_frame()
self.assertIsNone(result)
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
def test_read_frame_socket_error(self, mock_log):
"""Test read_frame handles socket error."""
# Set up driver
self.driver.socket = self.mock_socket
self.driver._running = True
# Mock setblocking to raise OSError
self.mock_socket.setblocking.side_effect = OSError('Test error')
with self.assertRaises(aprslib.ConnectionDrop):
self.driver.read_frame()
mock_log.error.assert_called_once()
if __name__ == '__main__':
unittest.main()