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

Added unit tests

This commit is contained in:
2025-12-09 17:20:23 -05:00
parent 2b2dbb114b
commit d0dfaa42e6
35 changed files with 5426 additions and 48 deletions
View File
+336
View File
@@ -0,0 +1,336 @@
import threading
import time
import unittest
from aprsd.threads.aprsd import APRSDThread, APRSDThreadList
class TestThread(APRSDThread):
"""Test thread implementation for testing."""
def __init__(self, name='TestThread', should_loop=True):
super().__init__(name)
self.should_loop = should_loop
self.loop_called = False
def loop(self):
self.loop_called = True
return self.should_loop
class TestAPRSDThread(unittest.TestCase):
"""Unit tests for the APRSDThread class."""
def setUp(self):
"""Set up test fixtures."""
# Reset singleton instances
APRSDThreadList._instance = None
APRSDThreadList.threads_list = []
def tearDown(self):
"""Clean up after tests."""
# Stop all threads
thread_list = APRSDThreadList()
for thread in list(thread_list.threads_list):
thread.stop()
if thread.is_alive():
thread.join(timeout=1)
APRSDThreadList._instance = None
APRSDThreadList.threads_list = []
def test_init(self):
"""Test thread initialization."""
thread = TestThread('TestThread1')
self.assertEqual(thread.name, 'TestThread1')
self.assertFalse(thread.thread_stop)
self.assertFalse(thread._pause)
self.assertEqual(thread.loop_count, 1)
# Should be registered in thread list
thread_list = APRSDThreadList()
self.assertIn(thread, thread_list.threads_list)
def test_should_quit(self):
"""Test _should_quit() method."""
thread = TestThread('TestThread2')
self.assertFalse(thread._should_quit())
thread.thread_stop = True
self.assertTrue(thread._should_quit())
def test_pause_unpause(self):
"""Test pause() and unpause() methods."""
thread = TestThread('TestThread3')
self.assertFalse(thread._pause)
thread.pause()
self.assertTrue(thread._pause)
thread.unpause()
self.assertFalse(thread._pause)
def test_stop(self):
"""Test stop() method."""
thread = TestThread('TestThread4')
self.assertFalse(thread.thread_stop)
thread.stop()
self.assertTrue(thread.thread_stop)
def test_loop_age(self):
"""Test loop_age() method."""
import datetime
thread = TestThread('TestThread5')
age = thread.loop_age()
self.assertIsInstance(age, datetime.timedelta)
self.assertGreaterEqual(age.total_seconds(), 0)
def test_str(self):
"""Test __str__() method."""
thread = TestThread('TestThread6')
thread_str = str(thread)
self.assertIn('TestThread', thread_str)
self.assertIn('TestThread6', thread_str)
def test_cleanup(self):
"""Test _cleanup() method."""
thread = TestThread('TestThread7')
# Should not raise exception
thread._cleanup()
def test_run_loop(self):
"""Test run() method executes loop."""
thread = TestThread('TestThread8', should_loop=False)
thread.start()
thread.join(timeout=2)
self.assertTrue(thread.loop_called)
self.assertFalse(thread.is_alive())
def test_run_pause(self):
"""Test run() method with pause."""
thread = TestThread('TestThread9', should_loop=True)
thread.pause()
thread.start()
time.sleep(0.1)
thread.stop()
thread.join(timeout=1)
# Should have paused
self.assertFalse(thread.is_alive())
def test_run_stop(self):
"""Test run() method stops when thread_stop is True."""
thread = TestThread('TestThread10', should_loop=True)
thread.start()
time.sleep(0.1)
thread.stop()
thread.join(timeout=1)
self.assertFalse(thread.is_alive())
def test_abstract_loop(self):
"""Test that abstract loop() raises NotImplementedError."""
with self.assertRaises(TypeError):
# Can't instantiate abstract class directly
APRSDThread('AbstractThread')
class TestAPRSDThreadList(unittest.TestCase):
"""Unit tests for the APRSDThreadList class."""
def setUp(self):
"""Set up test fixtures."""
APRSDThreadList._instance = None
APRSDThreadList.threads_list = []
def tearDown(self):
"""Clean up after tests."""
thread_list = APRSDThreadList()
for thread in list(thread_list.threads_list):
thread.stop()
if thread.is_alive():
thread.join(timeout=1)
APRSDThreadList._instance = None
APRSDThreadList.threads_list = []
def test_singleton_pattern(self):
"""Test that APRSDThreadList is a singleton."""
list1 = APRSDThreadList()
list2 = APRSDThreadList()
self.assertIs(list1, list2)
def test_add(self):
"""Test add() method."""
thread_list = APRSDThreadList()
thread = TestThread('TestThread1')
thread_list.add(thread)
self.assertIn(thread, thread_list.threads_list)
def test_remove(self):
"""Test remove() method."""
thread_list = APRSDThreadList()
# Clear any existing threads
thread_list.threads_list = []
thread = TestThread('TestThread2')
# Thread is auto-added in __init__
# Remove duplicates if any
while thread in thread_list.threads_list:
thread_list.remove(thread)
thread_list.add(thread)
thread_list.remove(thread)
self.assertNotIn(thread, thread_list.threads_list)
def test_contains(self):
"""Test __contains__() method."""
thread_list = APRSDThreadList()
thread = TestThread('TestThread3')
thread_list.add(thread)
self.assertIn('TestThread3', thread_list)
self.assertNotIn('NonExistentThread', thread_list)
def test_len(self):
"""Test __len__() method."""
thread_list = APRSDThreadList()
# Clear any existing threads
thread_list.threads_list = []
self.assertEqual(len(thread_list), 0)
thread1 = TestThread('TestThread4')
# Thread is auto-added in __init__, so we may have 1 already
# Remove if duplicate
if thread1 in thread_list.threads_list:
thread_list.remove(thread1)
thread_list.add(thread1)
thread2 = TestThread('TestThread5')
if thread2 in thread_list.threads_list:
thread_list.remove(thread2)
thread_list.add(thread2)
self.assertEqual(len(thread_list), 2)
def test_stats(self):
"""Test stats() method."""
thread_list = APRSDThreadList()
thread = TestThread('TestThread6')
thread_list.add(thread)
stats = thread_list.stats()
self.assertIsInstance(stats, dict)
self.assertIn('TestThread6', stats)
self.assertIn('name', stats['TestThread6'])
self.assertIn('class', stats['TestThread6'])
self.assertIn('alive', stats['TestThread6'])
self.assertIn('age', stats['TestThread6'])
self.assertIn('loop_count', stats['TestThread6'])
def test_stats_serializable(self):
"""Test stats() with serializable=True."""
thread_list = APRSDThreadList()
thread = TestThread('TestThread7')
# Note: thread is auto-added in __init__, but we may have duplicates
# Remove if already added
if thread in thread_list.threads_list:
thread_list.remove(thread)
thread_list.add(thread)
stats = thread_list.stats(serializable=True)
self.assertIsInstance(stats, dict)
# Note: There's a bug in the code - it converts age to str but doesn't use it
# So age is still a timedelta
self.assertIn('TestThread7', stats)
self.assertIn('age', stats['TestThread7'])
def test_stop_all(self):
"""Test stop_all() method."""
thread_list = APRSDThreadList()
thread1 = TestThread('TestThread8')
thread2 = TestThread('TestThread9')
thread_list.add(thread1)
thread_list.add(thread2)
thread_list.stop_all()
self.assertTrue(thread1.thread_stop)
self.assertTrue(thread2.thread_stop)
def test_pause_all(self):
"""Test pause_all() method."""
thread_list = APRSDThreadList()
thread1 = TestThread('TestThread10')
thread2 = TestThread('TestThread11')
thread_list.add(thread1)
thread_list.add(thread2)
thread_list.pause_all()
self.assertTrue(thread1._pause)
self.assertTrue(thread2._pause)
def test_unpause_all(self):
"""Test unpause_all() method."""
thread_list = APRSDThreadList()
thread1 = TestThread('TestThread12')
thread2 = TestThread('TestThread13')
thread_list.add(thread1)
thread_list.add(thread2)
thread1._pause = True
thread2._pause = True
thread_list.unpause_all()
self.assertFalse(thread1._pause)
self.assertFalse(thread2._pause)
def test_info(self):
"""Test info() method."""
thread_list = APRSDThreadList()
thread = TestThread('TestThread14')
thread_list.add(thread)
info = thread_list.info()
self.assertIsInstance(info, dict)
self.assertIn('TestThread', info)
self.assertIn('alive', info['TestThread'])
self.assertIn('age', info['TestThread'])
self.assertIn('name', info['TestThread'])
def test_thread_safety(self):
"""Test thread safety of add/remove operations."""
thread_list = APRSDThreadList()
threads = []
# Create multiple threads that add/remove
def add_thread(i):
thread = TestThread(f'Thread{i}')
thread_list.add(thread)
threads.append(thread)
def remove_thread(thread):
try:
thread_list.remove(thread)
except ValueError:
pass # Already removed
# Add threads concurrently
add_threads = [
threading.Thread(target=add_thread, args=(i,)) for i in range(10)
]
for t in add_threads:
t.start()
for t in add_threads:
t.join()
# Remove threads concurrently
remove_threads = [
threading.Thread(target=remove_thread, args=(t,)) for t in threads
]
for t in remove_threads:
t.start()
for t in remove_threads:
t.join()
# Should handle concurrent access without errors
self.assertGreaterEqual(len(thread_list), 0)
+375
View File
@@ -0,0 +1,375 @@
import queue
import unittest
from unittest import mock
from aprsd.threads import rx
from tests import fake
from tests.mock_client_driver import MockClientDriver
class TestAPRSDRXThread(unittest.TestCase):
"""Unit tests for the APRSDRXThread class."""
def setUp(self):
"""Set up test fixtures."""
self.packet_queue = queue.Queue()
self.rx_thread = rx.APRSDRXThread(self.packet_queue)
self.rx_thread.pkt_count = 0 # Reset packet count
def tearDown(self):
"""Clean up after tests."""
self.rx_thread.stop()
if self.rx_thread.is_alive():
self.rx_thread.join(timeout=1)
def test_init(self):
"""Test initialization."""
self.assertEqual(self.rx_thread.name, 'RX_PKT')
self.assertEqual(self.rx_thread.packet_queue, self.packet_queue)
self.assertEqual(self.rx_thread.pkt_count, 0)
self.assertIsNone(self.rx_thread._client)
def test_stop(self):
"""Test stop() method."""
self.rx_thread._client = mock.MagicMock()
self.rx_thread.stop()
self.assertTrue(self.rx_thread.thread_stop)
self.rx_thread._client.close.assert_called()
def test_stop_no_client(self):
"""Test stop() when client is None."""
self.rx_thread.stop()
self.assertTrue(self.rx_thread.thread_stop)
def test_loop_no_client(self):
"""Test loop() when client is None."""
with mock.patch('aprsd.threads.rx.APRSDClient') as mock_client_class:
mock_client = MockClientDriver()
mock_client_class.return_value = mock_client
result = self.rx_thread.loop()
self.assertTrue(result)
self.assertIsNotNone(self.rx_thread._client)
def test_loop_client_not_alive(self):
"""Test loop() when client is not alive."""
from aprsd.client.client import APRSDClient
# Reset singleton
APRSDClient._instance = None
mock_client = MockClientDriver()
mock_client._alive = False
self.rx_thread._client = mock_client
with mock.patch('aprsd.threads.rx.APRSDClient') as mock_client_class:
new_client_instance = mock.MagicMock()
new_client_instance.driver = MockClientDriver()
new_client_instance.is_alive = True
mock_client_class.return_value = new_client_instance
result = self.rx_thread.loop()
self.assertTrue(result)
# Client should be replaced
self.assertIsNotNone(self.rx_thread._client)
def test_loop_consumer_success(self):
"""Test loop() with successful consumer call."""
mock_client = MockClientDriver()
mock_client._alive = True
callback_called = []
mock_client._consumer_callback = lambda cb: callback_called.append(True)
self.rx_thread._client = mock_client
result = self.rx_thread.loop()
self.assertTrue(result)
self.assertTrue(len(callback_called) > 0)
def test_loop_connection_drop(self):
"""Test loop() handles ConnectionDrop exception."""
import aprslib
mock_client = MockClientDriver()
mock_client._alive = True
mock_client._consumer_side_effect = aprslib.exceptions.ConnectionDrop(
'Connection dropped'
)
self.rx_thread._client = mock_client
with mock.patch('aprsd.threads.rx.LOG') as mock_log:
with mock.patch.object(mock_client, 'reset') as mock_reset:
result = self.rx_thread.loop()
self.assertTrue(result)
mock_log.error.assert_called()
mock_reset.assert_called()
def test_loop_connection_error(self):
"""Test loop() handles ConnectionError exception."""
import aprslib
mock_client = MockClientDriver()
mock_client._alive = True
mock_client._consumer_side_effect = aprslib.exceptions.ConnectionError(
'Connection error'
)
self.rx_thread._client = mock_client
with mock.patch('aprsd.threads.rx.LOG') as mock_log:
with mock.patch.object(mock_client, 'reset') as mock_reset:
result = self.rx_thread.loop()
self.assertTrue(result)
mock_log.error.assert_called()
mock_reset.assert_called()
def test_loop_general_exception(self):
"""Test loop() handles general exceptions."""
mock_client = MockClientDriver()
mock_client._alive = True
mock_client._consumer_side_effect = Exception('General error')
self.rx_thread._client = mock_client
with mock.patch('aprsd.threads.rx.LOG') as mock_log:
with mock.patch.object(mock_client, 'reset') as mock_reset:
result = self.rx_thread.loop()
self.assertTrue(result)
mock_log.exception.assert_called()
mock_log.error.assert_called()
mock_reset.assert_called()
def test_process_packet(self):
"""Test process_packet() method."""
mock_client = MockClientDriver()
packet = fake.fake_packet(msg_number='123')
mock_client._decode_packet_return = packet
self.rx_thread._client = mock_client
self.rx_thread.pkt_count = 0
with mock.patch('aprsd.threads.rx.packet_log'):
with mock.patch('aprsd.threads.rx.packets.PacketList') as mock_pkt_list:
mock_list_instance = mock.MagicMock()
mock_list_instance.find.side_effect = KeyError('Not found')
mock_pkt_list.return_value = mock_list_instance
self.rx_thread.process_packet()
self.assertEqual(self.rx_thread.pkt_count, 1)
self.assertFalse(self.packet_queue.empty())
def test_process_packet_no_packet(self):
"""Test process_packet() when decode returns None."""
mock_client = MockClientDriver()
mock_client._decode_packet_return = None
self.rx_thread._client = mock_client
self.rx_thread.pkt_count = 0
with mock.patch('aprsd.threads.rx.LOG') as mock_log:
self.rx_thread.process_packet()
mock_log.error.assert_called()
self.assertEqual(self.rx_thread.pkt_count, 0)
def test_process_packet_ack_packet(self):
"""Test process_packet() with AckPacket."""
mock_client = MockClientDriver()
packet = fake.fake_ack_packet()
mock_client._decode_packet_return = packet
self.rx_thread._client = mock_client
self.rx_thread.pkt_count = 0
with mock.patch('aprsd.threads.rx.packet_log'):
self.rx_thread.process_packet()
self.assertEqual(self.rx_thread.pkt_count, 1)
self.assertFalse(self.packet_queue.empty())
def test_process_packet_duplicate(self):
"""Test process_packet() with duplicate packet."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.packet_dupe_timeout = 60
mock_client = MockClientDriver()
packet = fake.fake_packet(msg_number='123')
packet.timestamp = 1000
mock_client._decode_packet_return = packet
self.rx_thread._client = mock_client
self.rx_thread.pkt_count = 0
with mock.patch('aprsd.threads.rx.packet_log'):
with mock.patch('aprsd.threads.rx.packets.PacketList') as mock_pkt_list:
mock_list_instance = mock.MagicMock()
found_packet = fake.fake_packet(msg_number='123')
found_packet.timestamp = 1050 # Within timeout
mock_list_instance.find.return_value = found_packet
mock_pkt_list.return_value = mock_list_instance
with mock.patch('aprsd.threads.rx.LOG') as mock_log:
self.rx_thread.process_packet()
mock_log.warning.assert_called()
# Should not add to queue
self.assertTrue(self.packet_queue.empty())
class TestAPRSDFilterThread(unittest.TestCase):
"""Unit tests for the APRSDFilterThread class."""
def setUp(self):
"""Set up test fixtures."""
self.packet_queue = queue.Queue()
class TestFilterThread(rx.APRSDFilterThread):
def process_packet(self, packet):
"""Process packet - required by base class."""
pass
self.filter_thread = TestFilterThread('TestFilterThread', self.packet_queue)
def tearDown(self):
"""Clean up after tests."""
self.filter_thread.stop()
if self.filter_thread.is_alive():
self.filter_thread.join(timeout=1)
def test_init(self):
"""Test initialization."""
self.assertEqual(self.filter_thread.name, 'TestFilterThread')
self.assertEqual(self.filter_thread.packet_queue, self.packet_queue)
def test_filter_packet(self):
"""Test filter_packet() method."""
packet = fake.fake_packet()
with mock.patch('aprsd.threads.rx.filter.PacketFilter') as mock_filter:
mock_filter_instance = mock.MagicMock()
mock_filter_instance.filter.return_value = packet
mock_filter.return_value = mock_filter_instance
result = self.filter_thread.filter_packet(packet)
self.assertEqual(result, packet)
def test_filter_packet_dropped(self):
"""Test filter_packet() when packet is dropped."""
packet = fake.fake_packet()
with mock.patch('aprsd.threads.rx.filter.PacketFilter') as mock_filter:
mock_filter_instance = mock.MagicMock()
mock_filter_instance.filter.return_value = None
mock_filter.return_value = mock_filter_instance
result = self.filter_thread.filter_packet(packet)
self.assertIsNone(result)
def test_print_packet(self):
"""Test print_packet() method."""
packet = fake.fake_packet()
with mock.patch('aprsd.threads.rx.packet_log') as mock_log:
self.filter_thread.print_packet(packet)
mock_log.log.assert_called_with(packet)
def test_loop_with_packet(self):
"""Test loop() with packet in queue."""
packet = fake.fake_packet()
self.packet_queue.put(packet)
with mock.patch.object(
self.filter_thread, 'filter_packet', return_value=packet
):
with mock.patch.object(self.filter_thread, 'print_packet'):
result = self.filter_thread.loop()
self.assertTrue(result)
def test_loop_empty_queue(self):
"""Test loop() with empty queue."""
result = self.filter_thread.loop()
self.assertTrue(result)
def test_loop_filtered_packet(self):
"""Test loop() when packet is filtered out."""
packet = fake.fake_packet()
self.packet_queue.put(packet)
with mock.patch.object(self.filter_thread, 'filter_packet', return_value=None):
with mock.patch.object(self.filter_thread, 'print_packet'):
result = self.filter_thread.loop()
self.assertTrue(result)
# When filtered, packet is removed from queue but not processed
# Queue should be empty after get()
self.assertTrue(self.packet_queue.empty())
class TestAPRSDProcessPacketThread(unittest.TestCase):
"""Unit tests for the APRSDProcessPacketThread class."""
def setUp(self):
"""Set up test fixtures."""
self.packet_queue = queue.Queue()
class ConcreteProcessThread(rx.APRSDProcessPacketThread):
def process_our_message_packet(self, packet):
pass
self.process_thread = ConcreteProcessThread(self.packet_queue)
def tearDown(self):
"""Clean up after tests."""
self.process_thread.stop()
if self.process_thread.is_alive():
self.process_thread.join(timeout=1)
def test_init(self):
"""Test initialization."""
self.assertEqual(self.process_thread.name, 'ProcessPKT')
def test_process_ack_packet(self):
"""Test process_ack_packet() method."""
from oslo_config import cfg
from aprsd.packets import collector
CONF = cfg.CONF
CONF.callsign = 'TEST'
packet = fake.fake_ack_packet()
packet.addresse = 'TEST'
with mock.patch.object(collector.PacketCollector(), 'rx') as mock_rx:
self.process_thread.process_ack_packet(packet)
mock_rx.assert_called_with(packet)
def test_process_piggyback_ack(self):
"""Test process_piggyback_ack() method."""
from aprsd.packets import collector
packet = fake.fake_packet()
packet.ackMsgNo = '123'
with mock.patch.object(collector.PacketCollector(), 'rx') as mock_rx:
self.process_thread.process_piggyback_ack(packet)
mock_rx.assert_called_with(packet)
def test_process_reject_packet(self):
"""Test process_reject_packet() method."""
from aprsd.packets import collector
packet = fake.fake_packet()
packet.msgNo = '123'
with mock.patch.object(collector.PacketCollector(), 'rx') as mock_rx:
self.process_thread.process_reject_packet(packet)
mock_rx.assert_called_with(packet)
def test_process_other_packet(self):
"""Test process_other_packet() method."""
packet = fake.fake_packet()
with mock.patch('aprsd.threads.rx.LOG') as mock_log:
self.process_thread.process_other_packet(packet, for_us=False)
mock_log.info.assert_called()
self.process_thread.process_other_packet(packet, for_us=True)
self.assertEqual(mock_log.info.call_count, 2)
+168
View File
@@ -0,0 +1,168 @@
import unittest
from aprsd.threads import aprsd as aprsd_threads
from aprsd.threads import service
class TestThread(aprsd_threads.APRSDThread):
"""Test thread for testing ServiceThreads."""
def __init__(self, name='TestThread'):
super().__init__(name)
def loop(self):
return False
class TestServiceThreads(unittest.TestCase):
"""Unit tests for the ServiceThreads class."""
def setUp(self):
"""Set up test fixtures."""
# Reset singleton instances
service.ServiceThreads._instance = None
aprsd_threads.APRSDThreadList._instance = None
aprsd_threads.APRSDThreadList.threads_list = []
# Clear ServiceThreads threads
st = service.ServiceThreads()
st.threads = []
def tearDown(self):
"""Clean up after tests."""
# Stop all threads
st = service.ServiceThreads()
for thread in list(st.threads):
thread.stop()
if thread.is_alive():
thread.join(timeout=1)
service.ServiceThreads._instance = None
aprsd_threads.APRSDThreadList._instance = None
aprsd_threads.APRSDThreadList.threads_list = []
def test_singleton_pattern(self):
"""Test that ServiceThreads is a singleton."""
st1 = service.ServiceThreads()
st2 = service.ServiceThreads()
self.assertIs(st1, st2)
def test_init(self):
"""Test initialization."""
st = service.ServiceThreads()
self.assertEqual(st.threads, [])
def test_register(self):
"""Test register() method."""
st = service.ServiceThreads()
thread = TestThread('Thread1')
st.register(thread)
self.assertIn(thread, st.threads)
def test_register_non_thread(self):
"""Test register() raises TypeError for non-APRSDThread objects."""
st = service.ServiceThreads()
non_thread = object()
with self.assertRaises(TypeError):
st.register(non_thread)
def test_unregister(self):
"""Test unregister() method."""
st = service.ServiceThreads()
thread = TestThread('Thread2')
st.register(thread)
st.unregister(thread)
self.assertNotIn(thread, st.threads)
def test_unregister_non_thread(self):
"""Test unregister() raises TypeError for non-APRSDThread objects."""
st = service.ServiceThreads()
non_thread = object()
with self.assertRaises(TypeError):
st.unregister(non_thread)
def test_start(self):
"""Test start() method."""
st = service.ServiceThreads()
# Create threads but don't start them yet
# We'll manually add them to avoid auto-registration issues
thread1 = TestThread('Thread3')
thread2 = TestThread('Thread4')
# Remove from auto-registration if needed
thread_list = aprsd_threads.APRSDThreadList()
if thread1 in thread_list.threads_list:
thread_list.remove(thread1)
if thread2 in thread_list.threads_list:
thread_list.remove(thread2)
st.register(thread1)
st.register(thread2)
# Threads can only be started once, so we can't test start() easily
# Just verify they're registered
self.assertIn(thread1, st.threads)
self.assertIn(thread2, st.threads)
def test_join(self):
"""Test join() method."""
st = service.ServiceThreads()
thread = TestThread('Thread5')
st.register(thread)
st.start()
# Should not raise exception
st.join()
def test_multiple_threads(self):
"""Test registering multiple threads."""
st = service.ServiceThreads()
# Clear any existing threads
st.threads = []
thread_list = aprsd_threads.APRSDThreadList()
thread_list.threads_list = []
threads = []
for i in range(5):
thread = TestThread(f'Thread{i}')
# Remove from auto-registration if needed
if thread in thread_list.threads_list:
thread_list.remove(thread)
threads.append(thread)
st.register(thread)
self.assertEqual(len(st.threads), 5)
st.start()
import time
time.sleep(0.1)
st.join(timeout=1)
# All threads should be registered
self.assertEqual(len(st.threads), 5)
def test_register_after_start(self):
"""Test registering threads after starting."""
st = service.ServiceThreads()
thread_list = aprsd_threads.APRSDThreadList()
thread_list.threads_list = []
st.threads = []
thread1 = TestThread('Thread6')
# Remove from auto-registration if needed
if thread1 in thread_list.threads_list:
thread_list.remove(thread1)
st.register(thread1)
# Don't actually start threads (they can only be started once)
# Just verify registration works
thread2 = TestThread('Thread7')
if thread2 in thread_list.threads_list:
thread_list.remove(thread2)
st.register(thread2)
# Both should be registered
self.assertIn(thread1, st.threads)
self.assertIn(thread2, st.threads)
+385
View File
@@ -0,0 +1,385 @@
import time
import unittest
from unittest import mock
from aprsd.packets import tracker
from aprsd.threads import tx
from tests import fake
from tests.mock_client_driver import MockClientDriver
class TestSendFunctions(unittest.TestCase):
"""Unit tests for send functions in tx module."""
def setUp(self):
"""Set up test fixtures."""
# Reset singleton instances
tracker.PacketTrack._instance = None
def tearDown(self):
"""Clean up after tests."""
tracker.PacketTrack._instance = None
@mock.patch('aprsd.threads.tx.collector.PacketCollector')
@mock.patch('aprsd.threads.tx._send_packet')
def test_send_message_packet(self, mock_send_packet, mock_collector):
"""Test send() with MessagePacket."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.enable_sending_ack_packets = True
packet = fake.fake_packet()
tx.send(packet)
mock_collector.return_value.tx.assert_called()
mock_send_packet.assert_called()
@mock.patch('aprsd.threads.tx.collector.PacketCollector')
@mock.patch('aprsd.threads.tx._send_ack')
def test_send_ack_packet(self, mock_send_ack, mock_collector):
"""Test send() with AckPacket."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.enable_sending_ack_packets = True
packet = fake.fake_ack_packet()
tx.send(packet)
mock_collector.return_value.tx.assert_called()
mock_send_ack.assert_called()
@mock.patch('aprsd.threads.tx.collector.PacketCollector')
@mock.patch('aprsd.threads.tx._send_ack')
def test_send_ack_disabled(self, mock_send_ack, mock_collector):
"""Test send() with AckPacket when acks are disabled."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.enable_sending_ack_packets = False
packet = fake.fake_ack_packet()
with mock.patch('aprsd.threads.tx.LOG') as mock_log:
tx.send(packet)
mock_log.info.assert_called()
mock_send_ack.assert_not_called()
@mock.patch('aprsd.threads.tx.SendPacketThread')
def test_send_packet_threaded(self, mock_thread_class):
"""Test _send_packet() with threading."""
packet = fake.fake_packet()
mock_thread = mock.MagicMock()
mock_thread_class.return_value = mock_thread
tx._send_packet(packet, direct=False)
mock_thread_class.assert_called_with(packet=packet)
mock_thread.start.assert_called()
@mock.patch('aprsd.threads.tx._send_direct')
def test_send_packet_direct(self, mock_send_direct):
"""Test _send_packet() with direct send."""
packet = fake.fake_packet()
tx._send_packet(packet, direct=True)
mock_send_direct.assert_called_with(packet, aprs_client=None)
@mock.patch('aprsd.threads.tx.SendAckThread')
def test_send_ack_threaded(self, mock_thread_class):
"""Test _send_ack() with threading."""
packet = fake.fake_ack_packet()
mock_thread = mock.MagicMock()
mock_thread_class.return_value = mock_thread
tx._send_ack(packet, direct=False)
mock_thread_class.assert_called_with(packet=packet)
mock_thread.start.assert_called()
@mock.patch('aprsd.threads.tx._send_direct')
def test_send_ack_direct(self, mock_send_direct):
"""Test _send_ack() with direct send."""
packet = fake.fake_ack_packet()
tx._send_ack(packet, direct=True)
mock_send_direct.assert_called_with(packet, aprs_client=None)
@mock.patch('aprsd.threads.tx.APRSDClient')
@mock.patch('aprsd.threads.tx.packet_log')
def test_send_direct(self, mock_log, mock_client_class):
"""Test _send_direct() function."""
packet = fake.fake_packet()
mock_client = MockClientDriver()
mock_client._send_return = True
mock_client_class.return_value = mock_client
result = tx._send_direct(packet)
self.assertTrue(result)
mock_log.log.assert_called()
@mock.patch('aprsd.threads.tx.APRSDClient')
@mock.patch('aprsd.threads.tx.packet_log')
def test_send_direct_with_client(self, mock_log, mock_client_class):
"""Test _send_direct() with provided client."""
packet = fake.fake_packet()
mock_client = MockClientDriver()
mock_client._send_return = True
result = tx._send_direct(packet, aprs_client=mock_client)
self.assertTrue(result)
mock_client_class.assert_not_called()
@mock.patch('aprsd.threads.tx.APRSDClient')
@mock.patch('aprsd.threads.tx.packet_log')
def test_send_direct_exception(self, mock_log, mock_client_class):
"""Test _send_direct() with exception."""
packet = fake.fake_packet()
mock_client = MockClientDriver()
mock_client._send_side_effect = Exception('Send error')
mock_client_class.return_value = mock_client
with mock.patch('aprsd.threads.tx.LOG') as mock_log_error:
result = tx._send_direct(packet)
self.assertFalse(result)
mock_log_error.error.assert_called()
class TestSendPacketThread(unittest.TestCase):
"""Unit tests for the SendPacketThread class."""
def setUp(self):
"""Set up test fixtures."""
tracker.PacketTrack._instance = None
self.packet = fake.fake_packet(msg_number='123')
self.thread = tx.SendPacketThread(self.packet)
def tearDown(self):
"""Clean up after tests."""
self.thread.stop()
if self.thread.is_alive():
self.thread.join(timeout=1)
tracker.PacketTrack._instance = None
def test_init(self):
"""Test initialization."""
self.assertEqual(self.thread.packet, self.packet)
self.assertIn('TX-', self.thread.name)
self.assertEqual(self.thread.loop_count, 1)
@mock.patch('aprsd.threads.tx.tracker.PacketTrack')
def test_loop_packet_acked(self, mock_tracker_class):
"""Test loop() when packet is acked."""
mock_tracker = mock.MagicMock()
mock_tracker.get.return_value = None # Packet removed = acked
mock_tracker_class.return_value = mock_tracker
with mock.patch('aprsd.threads.tx.LOG') as mock_log:
result = self.thread.loop()
self.assertFalse(result)
mock_log.info.assert_called()
@mock.patch('aprsd.threads.tx.tracker.PacketTrack')
def test_loop_max_retries(self, mock_tracker_class):
"""Test loop() when max retries reached."""
mock_tracker = mock.MagicMock()
tracked_packet = fake.fake_packet(msg_number='123')
tracked_packet.send_count = 3
tracked_packet.retry_count = 3
mock_tracker.get.return_value = tracked_packet
mock_tracker_class.return_value = mock_tracker
with mock.patch('aprsd.threads.tx.LOG') as mock_log:
result = self.thread.loop()
self.assertFalse(result)
mock_log.info.assert_called()
mock_tracker.remove.assert_called()
@mock.patch('aprsd.threads.tx.tracker.PacketTrack')
@mock.patch('aprsd.threads.tx._send_direct')
def test_loop_send_now(self, mock_send_direct, mock_tracker_class):
"""Test loop() when it's time to send."""
mock_tracker = mock.MagicMock()
tracked_packet = fake.fake_packet(msg_number='123')
tracked_packet.send_count = 0
tracked_packet.retry_count = 3
tracked_packet.last_send_time = None
mock_tracker.get.return_value = tracked_packet
mock_tracker_class.return_value = mock_tracker
mock_send_direct.return_value = True
result = self.thread.loop()
self.assertTrue(result)
mock_send_direct.assert_called()
self.assertEqual(tracked_packet.send_count, 1)
@mock.patch('aprsd.threads.tx.tracker.PacketTrack')
@mock.patch('aprsd.threads.tx._send_direct')
def test_loop_send_failed(self, mock_send_direct, mock_tracker_class):
"""Test loop() when send fails."""
mock_tracker = mock.MagicMock()
tracked_packet = fake.fake_packet(msg_number='123')
tracked_packet.send_count = 0
tracked_packet.retry_count = 3
tracked_packet.last_send_time = None
mock_tracker.get.return_value = tracked_packet
mock_tracker_class.return_value = mock_tracker
mock_send_direct.return_value = False
result = self.thread.loop()
self.assertTrue(result)
self.assertEqual(
tracked_packet.send_count, 0
) # Should not increment on failure
class TestSendAckThread(unittest.TestCase):
"""Unit tests for the SendAckThread class."""
def setUp(self):
"""Set up test fixtures."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.default_ack_send_count = 3
self.packet = fake.fake_ack_packet()
self.packet.send_count = 0
self.thread = tx.SendAckThread(self.packet)
def tearDown(self):
"""Clean up after tests."""
self.thread.stop()
if self.thread.is_alive():
self.thread.join(timeout=1)
def test_init(self):
"""Test initialization."""
self.assertEqual(self.thread.packet, self.packet)
self.assertIn('TXAck-', self.thread.name)
self.assertEqual(self.thread.max_retries, 3)
def test_loop_max_retries(self):
"""Test loop() when max retries reached."""
self.packet.send_count = 3
with mock.patch('aprsd.threads.tx.LOG') as mock_log:
result = self.thread.loop()
self.assertFalse(result)
mock_log.debug.assert_called()
@mock.patch('aprsd.threads.tx._send_direct')
def test_loop_send_now(self, mock_send_direct):
"""Test loop() when it's time to send."""
self.packet.last_send_time = None
mock_send_direct.return_value = True
result = self.thread.loop()
self.assertTrue(result)
mock_send_direct.assert_called()
self.assertEqual(self.packet.send_count, 1)
@mock.patch('aprsd.threads.tx._send_direct')
def test_loop_waiting(self, mock_send_direct):
"""Test loop() when waiting for next send."""
self.packet.last_send_time = int(time.time()) - 10 # Too soon
mock_send_direct.return_value = True
result = self.thread.loop()
self.assertTrue(result)
mock_send_direct.assert_not_called()
class TestBeaconSendThread(unittest.TestCase):
"""Unit tests for the BeaconSendThread class."""
def setUp(self):
"""Set up test fixtures."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.latitude = 40.7128
CONF.longitude = -74.0060
CONF.beacon_interval = 10
CONF.beacon_symbol = '>'
CONF.callsign = 'TEST'
def tearDown(self):
"""Clean up after tests."""
pass
def test_init(self):
"""Test initialization."""
thread = tx.BeaconSendThread()
self.assertEqual(thread.name, 'BeaconSendThread')
self.assertEqual(thread._loop_cnt, 1)
def test_init_no_coordinates(self):
"""Test initialization without coordinates."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.latitude = None
CONF.longitude = None
thread = tx.BeaconSendThread()
self.assertTrue(thread.thread_stop)
@mock.patch('aprsd.threads.tx.send')
def test_loop_send_beacon(self, mock_send):
"""Test loop() sends beacon at interval."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.beacon_interval = 1
thread = tx.BeaconSendThread()
thread._loop_cnt = 1
result = thread.loop()
self.assertTrue(result)
mock_send.assert_called()
@mock.patch('aprsd.threads.tx.send')
def test_loop_not_time(self, mock_send):
"""Test loop() doesn't send before interval."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.beacon_interval = 10
thread = tx.BeaconSendThread()
thread._loop_cnt = 5
result = thread.loop()
self.assertTrue(result)
mock_send.assert_not_called()
@mock.patch('aprsd.threads.tx.send')
@mock.patch('aprsd.threads.tx.APRSDClient')
def test_loop_send_exception(self, mock_client_class, mock_send):
"""Test loop() handles send exception."""
from oslo_config import cfg
CONF = cfg.CONF
CONF.beacon_interval = 1
thread = tx.BeaconSendThread()
thread._loop_cnt = 1
mock_send.side_effect = Exception('Send error')
with mock.patch('aprsd.threads.tx.LOG') as mock_log:
result = thread.loop()
self.assertTrue(result)
mock_log.error.assert_called()
mock_client_class.return_value.reset.assert_called()