test_printer_manager.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. """Unit tests for PrinterManager service.
  2. Tests printer connection management, status tracking, and print control.
  3. """
  4. import asyncio
  5. from datetime import datetime
  6. from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
  7. import pytest
  8. from backend.app.services.printer_manager import (
  9. PrinterManager,
  10. get_derived_status_name,
  11. init_printer_connections,
  12. printer_state_to_dict,
  13. )
  14. class TestPrinterManager:
  15. """Tests for PrinterManager class."""
  16. @pytest.fixture
  17. def manager(self):
  18. """Create a fresh PrinterManager instance."""
  19. return PrinterManager()
  20. @pytest.fixture
  21. def mock_printer(self):
  22. """Create a mock Printer object."""
  23. printer = MagicMock()
  24. printer.id = 1
  25. printer.ip_address = "192.168.1.100"
  26. printer.serial_number = "00M09A123456789"
  27. printer.access_code = "12345678"
  28. printer.is_active = True
  29. return printer
  30. @pytest.fixture
  31. def mock_client(self):
  32. """Create a mock BambuMQTTClient."""
  33. client = MagicMock()
  34. client.state = MagicMock()
  35. client.state.connected = True
  36. client.state.state = "IDLE"
  37. client.state.progress = 0
  38. client.state.temperatures = {"nozzle": 25, "bed": 25}
  39. client.state.raw_data = {}
  40. client.logging_enabled = False
  41. return client
  42. # ========================================================================
  43. # Tests for initialization
  44. # ========================================================================
  45. def test_init_creates_empty_clients_dict(self, manager):
  46. """Verify manager initializes with empty clients dict."""
  47. assert manager._clients == {}
  48. def test_init_callbacks_are_none(self, manager):
  49. """Verify all callbacks are initially None."""
  50. assert manager._on_print_start is None
  51. assert manager._on_print_complete is None
  52. assert manager._on_status_change is None
  53. assert manager._on_ams_change is None
  54. def test_init_loop_is_none(self, manager):
  55. """Verify event loop is initially None."""
  56. assert manager._loop is None
  57. # ========================================================================
  58. # Tests for callback setters
  59. # ========================================================================
  60. def test_set_event_loop(self, manager):
  61. """Verify event loop can be set."""
  62. mock_loop = MagicMock()
  63. manager.set_event_loop(mock_loop)
  64. assert manager._loop == mock_loop
  65. def test_set_print_start_callback(self, manager):
  66. """Verify print start callback can be set."""
  67. callback = MagicMock()
  68. manager.set_print_start_callback(callback)
  69. assert manager._on_print_start == callback
  70. def test_set_print_complete_callback(self, manager):
  71. """Verify print complete callback can be set."""
  72. callback = MagicMock()
  73. manager.set_print_complete_callback(callback)
  74. assert manager._on_print_complete == callback
  75. def test_set_status_change_callback(self, manager):
  76. """Verify status change callback can be set."""
  77. callback = MagicMock()
  78. manager.set_status_change_callback(callback)
  79. assert manager._on_status_change == callback
  80. def test_set_ams_change_callback(self, manager):
  81. """Verify AMS change callback can be set."""
  82. callback = MagicMock()
  83. manager.set_ams_change_callback(callback)
  84. assert manager._on_ams_change == callback
  85. # ========================================================================
  86. # Tests for _schedule_async
  87. # ========================================================================
  88. def test_schedule_async_with_running_loop(self, manager):
  89. """Verify async coroutine is scheduled when loop is running."""
  90. mock_loop = MagicMock()
  91. mock_loop.is_running.return_value = True
  92. manager._loop = mock_loop
  93. async def dummy_coro():
  94. pass
  95. coro = dummy_coro()
  96. manager._schedule_async(coro)
  97. mock_loop.is_running.assert_called_once()
  98. # Clean up the coroutine
  99. coro.close()
  100. def test_schedule_async_without_loop(self, manager):
  101. """Verify nothing happens when no loop is set."""
  102. async def dummy_coro():
  103. pass
  104. coro = dummy_coro()
  105. # Should not raise
  106. manager._schedule_async(coro)
  107. coro.close()
  108. def test_schedule_async_with_stopped_loop(self, manager):
  109. """Verify nothing happens when loop is not running."""
  110. mock_loop = MagicMock()
  111. mock_loop.is_running.return_value = False
  112. manager._loop = mock_loop
  113. async def dummy_coro():
  114. pass
  115. coro = dummy_coro()
  116. manager._schedule_async(coro)
  117. coro.close()
  118. # ========================================================================
  119. # Tests for connect_printer
  120. # ========================================================================
  121. @pytest.mark.asyncio
  122. async def test_connect_printer_creates_client(self, manager, mock_printer):
  123. """Verify connecting creates an MQTT client."""
  124. with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient:
  125. mock_instance = MagicMock()
  126. mock_instance.state = MagicMock()
  127. mock_instance.state.connected = True
  128. MockClient.return_value = mock_instance
  129. result = await manager.connect_printer(mock_printer)
  130. MockClient.assert_called_once()
  131. mock_instance.connect.assert_called_once()
  132. assert mock_printer.id in manager._clients
  133. assert result is True
  134. @pytest.mark.asyncio
  135. async def test_connect_printer_disconnects_existing(self, manager, mock_printer, mock_client):
  136. """Verify connecting disconnects existing client first."""
  137. manager._clients[mock_printer.id] = mock_client
  138. with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient:
  139. new_client = MagicMock()
  140. new_client.state = MagicMock()
  141. new_client.state.connected = True
  142. MockClient.return_value = new_client
  143. await manager.connect_printer(mock_printer)
  144. mock_client.disconnect.assert_called_once()
  145. @pytest.mark.asyncio
  146. async def test_connect_printer_returns_false_on_failure(self, manager, mock_printer):
  147. """Verify returns False when connection fails."""
  148. with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient:
  149. mock_instance = MagicMock()
  150. mock_instance.state = MagicMock()
  151. mock_instance.state.connected = False
  152. MockClient.return_value = mock_instance
  153. result = await manager.connect_printer(mock_printer)
  154. assert result is False
  155. # ========================================================================
  156. # Tests for disconnect_printer
  157. # ========================================================================
  158. def test_disconnect_printer_removes_client(self, manager, mock_client):
  159. """Verify disconnecting removes and disconnects client."""
  160. manager._clients[1] = mock_client
  161. manager.disconnect_printer(1)
  162. mock_client.disconnect.assert_called_once()
  163. assert 1 not in manager._clients
  164. def test_disconnect_printer_handles_missing(self, manager):
  165. """Verify disconnecting non-existent printer doesn't raise."""
  166. manager.disconnect_printer(999) # Should not raise
  167. # ========================================================================
  168. # Tests for disconnect_all
  169. # ========================================================================
  170. def test_disconnect_all_disconnects_all_clients(self, manager):
  171. """Verify all clients are disconnected."""
  172. client1 = MagicMock()
  173. client2 = MagicMock()
  174. manager._clients[1] = client1
  175. manager._clients[2] = client2
  176. manager.disconnect_all()
  177. client1.disconnect.assert_called_once()
  178. client2.disconnect.assert_called_once()
  179. assert len(manager._clients) == 0
  180. # ========================================================================
  181. # Tests for get_status
  182. # ========================================================================
  183. def test_get_status_returns_state(self, manager, mock_client):
  184. """Verify get_status returns client state."""
  185. manager._clients[1] = mock_client
  186. result = manager.get_status(1)
  187. mock_client.check_staleness.assert_called_once()
  188. assert result == mock_client.state
  189. def test_get_status_returns_none_for_unknown(self, manager):
  190. """Verify get_status returns None for unknown printer."""
  191. result = manager.get_status(999)
  192. assert result is None
  193. # ========================================================================
  194. # Tests for get_all_statuses
  195. # ========================================================================
  196. def test_get_all_statuses_returns_all(self, manager):
  197. """Verify all statuses are returned."""
  198. client1 = MagicMock()
  199. client1.state = MagicMock(connected=True)
  200. client2 = MagicMock()
  201. client2.state = MagicMock(connected=False)
  202. manager._clients[1] = client1
  203. manager._clients[2] = client2
  204. result = manager.get_all_statuses()
  205. assert len(result) == 2
  206. assert 1 in result
  207. assert 2 in result
  208. client1.check_staleness.assert_called_once()
  209. client2.check_staleness.assert_called_once()
  210. # ========================================================================
  211. # Tests for is_connected
  212. # ========================================================================
  213. def test_is_connected_returns_true(self, manager, mock_client):
  214. """Verify is_connected returns True for connected printer."""
  215. mock_client.check_staleness.return_value = True
  216. manager._clients[1] = mock_client
  217. result = manager.is_connected(1)
  218. assert result is True
  219. def test_is_connected_returns_false_for_unknown(self, manager):
  220. """Verify is_connected returns False for unknown printer."""
  221. result = manager.is_connected(999)
  222. assert result is False
  223. # ========================================================================
  224. # Tests for get_client
  225. # ========================================================================
  226. def test_get_client_returns_client(self, manager, mock_client):
  227. """Verify get_client returns the client."""
  228. manager._clients[1] = mock_client
  229. result = manager.get_client(1)
  230. assert result == mock_client
  231. def test_get_client_returns_none_for_unknown(self, manager):
  232. """Verify get_client returns None for unknown printer."""
  233. result = manager.get_client(999)
  234. assert result is None
  235. # ========================================================================
  236. # Tests for mark_printer_offline
  237. # ========================================================================
  238. def test_mark_printer_offline_updates_state(self, manager, mock_client):
  239. """Verify mark_printer_offline updates client state."""
  240. mock_client.state.connected = True
  241. manager._clients[1] = mock_client
  242. manager.mark_printer_offline(1)
  243. assert mock_client.state.connected is False
  244. assert mock_client.state.state == "unknown"
  245. def test_mark_printer_offline_triggers_callback(self, manager, mock_client):
  246. """Verify mark_printer_offline triggers status callback."""
  247. mock_client.state.connected = True
  248. manager._clients[1] = mock_client
  249. # Callback must return a coroutine
  250. async def async_callback(printer_id, state):
  251. pass
  252. manager._on_status_change = async_callback
  253. # Need a running loop for callback
  254. mock_loop = MagicMock()
  255. mock_loop.is_running.return_value = True
  256. manager._loop = mock_loop
  257. manager.mark_printer_offline(1)
  258. # Callback should be scheduled via run_coroutine_threadsafe
  259. mock_loop.is_running.assert_called()
  260. # State should be updated
  261. assert mock_client.state.connected is False
  262. def test_mark_printer_offline_handles_unknown(self, manager):
  263. """Verify mark_printer_offline handles unknown printer."""
  264. manager.mark_printer_offline(999) # Should not raise
  265. def test_mark_printer_offline_skips_already_offline(self, manager, mock_client):
  266. """Verify mark_printer_offline skips already offline printer."""
  267. mock_client.state.connected = False
  268. manager._clients[1] = mock_client
  269. manager.mark_printer_offline(1)
  270. # State should remain unchanged
  271. assert mock_client.state.connected is False
  272. # ========================================================================
  273. # Tests for start_print
  274. # ========================================================================
  275. def test_start_print_calls_client(self, manager, mock_client):
  276. """Verify start_print calls client method."""
  277. mock_client.start_print.return_value = True
  278. manager._clients[1] = mock_client
  279. result = manager.start_print(1, "test.gcode")
  280. mock_client.start_print.assert_called_once_with("test.gcode", 1)
  281. assert result is True
  282. def test_start_print_returns_false_for_unknown(self, manager):
  283. """Verify start_print returns False for unknown printer."""
  284. result = manager.start_print(999, "test.gcode")
  285. assert result is False
  286. # ========================================================================
  287. # Tests for stop_print
  288. # ========================================================================
  289. def test_stop_print_calls_client(self, manager, mock_client):
  290. """Verify stop_print calls client method."""
  291. mock_client.stop_print.return_value = True
  292. manager._clients[1] = mock_client
  293. result = manager.stop_print(1)
  294. mock_client.stop_print.assert_called_once()
  295. assert result is True
  296. def test_stop_print_returns_false_for_unknown(self, manager):
  297. """Verify stop_print returns False for unknown printer."""
  298. result = manager.stop_print(999)
  299. assert result is False
  300. # ========================================================================
  301. # Tests for wait_for_cooldown
  302. # ========================================================================
  303. @pytest.mark.asyncio
  304. async def test_wait_for_cooldown_returns_true_when_cool(self, manager, mock_client):
  305. """Verify wait_for_cooldown returns True when printer is cool."""
  306. mock_client.state.connected = True
  307. mock_client.state.temperatures = {"nozzle": 40, "bed": 30}
  308. mock_client.check_staleness.return_value = True
  309. manager._clients[1] = mock_client
  310. result = await manager.wait_for_cooldown(1, target_temp=50)
  311. assert result is True
  312. @pytest.mark.asyncio
  313. async def test_wait_for_cooldown_returns_false_on_disconnect(self, manager, mock_client):
  314. """Verify wait_for_cooldown returns False when printer disconnects."""
  315. mock_client.state.connected = False
  316. mock_client.check_staleness.return_value = False
  317. manager._clients[1] = mock_client
  318. result = await manager.wait_for_cooldown(1, target_temp=50, timeout=1)
  319. assert result is False
  320. @pytest.mark.asyncio
  321. async def test_wait_for_cooldown_returns_false_for_unknown(self, manager):
  322. """Verify wait_for_cooldown returns False for unknown printer."""
  323. result = await manager.wait_for_cooldown(999, target_temp=50, timeout=1)
  324. assert result is False
  325. @pytest.mark.asyncio
  326. async def test_wait_for_cooldown_checks_both_nozzles(self, manager, mock_client):
  327. """Verify wait_for_cooldown checks both nozzles for dual extruders."""
  328. mock_client.state.connected = True
  329. mock_client.state.temperatures = {"nozzle": 40, "nozzle_2": 45, "bed": 30}
  330. mock_client.check_staleness.return_value = True
  331. manager._clients[1] = mock_client
  332. result = await manager.wait_for_cooldown(1, target_temp=50)
  333. assert result is True
  334. # ========================================================================
  335. # Tests for logging methods
  336. # ========================================================================
  337. def test_enable_logging_calls_client(self, manager, mock_client):
  338. """Verify enable_logging calls client method."""
  339. manager._clients[1] = mock_client
  340. result = manager.enable_logging(1, True)
  341. mock_client.enable_logging.assert_called_once_with(True)
  342. assert result is True
  343. def test_enable_logging_returns_false_for_unknown(self, manager):
  344. """Verify enable_logging returns False for unknown printer."""
  345. result = manager.enable_logging(999, True)
  346. assert result is False
  347. def test_get_logs_returns_logs(self, manager, mock_client):
  348. """Verify get_logs returns client logs."""
  349. mock_logs = [MagicMock(), MagicMock()]
  350. mock_client.get_logs.return_value = mock_logs
  351. manager._clients[1] = mock_client
  352. result = manager.get_logs(1)
  353. assert result == mock_logs
  354. def test_get_logs_returns_empty_for_unknown(self, manager):
  355. """Verify get_logs returns empty list for unknown printer."""
  356. result = manager.get_logs(999)
  357. assert result == []
  358. def test_clear_logs_calls_client(self, manager, mock_client):
  359. """Verify clear_logs calls client method."""
  360. manager._clients[1] = mock_client
  361. result = manager.clear_logs(1)
  362. mock_client.clear_logs.assert_called_once()
  363. assert result is True
  364. def test_clear_logs_returns_false_for_unknown(self, manager):
  365. """Verify clear_logs returns False for unknown printer."""
  366. result = manager.clear_logs(999)
  367. assert result is False
  368. def test_is_logging_enabled_returns_status(self, manager, mock_client):
  369. """Verify is_logging_enabled returns client status."""
  370. mock_client.logging_enabled = True
  371. manager._clients[1] = mock_client
  372. result = manager.is_logging_enabled(1)
  373. assert result is True
  374. def test_is_logging_enabled_returns_false_for_unknown(self, manager):
  375. """Verify is_logging_enabled returns False for unknown printer."""
  376. result = manager.is_logging_enabled(999)
  377. assert result is False
  378. # ========================================================================
  379. # Tests for request_status_update
  380. # ========================================================================
  381. def test_request_status_update_calls_client(self, manager, mock_client):
  382. """Verify request_status_update calls client method."""
  383. mock_client.request_status_update.return_value = True
  384. manager._clients[1] = mock_client
  385. result = manager.request_status_update(1)
  386. mock_client.request_status_update.assert_called_once()
  387. assert result is True
  388. def test_request_status_update_returns_false_for_unknown(self, manager):
  389. """Verify request_status_update returns False for unknown printer."""
  390. result = manager.request_status_update(999)
  391. assert result is False
  392. # ========================================================================
  393. # Tests for test_connection
  394. # ========================================================================
  395. @pytest.mark.asyncio
  396. async def test_test_connection_success(self, manager):
  397. """Verify test_connection returns success on connection."""
  398. with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient:
  399. mock_instance = MagicMock()
  400. mock_instance.state = MagicMock()
  401. mock_instance.state.connected = True
  402. mock_instance.state.state = "IDLE"
  403. mock_instance.state.raw_data = {"device_model": "X1C"}
  404. MockClient.return_value = mock_instance
  405. result = await manager.test_connection("192.168.1.100", "00M09A123456789", "12345678")
  406. assert result["success"] is True
  407. assert result["state"] == "IDLE"
  408. assert result["model"] == "X1C"
  409. mock_instance.disconnect.assert_called_once()
  410. @pytest.mark.asyncio
  411. async def test_test_connection_failure(self, manager):
  412. """Verify test_connection returns failure on connection error."""
  413. with patch("backend.app.services.printer_manager.BambuMQTTClient") as MockClient:
  414. mock_instance = MagicMock()
  415. mock_instance.state = MagicMock()
  416. mock_instance.state.connected = False
  417. MockClient.return_value = mock_instance
  418. result = await manager.test_connection("192.168.1.100", "00M09A123456789", "12345678")
  419. assert result["success"] is False
  420. assert result["state"] is None
  421. mock_instance.disconnect.assert_called_once()
  422. class TestPrinterStateToDict:
  423. """Tests for printer_state_to_dict helper function."""
  424. @pytest.fixture
  425. def mock_state(self):
  426. """Create a mock PrinterState."""
  427. state = MagicMock()
  428. state.connected = True
  429. state.state = "RUNNING"
  430. state.current_print = "test.3mf"
  431. state.subtask_name = "Test Print"
  432. state.gcode_file = "/sdcard/test.gcode"
  433. state.progress = 50
  434. state.remaining_time = 3600
  435. state.layer_num = 10
  436. state.total_layers = 20
  437. state.temperatures = {"nozzle": 200, "bed": 60}
  438. state.hms_errors = []
  439. state.ams_status_main = 0
  440. state.ams_status_sub = 0
  441. state.tray_now = "1"
  442. state.wifi_signal = -50
  443. state.raw_data = {}
  444. state.stg_cur = -1 # No calibration stage active
  445. return state
  446. def test_basic_conversion(self, mock_state):
  447. """Verify basic state fields are converted."""
  448. result = printer_state_to_dict(mock_state)
  449. assert result["connected"] is True
  450. assert result["state"] == "RUNNING"
  451. assert result["progress"] == 50
  452. assert result["temperatures"] == {"nozzle": 200, "bed": 60}
  453. def test_ams_data_parsing(self, mock_state):
  454. """Verify AMS data is parsed correctly."""
  455. mock_state.raw_data = {
  456. "ams": [
  457. {
  458. "id": 0,
  459. "humidity_raw": 45,
  460. "temp": 25,
  461. "tray": [
  462. {
  463. "id": 0,
  464. "tray_color": "FF0000",
  465. "tray_type": "PLA",
  466. "tray_sub_brands": "Generic",
  467. "remain": 80,
  468. "k": 0.5,
  469. "tag_uid": "ABC123",
  470. "tray_uuid": "uuid-123",
  471. }
  472. ],
  473. }
  474. ]
  475. }
  476. result = printer_state_to_dict(mock_state)
  477. assert result["ams"] is not None
  478. assert len(result["ams"]) == 1
  479. assert result["ams"][0]["humidity"] == 45
  480. assert len(result["ams"][0]["tray"]) == 1
  481. assert result["ams"][0]["tray"][0]["tray_color"] == "FF0000"
  482. def test_empty_tag_uid_becomes_none(self, mock_state):
  483. """Verify empty tag_uid is converted to None."""
  484. mock_state.raw_data = {
  485. "ams": [
  486. {
  487. "id": 0,
  488. "tray": [
  489. {
  490. "id": 0,
  491. "tag_uid": "",
  492. "tray_uuid": "00000000000000000000000000000000",
  493. }
  494. ],
  495. }
  496. ]
  497. }
  498. result = printer_state_to_dict(mock_state)
  499. assert result["ams"][0]["tray"][0]["tag_uid"] is None
  500. assert result["ams"][0]["tray"][0]["tray_uuid"] is None
  501. def test_zero_tag_uid_becomes_none(self, mock_state):
  502. """Verify zero tag_uid is converted to None."""
  503. mock_state.raw_data = {
  504. "ams": [
  505. {
  506. "id": 0,
  507. "tray": [
  508. {
  509. "id": 0,
  510. "tag_uid": "0000000000000000",
  511. }
  512. ],
  513. }
  514. ]
  515. }
  516. result = printer_state_to_dict(mock_state)
  517. assert result["ams"][0]["tray"][0]["tag_uid"] is None
  518. def test_vt_tray_parsing(self, mock_state):
  519. """Verify virtual tray is parsed correctly."""
  520. mock_state.raw_data = {
  521. "vt_tray": {
  522. "tray_color": "00FF00",
  523. "tray_type": "PETG",
  524. "tray_sub_brands": "Generic",
  525. "remain": 60,
  526. "tag_uid": "VT123",
  527. }
  528. }
  529. result = printer_state_to_dict(mock_state)
  530. assert result["vt_tray"] is not None
  531. assert result["vt_tray"]["id"] == 254
  532. assert result["vt_tray"]["tray_color"] == "00FF00"
  533. assert result["vt_tray"]["tray_type"] == "PETG"
  534. def test_hms_errors_conversion(self, mock_state):
  535. """Verify HMS errors are converted correctly."""
  536. error = MagicMock()
  537. error.code = "0700_0100"
  538. error.attr = 1
  539. error.module = "AMS"
  540. error.severity = 2
  541. mock_state.hms_errors = [error]
  542. result = printer_state_to_dict(mock_state)
  543. assert len(result["hms_errors"]) == 1
  544. assert result["hms_errors"][0]["code"] == "0700_0100"
  545. assert result["hms_errors"][0]["module"] == "AMS"
  546. def test_cover_url_added_for_running_print(self, mock_state):
  547. """Verify cover_url is added for running prints."""
  548. result = printer_state_to_dict(mock_state, printer_id=1)
  549. assert result["cover_url"] == "/api/v1/printers/1/cover"
  550. def test_cover_url_none_when_not_running(self, mock_state):
  551. """Verify cover_url is None when not printing."""
  552. mock_state.state = "IDLE"
  553. result = printer_state_to_dict(mock_state, printer_id=1)
  554. assert result["cover_url"] is None
  555. def test_ams_ht_detection(self, mock_state):
  556. """Verify AMS-HT is detected (1 tray vs 4)."""
  557. mock_state.raw_data = {
  558. "ams": [
  559. {
  560. "id": 0,
  561. "tray": [{"id": 0}], # Only 1 tray = AMS-HT
  562. }
  563. ]
  564. }
  565. result = printer_state_to_dict(mock_state)
  566. assert result["ams"][0]["is_ams_ht"] is True
  567. def test_regular_ams_detection(self, mock_state):
  568. """Verify regular AMS is detected (4 trays)."""
  569. mock_state.raw_data = {"ams": [{"id": 0, "tray": [{"id": 0}, {"id": 1}, {"id": 2}, {"id": 3}]}]}
  570. result = printer_state_to_dict(mock_state)
  571. assert result["ams"][0]["is_ams_ht"] is False
  572. class TestGetDerivedStatusName:
  573. """Tests for get_derived_status_name function."""
  574. def test_stg_cur_255_returns_none(self):
  575. """Verify stg_cur=255 (A1/P1 idle) returns None, not 'Unknown stage (255)'."""
  576. state = MagicMock()
  577. state.stg_cur = 255
  578. state.state = "IDLE"
  579. result = get_derived_status_name(state)
  580. assert result is None
  581. def test_stg_cur_negative_one_returns_none_when_idle(self):
  582. """Verify stg_cur=-1 (X1 idle) returns None."""
  583. state = MagicMock()
  584. state.stg_cur = -1
  585. state.state = "IDLE"
  586. result = get_derived_status_name(state)
  587. assert result is None
  588. def test_valid_stage_returns_name(self):
  589. """Verify valid stg_cur values return stage name."""
  590. state = MagicMock()
  591. state.stg_cur = 1 # Auto bed leveling
  592. result = get_derived_status_name(state)
  593. assert result == "Auto bed leveling"
  594. def test_stg_cur_zero_returns_printing(self):
  595. """Verify stg_cur=0 returns 'Printing'."""
  596. state = MagicMock()
  597. state.stg_cur = 0
  598. result = get_derived_status_name(state)
  599. assert result == "Printing"
  600. class TestInitPrinterConnections:
  601. """Tests for init_printer_connections function."""
  602. @pytest.mark.asyncio
  603. async def test_connects_all_active_printers(self):
  604. """Verify all active printers are connected."""
  605. mock_db = AsyncMock()
  606. mock_printer1 = MagicMock(id=1, is_active=True)
  607. mock_printer2 = MagicMock(id=2, is_active=True)
  608. mock_result = MagicMock()
  609. mock_result.scalars.return_value.all.return_value = [mock_printer1, mock_printer2]
  610. mock_db.execute.return_value = mock_result
  611. with patch("backend.app.services.printer_manager.printer_manager") as mock_manager:
  612. mock_manager.connect_printer = AsyncMock()
  613. await init_printer_connections(mock_db)
  614. assert mock_manager.connect_printer.call_count == 2
  615. @pytest.mark.asyncio
  616. async def test_handles_empty_printer_list(self):
  617. """Verify empty printer list is handled."""
  618. mock_db = AsyncMock()
  619. mock_result = MagicMock()
  620. mock_result.scalars.return_value.all.return_value = []
  621. mock_db.execute.return_value = mock_result
  622. with patch("backend.app.services.printer_manager.printer_manager") as mock_manager:
  623. mock_manager.connect_printer = AsyncMock()
  624. await init_printer_connections(mock_db)
  625. mock_manager.connect_printer.assert_not_called()