Fix UPS functionality for multiple devices
This commit is contained in:
+132
-30
@@ -4,7 +4,7 @@ from unittest.mock import patch, MagicMock
|
||||
|
||||
import bmspy.ups as ups_mod
|
||||
from bmspy.classes import BMSScalarField, BMSMultiField, BMSInfoField, UPS
|
||||
from bmspy.ups import _get_field_value, handle_shutdown, handle_email
|
||||
from bmspy.ups import _get_field_value, _resolve_ups_device, handle_shutdown, handle_email
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -204,6 +204,44 @@ class TestHandleEmail:
|
||||
assert args[1] == "root@myhost"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _resolve_ups_device
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _simple_ups_data(*names: str) -> dict[str, UPS]:
|
||||
return {name: UPS.from_dict({
|
||||
"bms_current_amps": {"help": "A", "raw_value": 0.0, "value": "0.00", "units": "A"},
|
||||
}) for name in names}
|
||||
|
||||
|
||||
class TestResolveUpsDevice:
|
||||
def test_single_device_no_request_returns_it(self):
|
||||
data = _simple_ups_data("myups")
|
||||
assert _resolve_ups_device(data, None) == "myups"
|
||||
|
||||
def test_requested_device_found(self):
|
||||
data = _simple_ups_data("ups1", "ups2")
|
||||
assert _resolve_ups_device(data, "ups2") == "ups2"
|
||||
|
||||
def test_requested_device_not_found_returns_none(self, capsys):
|
||||
data = _simple_ups_data("ups1")
|
||||
result = _resolve_ups_device(data, "missing")
|
||||
assert result is None
|
||||
assert "missing" in capsys.readouterr().out
|
||||
|
||||
def test_multiple_devices_no_request_returns_none(self, capsys):
|
||||
data = _simple_ups_data("ups1", "ups2")
|
||||
result = _resolve_ups_device(data, None)
|
||||
assert result is None
|
||||
out = capsys.readouterr().out
|
||||
assert "ups1" in out or "ups2" in out
|
||||
|
||||
def test_empty_data_returns_none(self, capsys):
|
||||
result = _resolve_ups_device({}, None)
|
||||
assert result is None
|
||||
assert "no UPS" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ups main() - comprehensive loop testing
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -229,27 +267,26 @@ def _make_ups(current_amps: float, charge_ratio: float) -> UPS:
|
||||
class TestUpsMain:
|
||||
"""Test ups.main() loop behavior by running a limited number of iterations."""
|
||||
|
||||
def _run_main_with_data(self, data_sequence, argv=None, extra_patches=None):
|
||||
"""Run main() with a sequence of UPS data, stopping when data runs out."""
|
||||
call_count = 0
|
||||
def _run_main_with_data(self, data_sequence, argv=None):
|
||||
"""Run main() with a sequence of UPS data, stopping when data runs out.
|
||||
|
||||
The first call to read_data is the device-discovery call and returns
|
||||
data_sequence[0]. Subsequent calls consume data_sequence in order.
|
||||
"""
|
||||
# discovery call returns data_sequence[0], then loop calls follow
|
||||
discovery_done = [False]
|
||||
loop_count = [0]
|
||||
|
||||
def _read_data(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
if call_count >= len(data_sequence):
|
||||
if not discovery_done[0]:
|
||||
discovery_done[0] = True
|
||||
return data_sequence[0]
|
||||
if loop_count[0] >= len(data_sequence):
|
||||
raise StopIteration("done")
|
||||
result = data_sequence[call_count]
|
||||
call_count += 1
|
||||
result = data_sequence[loop_count[0]]
|
||||
loop_count[0] += 1
|
||||
return result
|
||||
|
||||
patches = {
|
||||
"bmspy.ups.client.read_data": _read_data,
|
||||
"bmspy.ups.client.handle_registration": MagicMock(),
|
||||
"bmspy.ups.time.sleep": MagicMock(),
|
||||
"bmspy.ups.os.system": MagicMock(),
|
||||
}
|
||||
if extra_patches:
|
||||
patches.update(extra_patches)
|
||||
|
||||
with patch("sys.argv", ["bmspy-ups"] + (argv or [])):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=_read_data), \
|
||||
@@ -257,7 +294,7 @@ class TestUpsMain:
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.os.system"):
|
||||
ups_mod.main()
|
||||
return call_count
|
||||
return loop_count[0]
|
||||
|
||||
def _make_data(self, current_amps, charge_ratio):
|
||||
return {"testups": _make_ups(current_amps, charge_ratio)}
|
||||
@@ -269,7 +306,7 @@ class TestUpsMain:
|
||||
|
||||
with patch("sys.argv", ["bmspy-ups"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
@@ -285,7 +322,7 @@ class TestUpsMain:
|
||||
data = [self._make_data(0.0, 0.95)] * 5
|
||||
with patch("sys.argv", ["bmspy-ups", "-v", "-v"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data + [StopIteration]),\
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data[0]] + data + [StopIteration]),\
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.os.system"):
|
||||
@@ -305,7 +342,7 @@ class TestUpsMain:
|
||||
mock_email = MagicMock()
|
||||
with patch("sys.argv", ["bmspy-ups", "--critical", "30"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown", mock_shutdown), \
|
||||
@@ -325,7 +362,7 @@ class TestUpsMain:
|
||||
mock_email = MagicMock()
|
||||
with patch("sys.argv", ["bmspy-ups", "--warning", "75", "--critical", "30"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
@@ -352,7 +389,7 @@ class TestUpsMain:
|
||||
mock_email = MagicMock()
|
||||
with patch("sys.argv", ["bmspy-ups"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
@@ -381,7 +418,7 @@ class TestUpsMain:
|
||||
mock_shutdown = MagicMock()
|
||||
with patch("sys.argv", ["bmspy-ups"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown", mock_shutdown), \
|
||||
@@ -394,7 +431,7 @@ class TestUpsMain:
|
||||
data_seq = [self._make_data(0.0, 0.95)] * 5
|
||||
with patch("sys.argv", ["bmspy-ups", "-v", "-v"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
@@ -409,7 +446,7 @@ class TestUpsMain:
|
||||
data_seq = [self._make_data(0.0, 0.60)] * 5
|
||||
with patch("sys.argv", ["bmspy-ups", "-v", "--warning", "75", "--critical", "30"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
@@ -430,7 +467,7 @@ class TestUpsMain:
|
||||
]
|
||||
with patch("sys.argv", ["bmspy-ups", "-v"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
@@ -451,7 +488,7 @@ class TestUpsMain:
|
||||
]
|
||||
with patch("sys.argv", ["bmspy-ups", "-v"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
@@ -465,7 +502,7 @@ class TestUpsMain:
|
||||
data_seq = [self._make_data(1.0, 0.80)] * 5
|
||||
with patch("sys.argv", ["bmspy-ups", "-v", "-v"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
@@ -480,7 +517,7 @@ class TestUpsMain:
|
||||
data_seq = [self._make_data(0.0, 0.20)] * 5
|
||||
with patch("sys.argv", ["bmspy-ups", "-v", "--critical", "30"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
||||
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
@@ -488,3 +525,68 @@ class TestUpsMain:
|
||||
ups_mod.main()
|
||||
captured = capsys.readouterr()
|
||||
assert "critical" in captured.out.lower() or "threshold" in captured.out.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# main() — device selection errors and --device flag
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestUpsMainDeviceSelection:
|
||||
def _make_data(self, name, current_amps=0.0, charge_ratio=0.8):
|
||||
return {name: _make_ups(current_amps, charge_ratio)}
|
||||
|
||||
def test_main_no_devices_returns_early(self, capsys):
|
||||
"""main() exits cleanly when no devices are found."""
|
||||
with patch("sys.argv", ["bmspy-ups"]), \
|
||||
patch("bmspy.ups.client.read_data", return_value={}), \
|
||||
patch("bmspy.ups.client.handle_registration"):
|
||||
ups_mod.main()
|
||||
assert "no UPS" in capsys.readouterr().out
|
||||
|
||||
def test_main_multiple_devices_no_flag_returns_early(self, capsys):
|
||||
"""main() exits with an error when multiple devices exist and --device is not set."""
|
||||
two_devices = {
|
||||
"ups1": _make_ups(0.0, 0.9),
|
||||
"ups2": _make_ups(0.0, 0.8),
|
||||
}
|
||||
with patch("sys.argv", ["bmspy-ups"]), \
|
||||
patch("bmspy.ups.client.read_data", return_value=two_devices), \
|
||||
patch("bmspy.ups.client.handle_registration"):
|
||||
ups_mod.main()
|
||||
out = capsys.readouterr().out
|
||||
assert "ups1" in out or "ups2" in out
|
||||
|
||||
def test_main_ups_flag_selects_device(self):
|
||||
"""--device selects the correct device from multiple available ones."""
|
||||
data_seq = [self._make_data("ups2")] * 6
|
||||
with patch("sys.argv", ["bmspy-ups", "--device", "ups2"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data",
|
||||
side_effect=[{"ups1": _make_ups(0.0, 0.9), "ups2": _make_ups(0.0, 0.8)}]
|
||||
+ data_seq + [StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.handle_shutdown"), \
|
||||
patch("bmspy.ups.handle_email"):
|
||||
ups_mod.main()
|
||||
|
||||
def test_main_ups_flag_unknown_device_returns_early(self, capsys):
|
||||
"""--device with an unknown device name exits with an error."""
|
||||
with patch("sys.argv", ["bmspy-ups", "--device", "ghost"]), \
|
||||
patch("bmspy.ups.client.read_data", return_value={"ups1": _make_ups(0.0, 0.9)}), \
|
||||
patch("bmspy.ups.client.handle_registration"):
|
||||
ups_mod.main()
|
||||
assert "ghost" in capsys.readouterr().out
|
||||
|
||||
def test_main_device_disappears_in_loop_sleeps(self):
|
||||
"""When the named device is missing from a loop response, main() sleeps and retries."""
|
||||
initial = {"testups": _make_ups(0.0, 0.8)}
|
||||
# Second call returns empty dict (device gone), third raises StopIteration
|
||||
with patch("sys.argv", ["bmspy-ups"]):
|
||||
with pytest.raises(StopIteration):
|
||||
with patch("bmspy.ups.client.read_data",
|
||||
side_effect=[initial, {}, StopIteration("done")]), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep") as mock_sleep:
|
||||
ups_mod.main()
|
||||
mock_sleep.assert_called()
|
||||
|
||||
Reference in New Issue
Block a user