Select the bmspy-ups device from the configured devices
bmspy-ups selected its device from the devices that had data at startup, and exited with status 0 if the selection failed. One failed first read ended the monitor or made it select the wrong device. The server has a new DEVICES command that lists the configured names, and client.list_devices calls it. bmspy-ups selects from that list and then waits for the first data of its device. A wrong or missing --device now exits with status 2. The service file does not restart on that status, because a restart cannot correct it. An older server ends on the unknown DEVICES command, so update the server first.
This commit is contained in:
@@ -91,6 +91,12 @@ class TestReadData:
|
||||
call_args = mock_comms.call_args[0][1]
|
||||
assert call_args.get("ups") == "myups"
|
||||
|
||||
def test_list_devices_returns_configured_names(self):
|
||||
from bmspy.client import list_devices
|
||||
with patch("bmspy.client.socket_comms", return_value={"devices": ["network", "nas"]}) as mock_comms:
|
||||
assert list_devices("/fake/socket", "test") == ["network", "nas"]
|
||||
assert mock_comms.call_args[0][1] == {"command": "DEVICES", "client": "test"}
|
||||
|
||||
def test_no_ups_filter_not_in_request(self):
|
||||
with patch("bmspy.client.socket_comms") as mock_comms:
|
||||
mock_comms.return_value = {}
|
||||
|
||||
@@ -445,6 +445,20 @@ class TestServerMain:
|
||||
finally:
|
||||
self._stop_server(sock_path, t)
|
||||
|
||||
def test_devices_command_lists_configured_devices(self, tmp_path):
|
||||
"""DEVICES names every configured device, through the real client."""
|
||||
from bmspy import client
|
||||
|
||||
sock_path = str(tmp_path / "server_devices.sock")
|
||||
ready = threading.Event()
|
||||
t = self._make_server_thread(sock_path, ready, threading.Event())
|
||||
t.start()
|
||||
ready.wait(timeout=2)
|
||||
try:
|
||||
assert client.list_devices(sock_path, "test") == ["ttyUSB0"]
|
||||
finally:
|
||||
self._stop_server(sock_path, t)
|
||||
|
||||
def test_deregister_command(self, tmp_path):
|
||||
"""Test DEREGISTER command via server main()."""
|
||||
sock_path = str(tmp_path / "server_dereg.sock")
|
||||
|
||||
+52
-27
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
import smtplib
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import call, patch, MagicMock
|
||||
|
||||
import bmspy.ups as ups_mod
|
||||
from bmspy.classes import BMSScalarField, BMSMultiField, BMSInfoField, UPS
|
||||
@@ -24,6 +24,13 @@ def _reset_globals():
|
||||
ups_mod.alert_sent = False
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _configured_devices():
|
||||
"""The main() tests use one configured device unless they patch this themselves."""
|
||||
with patch("bmspy.ups.client.list_devices", return_value=["testups"]):
|
||||
yield
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _get_field_value
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -569,31 +576,57 @@ 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."""
|
||||
def test_main_waits_at_startup_until_the_device_has_data(self, capsys):
|
||||
"""A failed first read must not end the monitor: systemd would not restart it."""
|
||||
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
|
||||
patch("bmspy.ups.client.list_devices", return_value=["ups1"]), \
|
||||
patch("bmspy.ups.client.read_data",
|
||||
side_effect=[{}, {}, self._make_data("ups1"), StopIteration("done")]) as read_data, \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep") as mock_sleep:
|
||||
with pytest.raises(StopIteration):
|
||||
ups_mod.main()
|
||||
# Two empty answers, the answer with the device, then the first loop read.
|
||||
assert [c.kwargs for c in read_data.call_args_list] == [{"ups": "ups1"}] * 4
|
||||
assert mock_sleep.call_args_list == [call(ups_mod.STARTUP_RETRY_PERIOD)] * 2
|
||||
# The reason is reported once, not on each retry.
|
||||
assert capsys.readouterr().out.count("UPS 'ups1' has no data yet") == 1
|
||||
|
||||
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), \
|
||||
def test_main_selects_from_configured_devices_not_from_those_with_data(self):
|
||||
"""One device with data must not be picked when the server has several."""
|
||||
with patch("sys.argv", ["bmspy-ups", "--device", "ups2"]), \
|
||||
patch("bmspy.ups.client.list_devices", return_value=["ups1", "ups2"]), \
|
||||
patch("bmspy.ups.client.read_data",
|
||||
side_effect=[{}, self._make_data("ups2"), StopIteration("done")]) as read_data, \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep") as mock_sleep:
|
||||
with pytest.raises(StopIteration):
|
||||
ups_mod.main()
|
||||
assert [c.kwargs for c in read_data.call_args_list] == [{"ups": "ups2"}] * 3
|
||||
assert mock_sleep.call_count == 1
|
||||
|
||||
@pytest.mark.parametrize("argv,expected", [
|
||||
([], "multiple UPS devices found, specify one with --device: ups1, ups2"),
|
||||
(["--device", "ghost"], "UPS 'ghost' not found. Available: ups1, ups2"),
|
||||
])
|
||||
def test_main_wrong_or_missing_device_option_is_an_error_exit(self, capsys, argv, expected):
|
||||
"""Only ups1 has data, but both are configured: do not guess, and do not wait."""
|
||||
with patch("sys.argv", ["bmspy-ups"] + argv), \
|
||||
patch("bmspy.ups.client.list_devices", return_value=["ups1", "ups2"]), \
|
||||
patch("bmspy.ups.client.read_data", return_value=self._make_data("ups1")) as read_data, \
|
||||
patch("bmspy.ups.client.handle_registration"):
|
||||
ups_mod.main()
|
||||
out = capsys.readouterr().out
|
||||
assert "ups1" in out or "ups2" in out
|
||||
with pytest.raises(SystemExit) as exit_info:
|
||||
ups_mod.main()
|
||||
# The argparse usage status: systemd reports it and does not restart.
|
||||
assert exit_info.value.code == ups_mod.USAGE_ERROR_STATUS == 2
|
||||
assert expected in capsys.readouterr().out
|
||||
read_data.assert_not_called()
|
||||
|
||||
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 patch("sys.argv", ["bmspy-ups", "--device", "ups2"]), \
|
||||
patch("bmspy.ups.client.list_devices", return_value=["ups1", "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)}]
|
||||
@@ -619,14 +652,6 @@ class TestUpsMainDeviceSelection:
|
||||
ups_mod.main()
|
||||
assert read_data.call_args_list[1] == (("/tmp/test.sock", "ups"), {"ups": "testups"})
|
||||
|
||||
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)}
|
||||
|
||||
Reference in New Issue
Block a user