@@ -1158,3 +1158,137 @@ def test_orchestrator_stops_on_event(self, mock_web, mock_load):
11581158
11591159 polling_loop (config_mgr , storage , stop )
11601160 # If we get here without hanging, the test passes
1161+
1162+ @patch ("app.drivers.driver_registry.load_driver" )
1163+ @patch ("app.main.web" )
1164+ def test_driver_hot_swap_on_modem_type_change (self , mock_web , mock_load ):
1165+ """Polling loop should hot-swap the modem driver when modem_type changes."""
1166+ import threading
1167+ from app .main import polling_loop
1168+
1169+ mock_driver = MagicMock ()
1170+ mock_driver .get_device_info .return_value = {"model" : "Test" , "sw_version" : "1.0" }
1171+ mock_driver .get_connection_info .return_value = {}
1172+ mock_driver .get_docsis_data .return_value = {}
1173+ mock_load .return_value = mock_driver
1174+
1175+ config_mgr = self ._make_config_mgr ()
1176+ storage = MagicMock ()
1177+ stop = threading .Event ()
1178+
1179+ call_count = [0 ]
1180+ original_wait = stop .wait
1181+
1182+ def change_modem_after_first_tick (timeout = None ):
1183+ call_count [0 ] += 1
1184+ if call_count [0 ] == 1 :
1185+ # After first tick, change modem_type in config
1186+ config_mgr .get_all .return_value ["modem_type" ] = "tc4400"
1187+ config_mgr .get .side_effect = lambda k , d = None : {
1188+ "modem_type" : "tc4400" ,
1189+ "modem_url" : "http://fritz.box" ,
1190+ "modem_user" : "admin" ,
1191+ "modem_password" : "pass" ,
1192+ "poll_interval" : 900 ,
1193+ }.get (k , d )
1194+ return original_wait (0 )
1195+ elif call_count [0 ] >= 3 :
1196+ stop .set ()
1197+ return True
1198+ return original_wait (0 )
1199+
1200+ stop .wait = change_modem_after_first_tick
1201+
1202+ polling_loop (config_mgr , storage , stop )
1203+
1204+ # load_driver should have been called at least twice:
1205+ # once for initial setup, once for hot-swap
1206+ assert mock_load .call_count >= 2
1207+ # Second call should use the new modem type
1208+ second_call = mock_load .call_args_list [1 ]
1209+ assert second_call [0 ][0 ] == "tc4400"
1210+ # Web state should have been reset for the swap
1211+ mock_web .reset_modem_state .assert_called ()
1212+ mock_web .init_collector .assert_called ()
1213+
1214+ @patch ("app.drivers.driver_registry.load_driver" )
1215+ @patch ("app.main.web" )
1216+ def test_driver_hot_swap_on_url_change (self , mock_web , mock_load ):
1217+ """Hot-swap should trigger when modem URL changes, not just type."""
1218+ import threading
1219+ from app .main import polling_loop
1220+
1221+ mock_driver = MagicMock ()
1222+ mock_driver .get_device_info .return_value = {"model" : "Test" , "sw_version" : "1.0" }
1223+ mock_driver .get_connection_info .return_value = {}
1224+ mock_driver .get_docsis_data .return_value = {}
1225+ mock_load .return_value = mock_driver
1226+
1227+ config_mgr = self ._make_config_mgr ()
1228+ storage = MagicMock ()
1229+ stop = threading .Event ()
1230+
1231+ call_count = [0 ]
1232+ original_wait = stop .wait
1233+
1234+ def change_url_after_first_tick (timeout = None ):
1235+ call_count [0 ] += 1
1236+ if call_count [0 ] == 1 :
1237+ # Change URL but keep same modem_type
1238+ config_mgr .get .side_effect = lambda k , d = None : {
1239+ "modem_type" : "fritzbox" ,
1240+ "modem_url" : "http://192.168.100.1" ,
1241+ "modem_user" : "admin" ,
1242+ "modem_password" : "pass" ,
1243+ "poll_interval" : 900 ,
1244+ }.get (k , d )
1245+ return original_wait (0 )
1246+ elif call_count [0 ] >= 3 :
1247+ stop .set ()
1248+ return True
1249+ return original_wait (0 )
1250+
1251+ stop .wait = change_url_after_first_tick
1252+
1253+ polling_loop (config_mgr , storage , stop )
1254+
1255+ # load_driver called twice: initial + hot-swap for URL change
1256+ assert mock_load .call_count >= 2
1257+ second_call = mock_load .call_args_list [1 ]
1258+ assert second_call [0 ][1 ] == "http://192.168.100.1"
1259+
1260+ @patch ("app.drivers.driver_registry.load_driver" )
1261+ @patch ("app.main.web" )
1262+ def test_no_hot_swap_when_config_unchanged (self , mock_web , mock_load ):
1263+ """No hot-swap should occur when modem config hasn't changed."""
1264+ import threading
1265+ from app .main import polling_loop
1266+
1267+ mock_driver = MagicMock ()
1268+ mock_driver .get_device_info .return_value = {"model" : "Test" , "sw_version" : "1.0" }
1269+ mock_driver .get_connection_info .return_value = {}
1270+ mock_driver .get_docsis_data .return_value = {}
1271+ mock_load .return_value = mock_driver
1272+
1273+ config_mgr = self ._make_config_mgr ()
1274+ storage = MagicMock ()
1275+ stop = threading .Event ()
1276+
1277+ call_count = [0 ]
1278+ original_wait = stop .wait
1279+
1280+ def stop_after_ticks (timeout = None ):
1281+ call_count [0 ] += 1
1282+ if call_count [0 ] >= 3 :
1283+ stop .set ()
1284+ return True
1285+ return original_wait (0 )
1286+
1287+ stop .wait = stop_after_ticks
1288+
1289+ polling_loop (config_mgr , storage , stop )
1290+
1291+ # load_driver should only be called once (initial setup)
1292+ assert mock_load .call_count == 1
1293+ # reset_modem_state should NOT have been called (no swap)
1294+ mock_web .reset_modem_state .assert_not_called ()
0 commit comments