Files
ups/test_status_alerts.py
T

100 lines
3.7 KiB
Python

"""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()