mirror of
https://github.com/craigerl/aprsd.git
synced 2026-07-11 13:24:11 -04:00
Added new APRSDPushStatsThread
This allows an aprsd server instance to push it's to a remote location.
This commit is contained in:
+278
-1
@@ -1,8 +1,14 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import requests
|
||||
|
||||
from aprsd.stats import collector
|
||||
from aprsd.threads.stats import APRSDStatsStoreThread, StatsStore
|
||||
from aprsd.threads.stats import (
|
||||
APRSDPushStatsThread,
|
||||
APRSDStatsStoreThread,
|
||||
StatsStore,
|
||||
)
|
||||
|
||||
|
||||
class TestStatsStore(unittest.TestCase):
|
||||
@@ -145,5 +151,276 @@ class TestAPRSDStatsStoreThread(unittest.TestCase):
|
||||
# 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()
|
||||
|
||||
Reference in New Issue
Block a user