1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-02-13 03:23:44 -05:00
aprsd/tests/threads/test_stats.py
Walter Boring c5ca4f11af Added new APRSDPushStatsThread
This allows an aprsd server instance to push it's to a remote
location.
2026-02-10 18:49:23 -05:00

427 lines
16 KiB
Python

import unittest
from unittest import mock
import requests
from aprsd.stats import collector
from aprsd.threads.stats import (
APRSDPushStatsThread,
APRSDStatsStoreThread,
StatsStore,
)
class TestStatsStore(unittest.TestCase):
"""Unit tests for the StatsStore class."""
def test_init(self):
"""Test StatsStore initialization."""
ss = StatsStore()
self.assertIsNotNone(ss.lock)
self.assertFalse(hasattr(ss, 'data'))
def test_add(self):
"""Test add method."""
ss = StatsStore()
test_data = {'test': 'data'}
ss.add(test_data)
self.assertEqual(ss.data, test_data)
def test_add_concurrent(self):
"""Test add method with concurrent access."""
import threading
ss = StatsStore()
test_data = {'test': 'data'}
results = []
def add_data():
ss.add(test_data)
results.append(ss.data)
# Create multiple threads to test thread safety
threads = []
for _ in range(5):
t = threading.Thread(target=add_data)
threads.append(t)
t.start()
for t in threads:
t.join()
# All threads should have added the data
for result in results:
self.assertEqual(result, test_data)
class TestAPRSDStatsStoreThread(unittest.TestCase):
"""Unit tests for the APRSDStatsStoreThread class."""
def setUp(self):
"""Set up test fixtures."""
# Reset singleton instance
collector.Collector._instance = None
# Clear producers to start fresh
c = collector.Collector()
c.producers = []
def tearDown(self):
"""Clean up after tests."""
collector.Collector._instance = None
def test_init(self):
"""Test APRSDStatsStoreThread initialization."""
thread = APRSDStatsStoreThread()
self.assertEqual(thread.name, 'StatsStore')
self.assertEqual(thread.save_interval, 10)
self.assertTrue(hasattr(thread, 'loop_count'))
def test_loop_with_save(self):
"""Test loop method when save interval is reached."""
thread = APRSDStatsStoreThread()
# Mock the collector and save methods
with (
mock.patch('aprsd.stats.collector.Collector') as mock_collector_class,
mock.patch('aprsd.utils.objectstore.ObjectStoreMixin.save') as mock_save,
):
# Setup mock collector to return some stats
mock_collector_instance = mock.Mock()
mock_collector_instance.collect.return_value = {'test': 'data'}
mock_collector_class.return_value = mock_collector_instance
# Set loop_count to match save interval
thread.loop_count = 10
# Call loop
result = thread.loop()
# Should return True (continue looping)
self.assertTrue(result)
# Should have called collect and save
mock_collector_instance.collect.assert_called_once()
mock_save.assert_called_once()
def test_loop_without_save(self):
"""Test loop method when save interval is not reached."""
thread = APRSDStatsStoreThread()
# Mock the collector and save methods
with (
mock.patch('aprsd.stats.collector.Collector') as mock_collector_class,
mock.patch('aprsd.utils.objectstore.ObjectStoreMixin.save') as mock_save,
):
# Setup mock collector to return some stats
mock_collector_instance = mock.Mock()
mock_collector_instance.collect.return_value = {'test': 'data'}
mock_collector_class.return_value = mock_collector_instance
# Set loop_count to not match save interval
thread.loop_count = 1
# Call loop
result = thread.loop()
# Should return True (continue looping)
self.assertTrue(result)
# Should not have called save
mock_save.assert_not_called()
def test_loop_with_exception(self):
"""Test loop method when an exception occurs."""
thread = APRSDStatsStoreThread()
# Mock the collector to raise an exception
with mock.patch('aprsd.stats.collector.Collector') as mock_collector_class:
mock_collector_instance = mock.Mock()
mock_collector_instance.collect.side_effect = RuntimeError('Test exception')
mock_collector_class.return_value = mock_collector_instance
# Set loop_count to match save interval
thread.loop_count = 10
# Should raise the exception
with self.assertRaises(RuntimeError):
thread.loop()
# Removed test_loop_count_increment as it's not meaningful to test in isolation
# since the increment happens in the parent run() method, not in loop()
class TestAPRSDPushStatsThread(unittest.TestCase):
"""Unit tests for the APRSDPushStatsThread class."""
def test_init_with_explicit_args(self):
"""Test initialization with explicit push_url, frequency, and send_packetlist."""
thread = APRSDPushStatsThread(
push_url='https://example.com/api',
frequency_seconds=30,
send_packetlist=True,
)
self.assertEqual(thread.name, 'PushStats')
self.assertEqual(thread.push_url, 'https://example.com/api')
self.assertEqual(thread.period, 30)
self.assertTrue(thread.send_packetlist)
self.assertTrue(hasattr(thread, 'loop_count'))
def test_init_uses_conf_when_args_not_passed(self):
"""Test initialization uses CONF.push_stats when args omitted."""
with mock.patch('aprsd.threads.stats.CONF') as mock_conf:
mock_conf.push_stats.push_url = 'https://conf.example.com'
mock_conf.push_stats.frequency_seconds = 15
thread = APRSDPushStatsThread()
self.assertEqual(thread.push_url, 'https://conf.example.com')
self.assertEqual(thread.period, 15)
self.assertFalse(thread.send_packetlist)
def test_loop_skips_push_when_period_not_reached(self):
"""Test loop does not POST when loop_count not divisible by period."""
thread = APRSDPushStatsThread(
push_url='https://example.com',
frequency_seconds=10,
)
thread.loop_count = 3 # 3 % 10 != 0
with (
mock.patch('aprsd.threads.stats.collector.Collector') as mock_collector,
mock.patch('aprsd.threads.stats.requests.post') as mock_post,
mock.patch('aprsd.threads.stats.time.sleep'),
):
result = thread.loop()
self.assertTrue(result)
mock_collector.return_value.collect.assert_not_called()
mock_post.assert_not_called()
def test_loop_pushes_stats_and_removes_packetlist_by_default(self):
"""Test loop collects stats, POSTs to url/stats, and strips PacketList.packets."""
thread = APRSDPushStatsThread(
push_url='https://example.com',
frequency_seconds=10,
send_packetlist=False,
)
thread.loop_count = 10
collected = {
'PacketList': {'packets': [1, 2, 3], 'rx': 5, 'tx': 1},
'Other': 'data',
}
with (
mock.patch(
'aprsd.threads.stats.collector.Collector'
) as mock_collector_class,
mock.patch('aprsd.threads.stats.requests.post') as mock_post,
mock.patch('aprsd.threads.stats.time.sleep'),
mock.patch('aprsd.threads.stats.datetime') as mock_dt,
):
mock_collector_class.return_value.collect.return_value = collected
mock_dt.datetime.now.return_value.strftime.return_value = (
'01-01-2025 12:00:00'
)
result = thread.loop()
self.assertTrue(result)
mock_collector_class.return_value.collect.assert_called_once_with(
serializable=True
)
mock_post.assert_called_once()
call_args = mock_post.call_args
self.assertEqual(call_args[0][0], 'https://example.com/stats')
self.assertEqual(call_args[1]['headers'], {'Content-Type': 'application/json'})
self.assertEqual(call_args[1]['timeout'], 5)
body = call_args[1]['json']
self.assertEqual(body['time'], '01-01-2025 12:00:00')
self.assertNotIn('packets', body['stats']['PacketList'])
self.assertEqual(body['stats']['PacketList']['rx'], 5)
self.assertEqual(body['stats']['Other'], 'data')
def test_loop_pushes_stats_with_packetlist_when_send_packetlist_true(self):
"""Test loop includes PacketList.packets when send_packetlist is True."""
thread = APRSDPushStatsThread(
push_url='https://example.com',
frequency_seconds=10,
send_packetlist=True,
)
thread.loop_count = 10
collected = {'PacketList': {'packets': [1, 2, 3], 'rx': 5}}
with (
mock.patch(
'aprsd.threads.stats.collector.Collector'
) as mock_collector_class,
mock.patch('aprsd.threads.stats.requests.post') as mock_post,
mock.patch('aprsd.threads.stats.time.sleep'),
mock.patch('aprsd.threads.stats.datetime') as mock_dt,
):
mock_collector_class.return_value.collect.return_value = collected
mock_dt.datetime.now.return_value.strftime.return_value = (
'01-01-2025 12:00:00'
)
result = thread.loop()
self.assertTrue(result)
body = mock_post.call_args[1]['json']
self.assertEqual(body['stats']['PacketList']['packets'], [1, 2, 3])
def test_loop_on_http_200_logs_success(self):
"""Test loop logs info on successful 200 response."""
thread = APRSDPushStatsThread(
push_url='https://example.com',
frequency_seconds=10,
)
thread.loop_count = 10
with (
mock.patch(
'aprsd.threads.stats.collector.Collector'
) as mock_collector_class,
mock.patch('aprsd.threads.stats.requests.post') as mock_post,
mock.patch('aprsd.threads.stats.time.sleep'),
mock.patch('aprsd.threads.stats.datetime') as mock_dt,
mock.patch('aprsd.threads.stats.LOGU') as mock_logu,
):
mock_collector_class.return_value.collect.return_value = {}
mock_dt.datetime.now.return_value.strftime.return_value = (
'01-01-2025 12:00:00'
)
mock_post.return_value.status_code = 200
mock_post.return_value.raise_for_status = mock.Mock()
result = thread.loop()
self.assertTrue(result)
mock_logu.info.assert_called()
self.assertIn('Successfully pushed stats', mock_logu.info.call_args[0][0])
def test_loop_on_non_200_logs_warning(self):
"""Test loop logs warning when response is not 200."""
thread = APRSDPushStatsThread(
push_url='https://example.com',
frequency_seconds=10,
)
thread.loop_count = 10
with (
mock.patch(
'aprsd.threads.stats.collector.Collector'
) as mock_collector_class,
mock.patch('aprsd.threads.stats.requests.post') as mock_post,
mock.patch('aprsd.threads.stats.time.sleep'),
mock.patch('aprsd.threads.stats.datetime') as mock_dt,
mock.patch('aprsd.threads.stats.LOGU') as mock_logu,
):
mock_collector_class.return_value.collect.return_value = {}
mock_dt.datetime.now.return_value.strftime.return_value = (
'01-01-2025 12:00:00'
)
mock_post.return_value.status_code = 500
mock_post.return_value.raise_for_status = mock.Mock()
result = thread.loop()
self.assertTrue(result)
mock_logu.warning.assert_called_once()
self.assertIn('Failed to push stats', mock_logu.warning.call_args[0][0])
self.assertIn('500', mock_logu.warning.call_args[0][0])
def test_loop_on_request_exception_logs_error_and_continues(self):
"""Test loop logs error on requests.RequestException and returns True."""
thread = APRSDPushStatsThread(
push_url='https://example.com',
frequency_seconds=10,
)
thread.loop_count = 10
with (
mock.patch(
'aprsd.threads.stats.collector.Collector'
) as mock_collector_class,
mock.patch('aprsd.threads.stats.requests.post') as mock_post,
mock.patch('aprsd.threads.stats.time.sleep'),
mock.patch('aprsd.threads.stats.datetime') as mock_dt,
mock.patch('aprsd.threads.stats.LOGU') as mock_logu,
):
mock_collector_class.return_value.collect.return_value = {}
mock_dt.datetime.now.return_value.strftime.return_value = (
'01-01-2025 12:00:00'
)
mock_post.side_effect = requests.exceptions.ConnectionError(
'Connection refused'
)
result = thread.loop()
self.assertTrue(result)
mock_logu.error.assert_called_once()
self.assertIn('Error pushing stats', mock_logu.error.call_args[0][0])
def test_loop_on_other_exception_logs_error_and_continues(self):
"""Test loop logs error on unexpected exception and returns True."""
thread = APRSDPushStatsThread(
push_url='https://example.com',
frequency_seconds=10,
)
thread.loop_count = 10
with (
mock.patch(
'aprsd.threads.stats.collector.Collector'
) as mock_collector_class,
mock.patch('aprsd.threads.stats.requests.post') as mock_post,
mock.patch('aprsd.threads.stats.time.sleep'),
mock.patch('aprsd.threads.stats.datetime') as mock_dt,
mock.patch('aprsd.threads.stats.LOGU') as mock_logu,
):
mock_collector_class.return_value.collect.return_value = {}
mock_dt.datetime.now.return_value.strftime.return_value = (
'01-01-2025 12:00:00'
)
mock_post.side_effect = ValueError('unexpected')
result = thread.loop()
self.assertTrue(result)
mock_logu.error.assert_called_once()
self.assertIn('Unexpected error in stats push', mock_logu.error.call_args[0][0])
def test_loop_no_packetlist_key_in_stats(self):
"""Test loop does not fail when stats have no PacketList key."""
thread = APRSDPushStatsThread(
push_url='https://example.com',
frequency_seconds=10,
send_packetlist=False,
)
thread.loop_count = 10
collected = {'Only': 'data', 'No': 'PacketList'}
with (
mock.patch(
'aprsd.threads.stats.collector.Collector'
) as mock_collector_class,
mock.patch('aprsd.threads.stats.requests.post') as mock_post,
mock.patch('aprsd.threads.stats.time.sleep'),
mock.patch('aprsd.threads.stats.datetime') as mock_dt,
):
mock_collector_class.return_value.collect.return_value = collected
mock_dt.datetime.now.return_value.strftime.return_value = (
'01-01-2025 12:00:00'
)
result = thread.loop()
self.assertTrue(result)
body = mock_post.call_args[1]['json']
self.assertEqual(body['stats'], collected)
if __name__ == '__main__':
unittest.main()