Skip to content

Commit 2d69ed8

Browse files
committed
test(WebServer): increase Python coverage to 89%
1 parent c229d57 commit 2d69ed8

3 files changed

Lines changed: 674 additions & 0 deletions

File tree

Tools/WebServer/tests/test_main.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,63 @@ def test_restore_state_with_auto_monitor(
261261
device.auto_connect = False
262262
device.auto_monitor = False
263263
device.ser = None
264+
265+
@patch("main.start_monitor")
266+
@patch("main.start_device_worker")
267+
@patch("main.serial_open")
268+
def test_restore_state_monitor_failed(
269+
self, mock_serial_open, mock_start_worker, mock_start_monitor
270+
):
271+
"""Test restore_state when monitor start fails."""
272+
from main import restore_state
273+
from state import state
274+
275+
mock_serial = MagicMock()
276+
mock_serial_open.return_value = (mock_serial, None)
277+
mock_start_monitor.return_value = (False, "Monitor failed")
278+
279+
# Setup device for auto-connect and auto-monitor
280+
device = list(state.devices.values())[0]
281+
device.auto_connect = True
282+
device.port = "/dev/ttyUSB0"
283+
device.auto_monitor = True
284+
device.auto_monitor_mode = "cpu-usage"
285+
286+
restore_state()
287+
288+
mock_start_monitor.assert_called()
289+
290+
# Cleanup
291+
device.auto_connect = False
292+
device.auto_monitor = False
293+
device.ser = None
294+
295+
@patch("main.start_monitor")
296+
@patch("main.start_device_worker")
297+
@patch("main.serial_open")
298+
def test_restore_state_no_auto_monitor_mode(
299+
self, mock_serial_open, mock_start_worker, mock_start_monitor
300+
):
301+
"""Test restore_state when auto_monitor_mode is empty."""
302+
from main import restore_state
303+
from state import state
304+
305+
mock_serial = MagicMock()
306+
mock_serial_open.return_value = (mock_serial, None)
307+
308+
# Setup device for auto-connect but no auto_monitor_mode
309+
device = list(state.devices.values())[0]
310+
device.auto_connect = True
311+
device.port = "/dev/ttyUSB0"
312+
device.auto_monitor = True
313+
device.auto_monitor_mode = None
314+
315+
restore_state()
316+
317+
# Should not try to start monitor
318+
mock_start_monitor.assert_not_called()
319+
320+
# Cleanup
321+
device.auto_connect = False
322+
device.auto_monitor = False
323+
device.ser = None

Tools/WebServer/tests/test_monitor.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,3 +1023,211 @@ def test_cmd_file_with_crlf(self, tmp_path):
10231023

10241024
check_cmd_file(device)
10251025
assert not cmd_file.exists()
1026+
1027+
1028+
class TestMonitorTickWithThreshold:
1029+
"""Test monitor tick with threshold alarm."""
1030+
1031+
def test_monitor_tick_with_threshold_enabled(self):
1032+
"""Test monitor tick with threshold enabled."""
1033+
from monitor import _create_monitor_tick
1034+
from state import DeviceState
1035+
1036+
device = DeviceState("test", "Test")
1037+
device.monitor_running = True
1038+
device.monitor_mode_0 = "cpu-usage"
1039+
device.monitor_mode_1 = "none"
1040+
device.motor_min = 0
1041+
device.motor_max = 1000
1042+
device.ser = None
1043+
device.threshold_enable = True
1044+
device.threshold_mode = "cpu-usage"
1045+
device.threshold_value = 0 # Set to 0 to always trigger
1046+
device.threshold_freq = 1000
1047+
device.threshold_duration = 100
1048+
device.last_alarm_time = 0
1049+
1050+
tick = _create_monitor_tick(device)
1051+
tick()
1052+
1053+
# Threshold should have been checked
1054+
assert device.last_percent_0 is not None
1055+
1056+
1057+
class TestMonitorTickCH1Only:
1058+
"""Test monitor tick with CH1 only."""
1059+
1060+
def test_monitor_tick_ch1_only(self):
1061+
"""Test monitor tick with only CH1 active."""
1062+
from monitor import _create_monitor_tick
1063+
from state import DeviceState
1064+
1065+
device = DeviceState("test", "Test")
1066+
device.monitor_running = True
1067+
device.monitor_mode_0 = "none"
1068+
device.monitor_mode_1 = "cpu-usage"
1069+
device.motor_min = 0
1070+
device.motor_max = 1000
1071+
device.ser = None
1072+
device.threshold_enable = False
1073+
1074+
tick = _create_monitor_tick(device)
1075+
tick()
1076+
1077+
# last_percent_1 should be updated
1078+
assert device.last_percent_1 is not None
1079+
# last_percent should use CH1 value since CH0 is none
1080+
assert device.last_percent == device.last_percent_1
1081+
1082+
1083+
class TestMonitorTickWithSerialCH1:
1084+
"""Test monitor tick with serial for CH1."""
1085+
1086+
def test_monitor_tick_ch1_with_serial(self):
1087+
"""Test monitor tick CH1 with serial port."""
1088+
from monitor import _create_monitor_tick
1089+
from state import DeviceState
1090+
1091+
device = DeviceState("test", "Test")
1092+
device.monitor_running = True
1093+
device.monitor_mode_0 = "none"
1094+
device.monitor_mode_1 = "mem-usage"
1095+
device.motor_min = 0
1096+
device.motor_max = 1000
1097+
device.ser = MagicMock()
1098+
device.ser.isOpen.return_value = True
1099+
device.threshold_enable = False
1100+
1101+
tick = _create_monitor_tick(device)
1102+
tick()
1103+
1104+
assert device.last_percent_1 is not None
1105+
1106+
1107+
class TestStartMonitorWithAudioMode:
1108+
"""Test start_monitor with audio mode."""
1109+
1110+
def test_start_monitor_audio_mode(self):
1111+
"""Test start_monitor with audio-level mode."""
1112+
from monitor import start_monitor, stop_monitor
1113+
from state import DeviceState
1114+
1115+
device = DeviceState("test", "Test")
1116+
device.monitor_mode_0 = "audio-level"
1117+
device.monitor_mode_1 = "none"
1118+
device.period = 1.0
1119+
1120+
result, error = start_monitor(device, "audio-level")
1121+
assert result is True
1122+
1123+
# Cleanup
1124+
stop_monitor(device)
1125+
1126+
def test_start_monitor_audio_left_mode(self):
1127+
"""Test start_monitor with audio-left mode."""
1128+
from monitor import start_monitor, stop_monitor
1129+
from state import DeviceState
1130+
1131+
device = DeviceState("test", "Test")
1132+
device.monitor_mode_0 = "audio-left"
1133+
device.monitor_mode_1 = "none"
1134+
device.period = 1.0
1135+
1136+
result, error = start_monitor(device, "audio-left")
1137+
assert result is True
1138+
1139+
# Cleanup
1140+
stop_monitor(device)
1141+
1142+
def test_start_monitor_audio_right_mode(self):
1143+
"""Test start_monitor with audio-right mode."""
1144+
from monitor import start_monitor, stop_monitor
1145+
from state import DeviceState
1146+
1147+
device = DeviceState("test", "Test")
1148+
device.monitor_mode_0 = "none"
1149+
device.monitor_mode_1 = "audio-right"
1150+
device.period = 1.0
1151+
1152+
result, error = start_monitor(device, "audio-right")
1153+
assert result is True
1154+
1155+
# Cleanup
1156+
stop_monitor(device)
1157+
1158+
1159+
class TestStopMonitorWithTimers:
1160+
"""Test stop_monitor with various timer states."""
1161+
1162+
def test_stop_monitor_no_timers(self):
1163+
"""Test stop_monitor when no timers exist."""
1164+
from monitor import stop_monitor
1165+
from state import DeviceState
1166+
1167+
device = DeviceState("test", "Test")
1168+
device.monitor_running = True
1169+
device.monitor_timer = None
1170+
device.cmd_file_timer = None
1171+
device.audio_recorder = None
1172+
1173+
mock_tm = MagicMock()
1174+
device.worker = MagicMock()
1175+
device.worker.run_in_worker = MagicMock(side_effect=lambda f, t: f())
1176+
device.worker.get_timer_manager.return_value = mock_tm
1177+
1178+
result, error = stop_monitor(device)
1179+
assert result is True
1180+
assert device.monitor_running is False
1181+
1182+
1183+
class TestGetAudioLevelRightChannelMono:
1184+
"""Test get_audio_level with right channel on mono audio."""
1185+
1186+
def test_get_audio_level_right_channel_mono(self):
1187+
"""Test get_audio_level right channel with mono audio."""
1188+
from monitor import get_audio_level, sc
1189+
from state import DeviceState
1190+
1191+
if sc is None:
1192+
return
1193+
1194+
import numpy as np
1195+
1196+
device = DeviceState("test", "Test")
1197+
mock_recorder = MagicMock()
1198+
# Simulate mono audio data (single channel)
1199+
mock_recorder.record.return_value = np.array([[0.5], [0.4], [0.6]])
1200+
device.audio_recorder = mock_recorder
1201+
device.audio_db_min = -60
1202+
device.audio_db_max = 0
1203+
device.audio_channel = "right"
1204+
1205+
value, error = get_audio_level(device)
1206+
# Should fall back to channel 0 for mono
1207+
assert error is None
1208+
1209+
1210+
class TestGetAudioLevelChannelMono:
1211+
"""Test get_audio_level_channel with mono audio."""
1212+
1213+
def test_get_audio_level_channel_right_mono(self):
1214+
"""Test get_audio_level_channel right with mono audio."""
1215+
from monitor import get_audio_level_channel, sc
1216+
from state import DeviceState
1217+
1218+
if sc is None:
1219+
return
1220+
1221+
import numpy as np
1222+
1223+
device = DeviceState("test", "Test")
1224+
mock_recorder = MagicMock()
1225+
# Simulate mono audio data
1226+
mock_recorder.record.return_value = np.array([[0.5], [0.4], [0.6]])
1227+
device.audio_recorder = mock_recorder
1228+
device.audio_db_min = -60
1229+
device.audio_db_max = 0
1230+
1231+
value, error = get_audio_level_channel(device, "right")
1232+
# Should fall back to channel 0
1233+
assert error is None

0 commit comments

Comments
 (0)