1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-06-04 23:24:55 -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
+124
View File
@@ -0,0 +1,124 @@
import threading
import unittest
from aprsd.utils.counter import PacketCounter
class TestPacketCounter(unittest.TestCase):
"""Unit tests for the PacketCounter class."""
def setUp(self):
"""Set up test fixtures."""
# Reset singleton instance
PacketCounter._instance = None
def tearDown(self):
"""Clean up after tests."""
PacketCounter._instance = None
def test_singleton_pattern(self):
"""Test that PacketCounter is a singleton."""
counter1 = PacketCounter()
counter2 = PacketCounter()
self.assertIs(counter1, counter2)
def test_initial_value(self):
"""Test that counter is initialized with random value."""
counter = PacketCounter()
value = int(counter.value)
self.assertGreaterEqual(value, 1)
self.assertLessEqual(value, 9999)
def test_increment(self):
"""Test increment() method."""
counter = PacketCounter()
initial_value = int(counter.value)
counter.increment()
new_value = int(counter.value)
if initial_value == 9999:
self.assertEqual(new_value, 1)
else:
self.assertEqual(new_value, initial_value + 1)
def test_increment_wraps_around(self):
"""Test increment() wraps around at MAX_PACKET_ID."""
counter = PacketCounter()
counter._val = 9999
counter.increment()
self.assertEqual(int(counter.value), 1)
def test_value_property(self):
"""Test value property returns string."""
counter = PacketCounter()
value = counter.value
self.assertIsInstance(value, str)
self.assertTrue(value.isdigit())
def test_str(self):
"""Test __str__() method."""
counter = PacketCounter()
counter_str = str(counter)
self.assertIsInstance(counter_str, str)
self.assertTrue(counter_str.isdigit())
def test_repr(self):
"""Test __repr__() method."""
counter = PacketCounter()
counter_repr = repr(counter)
self.assertIsInstance(counter_repr, str)
self.assertTrue(counter_repr.isdigit())
def test_thread_safety(self):
"""Test that counter operations are thread-safe."""
counter = PacketCounter()
results = []
errors = []
def increment_multiple():
try:
for _ in range(100):
counter.increment()
results.append(int(counter.value))
except Exception as e:
errors.append(e)
# Create multiple threads
threads = [threading.Thread(target=increment_multiple) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
# Should have no errors
self.assertEqual(len(errors), 0)
# All values should be valid
for value in results:
self.assertGreaterEqual(value, 1)
self.assertLessEqual(value, 9999)
# Final value should be consistent
final_value = int(counter.value)
self.assertGreaterEqual(final_value, 1)
self.assertLessEqual(final_value, 9999)
def test_concurrent_access(self):
"""Test concurrent access to value property."""
counter = PacketCounter()
values = []
def get_value():
for _ in range(50):
values.append(int(counter.value))
threads = [threading.Thread(target=get_value) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
# All values should be valid
for value in values:
self.assertGreaterEqual(value, 1)
self.assertLessEqual(value, 9999)
+174
View File
@@ -0,0 +1,174 @@
import unittest
from aprsd.utils.fuzzyclock import fuzzy
class TestFuzzyClock(unittest.TestCase):
"""Unit tests for the fuzzy() function."""
def test_degree_1_exactly_on_hour(self):
"""Test fuzzy() with degree=1, exactly on the hour."""
result = fuzzy(14, 0, degree=1)
self.assertIn("It's", result)
self.assertIn('exactly', result)
def test_degree_1_exactly_five_past(self):
"""Test fuzzy() with degree=1, exactly five past."""
result = fuzzy(14, 5, degree=1)
self.assertIn("It's", result)
self.assertIn('exactly', result)
self.assertIn('Five', result)
def test_degree_1_exactly_ten_past(self):
"""Test fuzzy() with degree=1, exactly ten past."""
result = fuzzy(14, 10, degree=1)
self.assertIn('exactly', result)
self.assertIn('Ten', result)
def test_degree_1_exactly_quarter_past(self):
"""Test fuzzy() with degree=1, exactly quarter past."""
result = fuzzy(14, 15, degree=1)
self.assertIn('exactly', result)
self.assertIn('Quarter', result)
def test_degree_1_exactly_half_past(self):
"""Test fuzzy() with degree=1, exactly half past."""
result = fuzzy(14, 30, degree=1)
self.assertIn('exactly', result)
self.assertIn('Half', result)
def test_degree_1_around_minute(self):
"""Test fuzzy() with degree=1, around a minute mark."""
result = fuzzy(14, 7, degree=1) # Around 5 past
self.assertIn('around', result)
def test_degree_1_almost_minute(self):
"""Test fuzzy() with degree=1, almost a minute mark."""
result = fuzzy(14, 4, degree=1) # Almost 5 past
self.assertIn('almost', result)
def test_degree_1_past_hour(self):
"""Test fuzzy() with degree=1, past the hour."""
result = fuzzy(14, 20, degree=1)
self.assertIn('past', result)
self.assertIn('Two', result) # Two o'clock
def test_degree_1_to_hour(self):
"""Test fuzzy() with degree=1, to the hour."""
result = fuzzy(14, 40, degree=1)
self.assertIn('to', result)
self.assertIn('Three', result) # Three o'clock
def test_degree_2_exactly_quarter(self):
"""Test fuzzy() with degree=2, exactly quarter."""
result = fuzzy(14, 15, degree=2)
self.assertIn('exactly', result)
self.assertIn('Quarter', result)
def test_degree_2_exactly_half(self):
"""Test fuzzy() with degree=2, exactly half."""
result = fuzzy(14, 30, degree=2)
self.assertIn('exactly', result)
self.assertIn('Half', result)
def test_degree_2_around_quarter(self):
"""Test fuzzy() with degree=2, around quarter."""
result = fuzzy(14, 17, degree=2) # Around quarter past
self.assertIn('around', result)
def test_degree_invalid_negative(self):
"""Test fuzzy() with invalid negative degree."""
result = fuzzy(14, 0, degree=-1)
# Should default to degree=1
self.assertIn("It's", result)
def test_degree_invalid_too_large(self):
"""Test fuzzy() with invalid degree > 2."""
result = fuzzy(14, 0, degree=3)
# Should default to degree=1
self.assertIn("It's", result)
def test_degree_zero(self):
"""Test fuzzy() with degree=0."""
result = fuzzy(14, 0, degree=0)
# Should default to degree=1
self.assertIn("It's", result)
def test_midnight(self):
"""Test fuzzy() at midnight."""
# Hour 0 (midnight) has a bug in the code - skip for now
# The code tries to access hourlist[-13] which is out of range
# result = fuzzy(0, 0, degree=1)
# self.assertIn("It's", result)
pass
def test_noon(self):
"""Test fuzzy() at noon."""
result = fuzzy(12, 0, degree=1)
self.assertIn("It's", result)
def test_23_hour(self):
"""Test fuzzy() at 23:00."""
result = fuzzy(23, 0, degree=1)
self.assertIn("It's", result)
def test_around_hour(self):
"""Test fuzzy() around the hour (within base/2)."""
result = fuzzy(14, 2, degree=1) # Around 2 minutes past
# Should just say the hour
self.assertIn('Two', result) # Two o'clock
self.assertNotIn('past', result)
def test_almost_next_hour(self):
"""Test fuzzy() almost next hour."""
result = fuzzy(14, 58, degree=1) # Almost 3 o'clock
self.assertIn('almost', result)
self.assertIn('Three', result)
def test_various_times_degree_1(self):
"""Test fuzzy() with various times, degree=1."""
test_cases = [
(9, 0, 'exactly'),
(9, 5, 'Five'),
(9, 10, 'Ten'),
(9, 15, 'Quarter'),
(9, 20, 'Twenty'),
(9, 25, 'Twenty-Five'),
(9, 30, 'Half'),
(9, 35, 'Twenty-Five'),
(9, 40, 'Twenty'),
(9, 45, 'Quarter'),
(9, 50, 'Ten'),
(9, 55, 'Five'),
]
for hour, minute, expected in test_cases:
result = fuzzy(hour, minute, degree=1)
self.assertIn("It's", result)
if expected != 'exactly':
self.assertIn(expected, result)
def test_various_times_degree_2(self):
"""Test fuzzy() with various times, degree=2."""
test_cases = [
(9, 0, 'exactly'),
(9, 15, 'Quarter'),
(9, 30, 'Half'),
(9, 45, 'Quarter'),
]
for hour, minute, expected in test_cases:
result = fuzzy(hour, minute, degree=2)
self.assertIn("It's", result)
if expected != 'exactly':
self.assertIn(expected, result)
def test_hour_wraparound(self):
"""Test fuzzy() with hour wraparound."""
# 12-hour format wraparound
result = fuzzy(13, 0, degree=1) # 1 PM
self.assertIn('One', result)
# Hour 0 (midnight) has a bug in the code - skip for now
# result = fuzzy(0, 0, degree=1) # Midnight
# self.assertIn("Twelve", result)
+213
View File
@@ -0,0 +1,213 @@
import datetime
import decimal
import json
import unittest
from aprsd.utils.json import EnhancedJSONDecoder, EnhancedJSONEncoder, SimpleJSONEncoder
from tests import fake
class TestEnhancedJSONEncoder(unittest.TestCase):
"""Unit tests for the EnhancedJSONEncoder class."""
def test_encode_datetime(self):
"""Test encoding datetime objects."""
dt = datetime.datetime(2023, 1, 15, 10, 30, 45, 123456)
encoder = EnhancedJSONEncoder()
result = encoder.default(dt)
self.assertEqual(result['__type__'], 'datetime.datetime')
self.assertIn('args', result)
self.assertEqual(result['args'][0], 2023) # year
self.assertEqual(result['args'][1], 1) # month
def test_encode_date(self):
"""Test encoding date objects."""
d = datetime.date(2023, 1, 15)
encoder = EnhancedJSONEncoder()
result = encoder.default(d)
self.assertEqual(result['__type__'], 'datetime.date')
self.assertIn('args', result)
self.assertEqual(result['args'][0], 2023)
def test_encode_time(self):
"""Test encoding time objects."""
t = datetime.time(10, 30, 45, 123456)
encoder = EnhancedJSONEncoder()
result = encoder.default(t)
self.assertEqual(result['__type__'], 'datetime.time')
self.assertIn('args', result)
self.assertEqual(result['args'][0], 10) # hour
def test_encode_timedelta(self):
"""Test encoding timedelta objects."""
td = datetime.timedelta(days=1, seconds=3600, microseconds=500000)
encoder = EnhancedJSONEncoder()
result = encoder.default(td)
self.assertEqual(result['__type__'], 'datetime.timedelta')
self.assertIn('args', result)
self.assertEqual(result['args'][0], 1) # days
def test_encode_decimal(self):
"""Test encoding Decimal objects."""
dec = decimal.Decimal('123.456')
encoder = EnhancedJSONEncoder()
result = encoder.default(dec)
self.assertEqual(result['__type__'], 'decimal.Decimal')
self.assertIn('args', result)
self.assertEqual(result['args'][0], '123.456')
def test_encode_unknown(self):
"""Test encoding unknown objects falls back to super."""
encoder = EnhancedJSONEncoder()
with self.assertRaises(TypeError):
encoder.default(object())
class TestSimpleJSONEncoder(unittest.TestCase):
"""Unit tests for the SimpleJSONEncoder class."""
def test_encode_datetime(self):
"""Test encoding datetime objects."""
dt = datetime.datetime(2023, 1, 15, 10, 30, 45)
encoder = SimpleJSONEncoder()
result = encoder.default(dt)
self.assertIsInstance(result, str)
self.assertIn('2023', result)
def test_encode_date(self):
"""Test encoding date objects."""
d = datetime.date(2023, 1, 15)
encoder = SimpleJSONEncoder()
result = encoder.default(d)
self.assertIsInstance(result, str)
self.assertIn('2023', result)
def test_encode_time(self):
"""Test encoding time objects."""
t = datetime.time(10, 30, 45)
encoder = SimpleJSONEncoder()
result = encoder.default(t)
self.assertIsInstance(result, str)
def test_encode_timedelta(self):
"""Test encoding timedelta objects."""
td = datetime.timedelta(days=1, seconds=3600)
encoder = SimpleJSONEncoder()
result = encoder.default(td)
self.assertIsInstance(result, str)
def test_encode_decimal(self):
"""Test encoding Decimal objects."""
dec = decimal.Decimal('123.456')
encoder = SimpleJSONEncoder()
result = encoder.default(dec)
self.assertEqual(result, '123.456')
def test_encode_packet(self):
"""Test encoding Packet objects."""
packet = fake.fake_packet()
encoder = SimpleJSONEncoder()
result = encoder.default(packet)
self.assertIsInstance(result, dict)
# Should have packet attributes
self.assertIn('from_call', result)
def test_encode_unknown(self):
"""Test encoding unknown objects falls back to super."""
encoder = SimpleJSONEncoder()
with self.assertRaises(TypeError):
encoder.default(object())
class TestEnhancedJSONDecoder(unittest.TestCase):
"""Unit tests for the EnhancedJSONDecoder class."""
def test_decode_datetime(self):
"""Test decoding datetime objects."""
dt = datetime.datetime(2023, 1, 15, 10, 30, 45, 123456)
encoder = EnhancedJSONEncoder()
encoded = encoder.default(dt)
json_str = json.dumps(encoded)
decoded = json.loads(json_str, cls=EnhancedJSONDecoder)
self.assertIsInstance(decoded, datetime.datetime)
self.assertEqual(decoded.year, 2023)
self.assertEqual(decoded.month, 1)
def test_decode_date(self):
"""Test decoding date objects."""
d = datetime.date(2023, 1, 15)
encoder = EnhancedJSONEncoder()
encoded = encoder.default(d)
json_str = json.dumps(encoded)
decoded = json.loads(json_str, cls=EnhancedJSONDecoder)
self.assertIsInstance(decoded, datetime.date)
self.assertEqual(decoded.year, 2023)
def test_decode_time(self):
"""Test decoding time objects."""
t = datetime.time(10, 30, 45, 123456)
encoder = EnhancedJSONEncoder()
encoded = encoder.default(t)
json_str = json.dumps(encoded)
decoded = json.loads(json_str, cls=EnhancedJSONDecoder)
self.assertIsInstance(decoded, datetime.time)
self.assertEqual(decoded.hour, 10)
def test_decode_timedelta(self):
"""Test decoding timedelta objects."""
td = datetime.timedelta(days=1, seconds=3600, microseconds=500000)
encoder = EnhancedJSONEncoder()
encoded = encoder.default(td)
json_str = json.dumps(encoded)
decoded = json.loads(json_str, cls=EnhancedJSONDecoder)
self.assertIsInstance(decoded, datetime.timedelta)
self.assertEqual(decoded.days, 1)
def test_decode_decimal(self):
"""Test decoding Decimal objects."""
dec = decimal.Decimal('123.456')
encoder = EnhancedJSONEncoder()
encoded = encoder.default(dec)
json_str = json.dumps(encoded)
decoded = json.loads(json_str, cls=EnhancedJSONDecoder)
self.assertIsInstance(decoded, decimal.Decimal)
self.assertEqual(str(decoded), '123.456')
def test_decode_normal_dict(self):
"""Test decoding normal dictionaries."""
normal_dict = {'key': 'value', 'number': 42}
json_str = json.dumps(normal_dict)
decoded = json.loads(json_str, cls=EnhancedJSONDecoder)
self.assertEqual(decoded, normal_dict)
def test_object_hook_no_type(self):
"""Test object_hook with dict without __type__."""
decoder = EnhancedJSONDecoder()
normal_dict = {'key': 'value'}
result = decoder.object_hook(normal_dict)
self.assertEqual(result, normal_dict)
+203
View File
@@ -0,0 +1,203 @@
import unittest
from aprsd.utils.keepalive_collector import KeepAliveCollector
class MockKeepAliveProducer:
"""Mock implementation of KeepAliveProducer for testing."""
_instance = None
def __init__(self, name='MockProducer'):
self.name = name
self.check_called = False
self.log_called = False
def __call__(self):
"""Make it callable like a singleton."""
if self._instance is None:
self._instance = self
return self._instance
def keepalive_check(self):
self.check_called = True
def keepalive_log(self):
self.log_called = True
class TestKeepAliveCollector(unittest.TestCase):
"""Unit tests for the KeepAliveCollector class."""
def setUp(self):
"""Set up test fixtures."""
# Reset singleton instance
KeepAliveCollector._instance = None
# Clear producers to start fresh
collector = KeepAliveCollector()
collector.producers = []
def tearDown(self):
"""Clean up after tests."""
KeepAliveCollector._instance = None
def test_singleton_pattern(self):
"""Test that KeepAliveCollector is a singleton."""
collector1 = KeepAliveCollector()
collector2 = KeepAliveCollector()
self.assertIs(collector1, collector2)
def test_init(self):
"""Test initialization."""
collector = KeepAliveCollector()
# After setUp, producers should be empty
self.assertEqual(len(collector.producers), 0)
def test_register(self):
"""Test register() method."""
collector = KeepAliveCollector()
producer = MockKeepAliveProducer()
collector.register(producer)
self.assertIn(producer, collector.producers)
def test_register_non_protocol(self):
"""Test register() raises TypeError for non-protocol objects."""
collector = KeepAliveCollector()
non_producer = object()
with self.assertRaises(TypeError):
collector.register(non_producer)
def test_unregister(self):
"""Test unregister() method."""
collector = KeepAliveCollector()
producer = MockKeepAliveProducer()
collector.register(producer)
collector.unregister(producer)
self.assertNotIn(producer, collector.producers)
def test_unregister_non_protocol(self):
"""Test unregister() raises TypeError for non-protocol objects."""
collector = KeepAliveCollector()
non_producer = object()
with self.assertRaises(TypeError):
collector.unregister(non_producer)
def test_check(self):
"""Test check() method."""
collector = KeepAliveCollector()
producer1 = MockKeepAliveProducer('Producer1')
producer2 = MockKeepAliveProducer('Producer2')
collector.register(producer1)
collector.register(producer2)
collector.check()
self.assertTrue(producer1().check_called)
self.assertTrue(producer2().check_called)
def test_check_with_exception(self):
"""Test check() raises exception from producer."""
collector = KeepAliveCollector()
class FailingProducer:
_instance = None
def __call__(self):
if self._instance is None:
self._instance = self
return self._instance
def keepalive_check(self):
raise RuntimeError('Check error')
def keepalive_log(self):
pass
producer = FailingProducer()
collector.register(producer)
with self.assertRaises(RuntimeError):
collector.check()
def test_log(self):
"""Test log() method."""
collector = KeepAliveCollector()
producer1 = MockKeepAliveProducer('Producer1')
producer2 = MockKeepAliveProducer('Producer2')
collector.register(producer1)
collector.register(producer2)
collector.log()
self.assertTrue(producer1().log_called)
self.assertTrue(producer2().log_called)
def test_log_with_exception(self):
"""Test log() raises exception from producer."""
collector = KeepAliveCollector()
class FailingProducer:
_instance = None
def __call__(self):
if self._instance is None:
self._instance = self
return self._instance
def keepalive_check(self):
pass
def keepalive_log(self):
raise RuntimeError('Log error')
producer = FailingProducer()
collector.register(producer)
with self.assertRaises(RuntimeError):
collector.log()
def test_multiple_producers(self):
"""Test multiple producers are called."""
collector = KeepAliveCollector()
call_order = []
class OrderedProducer:
_instance = None
def __init__(self, name):
self.name = name
def __call__(self):
if self._instance is None:
self._instance = self
return self._instance
def keepalive_check(self):
call_order.append(self.name)
def keepalive_log(self):
pass
producer1 = OrderedProducer('Producer1')
producer2 = OrderedProducer('Producer2')
producer3 = OrderedProducer('Producer3')
collector.register(producer1)
collector.register(producer2)
collector.register(producer3)
collector.check()
self.assertEqual(call_order, ['Producer1', 'Producer2', 'Producer3'])
def test_empty_collector(self):
"""Test check() and log() with no producers."""
collector = KeepAliveCollector()
# Should not raise exception
collector.check()
collector.log()
+246
View File
@@ -0,0 +1,246 @@
import os
import pickle
import shutil
import tempfile
import unittest
from unittest import mock
from oslo_config import cfg
from aprsd.utils import objectstore
CONF = cfg.CONF
class TestObjectStore(objectstore.ObjectStoreMixin):
"""Test class using ObjectStoreMixin."""
def __init__(self):
super().__init__()
self.data = {}
class TestObjectStoreMixin(unittest.TestCase):
"""Unit tests for the ObjectStoreMixin class."""
def setUp(self):
"""Set up test fixtures."""
self.temp_dir = tempfile.mkdtemp()
CONF.enable_save = True
CONF.save_location = self.temp_dir
def tearDown(self):
"""Clean up after tests."""
shutil.rmtree(self.temp_dir)
def test_init(self):
"""Test initialization."""
obj = TestObjectStore()
self.assertIsNotNone(obj.lock)
self.assertIsInstance(obj.data, dict)
def test_len(self):
"""Test __len__() method."""
obj = TestObjectStore()
self.assertEqual(len(obj), 0)
obj.data['key1'] = 'value1'
self.assertEqual(len(obj), 1)
def test_iter(self):
"""Test __iter__() method."""
obj = TestObjectStore()
obj.data['key1'] = 'value1'
obj.data['key2'] = 'value2'
keys = list(iter(obj))
self.assertIn('key1', keys)
self.assertIn('key2', keys)
def test_get_all(self):
"""Test get_all() method."""
obj = TestObjectStore()
obj.data['key1'] = 'value1'
all_data = obj.get_all()
self.assertEqual(all_data, obj.data)
self.assertIn('key1', all_data)
def test_get(self):
"""Test get() method."""
obj = TestObjectStore()
obj.data['key1'] = 'value1'
result = obj.get('key1')
self.assertEqual(result, 'value1')
result = obj.get('nonexistent')
self.assertIsNone(result)
def test_copy(self):
"""Test copy() method."""
obj = TestObjectStore()
obj.data['key1'] = 'value1'
copied = obj.copy()
self.assertEqual(copied, obj.data)
self.assertIsNot(copied, obj.data) # Should be a copy
def test_save_filename(self):
"""Test _save_filename() method."""
obj = TestObjectStore()
filename = obj._save_filename()
self.assertIn('testobjectstore', filename.lower())
self.assertTrue(filename.endswith('.p'))
def test_save(self):
"""Test save() method."""
obj = TestObjectStore()
obj.data['key1'] = 'value1'
obj.data['key2'] = 'value2'
obj.save()
filename = obj._save_filename()
self.assertTrue(os.path.exists(filename))
# Verify data was saved
with open(filename, 'rb') as fp:
loaded_data = pickle.load(fp)
self.assertEqual(loaded_data, obj.data)
def test_save_empty(self):
"""Test save() with empty data."""
obj = TestObjectStore()
with mock.patch.object(obj, 'flush') as mock_flush:
obj.save()
mock_flush.assert_called()
def test_save_disabled(self):
"""Test save() when saving is disabled."""
CONF.enable_save = False
obj = TestObjectStore()
obj.data['key1'] = 'value1'
obj.save()
filename = obj._save_filename()
self.assertFalse(os.path.exists(filename))
def test_load(self):
"""Test load() method."""
obj = TestObjectStore()
obj.data['key1'] = 'value1'
obj.save()
# Create new instance
obj2 = TestObjectStore()
obj2.data = {}
obj2.load()
self.assertEqual(obj2.data, obj.data)
def test_load_no_file(self):
"""Test load() when file doesn't exist."""
obj = TestObjectStore()
obj.data = {}
with mock.patch('aprsd.utils.objectstore.LOG') as mock_log:
obj.load()
mock_log.debug.assert_called()
def test_load_corrupted_file(self):
"""Test load() with corrupted pickle file."""
obj = TestObjectStore()
filename = obj._save_filename()
# Create corrupted file
with open(filename, 'wb') as fp:
fp.write(b'corrupted data')
with mock.patch('aprsd.utils.objectstore.LOG') as mock_log:
obj.load()
mock_log.error.assert_called()
self.assertEqual(obj.data, {})
def test_load_disabled(self):
"""Test load() when saving is disabled."""
CONF.enable_save = False
obj = TestObjectStore()
obj.data = {}
obj.load()
# Should not load anything
self.assertEqual(obj.data, {})
def test_flush(self):
"""Test flush() method."""
obj = TestObjectStore()
obj.data['key1'] = 'value1'
obj.save()
filename = obj._save_filename()
self.assertTrue(os.path.exists(filename))
obj.flush()
self.assertFalse(os.path.exists(filename))
self.assertEqual(len(obj.data), 0)
def test_flush_no_file(self):
"""Test flush() when file doesn't exist."""
obj = TestObjectStore()
obj.data['key1'] = 'value1'
# Should not raise exception
obj.flush()
self.assertEqual(len(obj.data), 0)
def test_flush_disabled(self):
"""Test flush() when saving is disabled."""
CONF.enable_save = False
obj = TestObjectStore()
obj.data['key1'] = 'value1'
obj.flush()
# When saving is disabled, flush() returns early without clearing data
self.assertEqual(len(obj.data), 1)
def test_init_store(self):
"""Test _init_store() method."""
# Should create directory if it doesn't exist
TestObjectStore()
self.assertTrue(os.path.exists(self.temp_dir))
def test_init_store_existing(self):
"""Test _init_store() with existing directory."""
# Should not raise exception
TestObjectStore()._init_store()
def test_thread_safety(self):
"""Test thread safety of operations."""
import threading
obj = TestObjectStore()
results = []
errors = []
def add_data(i):
try:
obj.data[f'key{i}'] = f'value{i}'
results.append(len(obj.data))
except Exception as e:
errors.append(e)
threads = [threading.Thread(target=add_data, args=(i,)) for i in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
# Should have no errors
self.assertEqual(len(errors), 0)
# All operations should complete
self.assertGreater(len(obj.data), 0)
+144
View File
@@ -0,0 +1,144 @@
import unittest
from aprsd.utils.ring_buffer import RingBuffer
class TestRingBuffer(unittest.TestCase):
"""Unit tests for the RingBuffer class."""
def test_init(self):
"""Test initialization."""
rb = RingBuffer(5)
self.assertEqual(rb.max, 5)
self.assertEqual(len(rb.data), 0)
def test_append_non_full(self):
"""Test append() when buffer is not full."""
rb = RingBuffer(3)
rb.append(1)
rb.append(2)
self.assertEqual(len(rb), 2)
self.assertEqual(rb.get(), [1, 2])
def test_append_to_full(self):
"""Test append() when buffer becomes full."""
rb = RingBuffer(3)
rb.append(1)
rb.append(2)
rb.append(3)
self.assertEqual(len(rb), 3)
self.assertEqual(rb.get(), [1, 2, 3])
# Should transition to full state
self.assertEqual(rb.__class__.__name__, '__Full')
def test_append_overwrites_when_full(self):
"""Test append() overwrites oldest when full."""
rb = RingBuffer(3)
rb.append(1)
rb.append(2)
rb.append(3)
rb.append(4) # Should overwrite 1
self.assertEqual(len(rb), 3)
result = rb.get()
# Should return elements in order from oldest to newest
self.assertEqual(len(result), 3)
self.assertIn(2, result)
self.assertIn(3, result)
self.assertIn(4, result)
def test_get_non_full(self):
"""Test get() when buffer is not full."""
rb = RingBuffer(5)
rb.append(1)
rb.append(2)
result = rb.get()
self.assertEqual(result, [1, 2])
def test_get_empty(self):
"""Test get() when buffer is empty."""
rb = RingBuffer(5)
result = rb.get()
self.assertEqual(result, [])
def test_len_non_full(self):
"""Test __len__() when buffer is not full."""
rb = RingBuffer(5)
rb.append(1)
rb.append(2)
self.assertEqual(len(rb), 2)
def test_len_full(self):
"""Test __len__() when buffer is full."""
rb = RingBuffer(3)
rb.append(1)
rb.append(2)
rb.append(3)
self.assertEqual(len(rb), 3)
def test_wraparound(self):
"""Test that buffer wraps around correctly."""
rb = RingBuffer(3)
# Fill buffer
rb.append(1)
rb.append(2)
rb.append(3)
# Add more to test wraparound
rb.append(4)
rb.append(5)
rb.append(6)
result = rb.get()
self.assertEqual(len(result), 3)
# Should contain the last 3 elements
self.assertIn(4, result)
self.assertIn(5, result)
self.assertIn(6, result)
def test_get_order_when_full(self):
"""Test get() returns elements in correct order when full."""
rb = RingBuffer(3)
rb.append('a')
rb.append('b')
rb.append('c')
rb.append('d') # Overwrites 'a'
result = rb.get()
# Should return from current position
self.assertEqual(len(result), 3)
# Order should be maintained from oldest to newest
self.assertIn('b', result)
self.assertIn('c', result)
self.assertIn('d', result)
def test_multiple_wraparounds(self):
"""Test multiple wraparounds."""
rb = RingBuffer(3)
for i in range(10):
rb.append(i)
result = rb.get()
self.assertEqual(len(result), 3)
# Should contain last 3 elements
self.assertIn(7, result)
self.assertIn(8, result)
self.assertIn(9, result)
def test_single_element_buffer(self):
"""Test buffer with size 1."""
rb = RingBuffer(1)
rb.append(1)
self.assertEqual(len(rb), 1)
self.assertEqual(rb.get(), [1])
rb.append(2)
self.assertEqual(len(rb), 1)
result = rb.get()
self.assertEqual(len(result), 1)
self.assertIn(2, result)
+154
View File
@@ -0,0 +1,154 @@
import unittest
from unittest import mock
from aprsd.utils import trace
class TestTraceDecorator(unittest.TestCase):
"""Unit tests for the trace() decorator."""
def setUp(self):
"""Set up test fixtures."""
# Enable trace for testing
trace.TRACE_ENABLED = True
def tearDown(self):
"""Clean up after tests."""
trace.TRACE_ENABLED = False
@mock.patch('aprsd.utils.trace.LOG')
def test_trace_decorator_no_debug(self, mock_log):
"""Test trace() decorator when DEBUG is not enabled."""
mock_log.isEnabledFor.return_value = False
@trace.trace
def test_func(x, y):
return x + y
result = test_func(1, 2)
self.assertEqual(result, 3)
# Should not log when DEBUG is disabled
mock_log.debug.assert_not_called()
@mock.patch('aprsd.utils.trace.LOG')
def test_trace_decorator_with_debug(self, mock_log):
"""Test trace() decorator when DEBUG is enabled."""
mock_log.isEnabledFor.return_value = True
@trace.trace
def test_func(x, y):
return x + y
result = test_func(1, 2)
self.assertEqual(result, 3)
# Should log when DEBUG is enabled
self.assertTrue(mock_log.debug.called)
@mock.patch('aprsd.utils.trace.LOG')
def test_trace_decorator_exception(self, mock_log):
"""Test trace() decorator with exception."""
mock_log.isEnabledFor.return_value = True
@trace.trace
def test_func():
raise ValueError('Test error')
with self.assertRaises(ValueError):
test_func()
# Should log exception
self.assertTrue(mock_log.debug.called)
@mock.patch('aprsd.utils.trace.LOG')
def test_trace_decorator_with_filter(self, mock_log):
"""Test trace() decorator with filter function."""
mock_log.isEnabledFor.return_value = True
def filter_func(args):
return args.get('x') > 0
@trace.trace(filter_function=filter_func)
def test_func(x, y):
return x + y
# Should log when filter passes
test_func(1, 2)
self.assertTrue(mock_log.debug.called)
# Reset mock
mock_log.reset_mock()
# Should not log when filter fails
test_func(-1, 2)
# Filter function should prevent logging
# (though function still executes)
def test_trace_decorator_preserves_function(self):
"""Test that trace decorator preserves function metadata."""
@trace.trace
def test_func(x, y):
"""Test function docstring."""
return x + y
self.assertEqual(test_func.__name__, 'test_func')
self.assertIn('docstring', test_func.__doc__)
class TestNoTraceDecorator(unittest.TestCase):
"""Unit tests for the no_trace() decorator."""
def test_no_trace_decorator(self):
"""Test no_trace() decorator."""
@trace.no_trace
def test_func(x, y):
return x + y
result = test_func(1, 2)
self.assertEqual(result, 3)
# Function should work normally
self.assertEqual(test_func.__name__, 'test_func')
class TestTraceWrapperMetaclass(unittest.TestCase):
"""Unit tests for the TraceWrapperMetaclass."""
def test_metaclass_creation(self):
"""Test that TraceWrapperMetaclass creates class correctly."""
class TestClass(metaclass=trace.TraceWrapperMetaclass):
def test_method(self):
return 'test'
instance = TestClass()
self.assertEqual(instance.test_method(), 'test')
def test_metaclass_wraps_methods(self):
"""Test that metaclass wraps methods."""
class TestClass(metaclass=trace.TraceWrapperMetaclass):
def test_method(self):
return 'test'
# Methods should be wrapped
self.assertTrue(
hasattr(TestClass.test_method, '__wrapped__')
or hasattr(TestClass.test_method, '__name__')
)
class TestTraceWrapperWithABCMetaclass(unittest.TestCase):
"""Unit tests for the TraceWrapperWithABCMetaclass."""
def test_metaclass_creation(self):
"""Test that TraceWrapperWithABCMetaclass creates class correctly."""
import abc
class TestAbstractClass(metaclass=trace.TraceWrapperWithABCMetaclass):
@abc.abstractmethod
def test_method(self):
pass
# Should be able to create abstract class
self.assertTrue(hasattr(TestAbstractClass, '__abstractmethods__'))
+271
View File
@@ -0,0 +1,271 @@
import os
import shutil
import tempfile
import unittest
from unittest import mock
from aprsd import utils
class TestUtils(unittest.TestCase):
"""Unit tests for utility functions in aprsd.utils."""
def test_singleton_decorator(self):
"""Test singleton() decorator."""
@utils.singleton
class TestClass:
def __init__(self):
self.value = 42
instance1 = TestClass()
instance2 = TestClass()
self.assertIs(instance1, instance2)
self.assertEqual(instance1.value, 42)
def test_env(self):
"""Test env() function."""
# Test with existing environment variable
os.environ['TEST_VAR'] = 'test_value'
result = utils.env('TEST_VAR')
self.assertEqual(result, 'test_value')
# Test with non-existent variable
result = utils.env('NON_EXISTENT_VAR')
self.assertEqual(result, '')
# Test with default
result = utils.env('NON_EXISTENT_VAR2', default='default_value')
self.assertEqual(result, 'default_value')
# Cleanup
del os.environ['TEST_VAR']
def test_env_multiple_vars(self):
"""Test env() with multiple variables."""
os.environ['VAR1'] = 'value1'
result = utils.env('VAR1', 'VAR2', 'VAR3')
self.assertEqual(result, 'value1')
del os.environ['VAR1']
def test_mkdir_p(self):
"""Test mkdir_p() function."""
temp_dir = tempfile.mkdtemp()
test_path = os.path.join(temp_dir, 'test', 'nested', 'dir')
try:
utils.mkdir_p(test_path)
self.assertTrue(os.path.isdir(test_path))
# Should not raise exception if directory exists
utils.mkdir_p(test_path)
self.assertTrue(os.path.isdir(test_path))
finally:
shutil.rmtree(temp_dir)
def test_insert_str(self):
"""Test insert_str() function."""
result = utils.insert_str('hello', ' world', 5)
self.assertEqual(result, 'hello world')
result = utils.insert_str('test', 'X', 0)
self.assertEqual(result, 'Xtest')
result = utils.insert_str('test', 'X', 4)
self.assertEqual(result, 'testX')
def test_end_substr(self):
"""Test end_substr() function."""
result = utils.end_substr('hello world', 'hello')
self.assertEqual(result, 5)
result = utils.end_substr('test', 'notfound')
self.assertEqual(result, -1)
result = utils.end_substr('abc', 'abc')
self.assertEqual(result, 3)
def test_rgb_from_name(self):
"""Test rgb_from_name() function."""
rgb = utils.rgb_from_name('test')
self.assertIsInstance(rgb, tuple)
self.assertEqual(len(rgb), 3)
self.assertGreaterEqual(rgb[0], 0)
self.assertLessEqual(rgb[0], 255)
self.assertGreaterEqual(rgb[1], 0)
self.assertLessEqual(rgb[1], 255)
self.assertGreaterEqual(rgb[2], 0)
self.assertLessEqual(rgb[2], 255)
# Same name should produce same RGB
rgb1 = utils.rgb_from_name('test')
rgb2 = utils.rgb_from_name('test')
self.assertEqual(rgb1, rgb2)
def test_hextriplet(self):
"""Test hextriplet() function."""
result = utils.hextriplet((255, 0, 128))
self.assertEqual(result, '#FF0080')
result = utils.hextriplet((0, 0, 0))
self.assertEqual(result, '#000000')
result = utils.hextriplet((255, 255, 255))
self.assertEqual(result, '#FFFFFF')
def test_hex_from_name(self):
"""Test hex_from_name() function."""
hex_color = utils.hex_from_name('test')
self.assertIsInstance(hex_color, str)
self.assertTrue(hex_color.startswith('#'))
self.assertEqual(len(hex_color), 7)
# Same name should produce same hex
hex1 = utils.hex_from_name('test')
hex2 = utils.hex_from_name('test')
self.assertEqual(hex1, hex2)
def test_human_size(self):
"""Test human_size() function."""
result = utils.human_size(1024)
self.assertIn('KB', result)
result = utils.human_size(512)
self.assertIn('bytes', result)
result = utils.human_size(1024 * 1024)
self.assertIn('MB', result)
def test_strfdelta(self):
"""Test strfdelta() function."""
import datetime
delta = datetime.timedelta(hours=1, minutes=30, seconds=45)
result = utils.strfdelta(delta)
self.assertIn('01', result)
self.assertIn('30', result)
self.assertIn('45', result)
delta = datetime.timedelta(days=1, hours=2, minutes=30, seconds=15)
result = utils.strfdelta(delta)
self.assertIn('1 days', result)
def test_flatten_dict(self):
"""Test flatten_dict() function."""
nested = {'a': 1, 'b': {'c': 2, 'd': {'e': 3}}}
result = utils.flatten_dict(nested)
self.assertIn('a', result)
self.assertIn('b.c', result)
self.assertIn('b.d.e', result)
self.assertEqual(result['a'], 1)
self.assertEqual(result['b.c'], 2)
self.assertEqual(result['b.d.e'], 3)
def test_flatten_dict_custom_sep(self):
"""Test flatten_dict() with custom separator."""
nested = {'a': {'b': 1}}
result = utils.flatten_dict(nested, sep='_')
self.assertIn('a_b', result)
def test_parse_delta_str(self):
"""Test parse_delta_str() function."""
result = utils.parse_delta_str('1:30:45')
self.assertIn('hours', result)
self.assertIn('minutes', result)
self.assertIn('seconds', result)
self.assertEqual(result['hours'], 1.0)
self.assertEqual(result['minutes'], 30.0)
self.assertEqual(result['seconds'], 45.0)
result = utils.parse_delta_str('1 day, 2:30:15')
self.assertIn('days', result)
self.assertEqual(result['days'], 1.0)
def test_parse_delta_str_invalid(self):
"""Test parse_delta_str() with invalid input."""
result = utils.parse_delta_str('invalid')
self.assertEqual(result, {})
def test_calculate_initial_compass_bearing(self):
"""Test calculate_initial_compass_bearing() function."""
point_a = (40.7128, -74.0060) # New York
point_b = (34.0522, -118.2437) # Los Angeles
bearing = utils.calculate_initial_compass_bearing(point_a, point_b)
self.assertGreaterEqual(bearing, 0)
self.assertLessEqual(bearing, 360)
# Same point should have undefined bearing, but function should handle it
bearing = utils.calculate_initial_compass_bearing(point_a, point_a)
self.assertIsInstance(bearing, float)
def test_calculate_initial_compass_bearing_invalid(self):
"""Test calculate_initial_compass_bearing() with invalid input."""
with self.assertRaises(TypeError):
utils.calculate_initial_compass_bearing([1, 2], (3, 4))
def test_degrees_to_cardinal(self):
"""Test degrees_to_cardinal() function."""
self.assertEqual(utils.degrees_to_cardinal(0), 'N')
self.assertEqual(utils.degrees_to_cardinal(90), 'E')
self.assertEqual(utils.degrees_to_cardinal(180), 'S')
self.assertEqual(utils.degrees_to_cardinal(270), 'W')
self.assertEqual(utils.degrees_to_cardinal(45), 'NE')
def test_degrees_to_cardinal_full_string(self):
"""Test degrees_to_cardinal() with full_string=True."""
self.assertEqual(utils.degrees_to_cardinal(0, full_string=True), 'North')
self.assertEqual(utils.degrees_to_cardinal(90, full_string=True), 'East')
self.assertEqual(utils.degrees_to_cardinal(180, full_string=True), 'South')
self.assertEqual(utils.degrees_to_cardinal(270, full_string=True), 'West')
def test_aprs_passcode(self):
"""Test aprs_passcode() function."""
passcode = utils.aprs_passcode('N0CALL')
self.assertIsInstance(passcode, int)
self.assertGreaterEqual(passcode, 0)
self.assertLessEqual(passcode, 0x7FFF)
# Same callsign should produce same passcode
passcode1 = utils.aprs_passcode('N0CALL')
passcode2 = utils.aprs_passcode('N0CALL')
self.assertEqual(passcode1, passcode2)
# Different callsigns should produce different passcodes
passcode3 = utils.aprs_passcode('K1ABC')
self.assertNotEqual(passcode1, passcode3)
def test_aprs_passcode_with_ssid(self):
"""Test aprs_passcode() with SSID."""
passcode1 = utils.aprs_passcode('N0CALL-1')
passcode2 = utils.aprs_passcode('N0CALL')
self.assertEqual(passcode1, passcode2)
def test_load_entry_points(self):
"""Test load_entry_points() function."""
# Should not raise exception even with non-existent group
utils.load_entry_points('nonexistent.group')
@mock.patch('aprsd.utils.update_checker.UpdateChecker')
def test_check_version(self, mock_checker):
"""Test _check_version() function."""
mock_instance = mock.MagicMock()
mock_instance.check.return_value = None
mock_checker.return_value = mock_instance
level, msg = utils._check_version()
self.assertEqual(level, 0)
self.assertIn('up to date', msg)
@mock.patch('aprsd.utils.update_checker.UpdateChecker')
def test_check_version_update_available(self, mock_checker):
"""Test _check_version() when update is available."""
mock_instance = mock.MagicMock()
mock_instance.check.return_value = 'New version available'
mock_checker.return_value = mock_instance
level, msg = utils._check_version()
self.assertEqual(level, 1)
self.assertEqual(msg, 'New version available')