Files

100 lines
3.7 KiB
Python
Raw Permalink Normal View History

2026-09-10 20:46:31 +02:00
"""Status alert regression tests with hardware integrations stubbed out."""
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import MagicMock, patch
with patch.dict(sys.modules, {
name: MagicMock()
for name in ("paramiko", "rrdtool", "dbus_next", "dbus_next.aio",
"dbus_next.constants", "PyNUTClient")
}):
spec = importlib.util.spec_from_file_location(
"collector_under_test", Path(__file__).with_name("ups2rrd.py")
)
collector = importlib.util.module_from_spec(spec)
spec.loader.exec_module(collector)
class StatusAlertTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.state_file = str(Path(self.directory.name) / "state.json")
for target, value in (
("ALERT_STATE_FILE", self.state_file),
("notify", MagicMock()),
("shutdown_remote_server", MagicMock()),
("shutdown_local_server", MagicMock()),
):
patcher = patch.object(collector, target, value)
patcher.start()
self.addCleanup(patcher.stop)
def sample(self, seconds, status, runtime=1000):
with patch.object(collector.time, "time", return_value=seconds):
collector.process_alerts({
"status": status, "battery_charge": 100,
"battery_runtime": runtime,
})
with open(self.state_file) as state_file:
return json.load(state_file)
def test_initial_baseline_and_short_flip_flops(self):
self.sample(0, "OL")
self.sample(60, "OB")
self.sample(659, "OL")
self.sample(700, "OB")
state = self.sample(1300, "OL")
collector.notify.assert_not_called()
self.assertIsNone(state["pending_status"])
def test_persistent_change_and_recovery_each_alert_once(self):
self.sample(0, "OL")
self.sample(60, "OB")
self.sample(659, "OB")
collector.notify.assert_not_called()
self.sample(660, "OB")
self.sample(720, "OB")
collector.notify.assert_called_once_with("UPS status changed: OL -> OB")
self.sample(780, "OL")
self.sample(1380, "OL")
self.assertEqual(collector.notify.call_count, 2)
collector.notify.assert_called_with("UPS status changed: OB -> OL")
def test_different_pending_status_restarts_timer(self):
self.sample(0, "OL")
self.sample(60, "OB")
self.sample(600, "LB")
self.sample(660, "LB")
collector.notify.assert_not_called()
self.sample(1200, "LB")
collector.notify.assert_called_once_with("UPS status changed: OL -> LB")
def test_existing_state_migrates_and_alerts_after_late_poll(self):
with open(self.state_file, "w") as state_file:
json.dump({"status": "OL"}, state_file)
self.sample(100, "OB")
collector.notify.assert_not_called()
self.sample(750, "OB")
collector.notify.assert_called_once_with("UPS status changed: OL -> OB")
def test_runtime_alert_and_shutdowns_are_immediate(self):
self.sample(0, "OL")
state = self.sample(60, "OB", runtime=100)
self.assertEqual(state["confirmed_status"], "OL")
collector.shutdown_remote_server.assert_called_once()
collector.shutdown_local_server.assert_called_once()
messages = [call.args[0] for call in collector.notify.call_args_list]
self.assertTrue(any("runtime is below 5 minutes" in m for m in messages))
self.assertFalse(any("UPS status changed" in m for m in messages))
if __name__ == "__main__":
unittest.main()