test_bambu_mqtt.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. """
  2. Tests for the BambuMQTTClient service.
  3. These tests focus on timelapse tracking during prints.
  4. """
  5. from unittest.mock import MagicMock, patch
  6. import pytest
  7. class TestTimelapseTracking:
  8. """Tests for timelapse state tracking during prints."""
  9. @pytest.fixture
  10. def mqtt_client(self):
  11. """Create a BambuMQTTClient instance for testing."""
  12. from backend.app.services.bambu_mqtt import BambuMQTTClient
  13. client = BambuMQTTClient(
  14. ip_address="192.168.1.100",
  15. serial_number="TEST123",
  16. access_code="12345678",
  17. )
  18. return client
  19. def test_timelapse_flag_initializes_to_false(self, mqtt_client):
  20. """Verify _timelapse_during_print starts as False."""
  21. assert mqtt_client._timelapse_during_print is False
  22. def test_timelapse_flag_set_when_timelapse_active_during_running(self, mqtt_client):
  23. """Verify timelapse flag is set when timelapse is active while printing."""
  24. # Simulate print running
  25. mqtt_client._was_running = True
  26. mqtt_client.state.timelapse = False
  27. # Simulate xcam data showing timelapse is enabled
  28. xcam_data = {"timelapse": "enable"}
  29. mqtt_client._parse_xcam_data(xcam_data)
  30. assert mqtt_client.state.timelapse is True
  31. assert mqtt_client._timelapse_during_print is True
  32. def test_timelapse_flag_not_set_when_not_running(self, mqtt_client):
  33. """Verify timelapse flag is NOT set when printer not running."""
  34. # Printer is idle (not running)
  35. mqtt_client._was_running = False
  36. mqtt_client.state.timelapse = False
  37. # Timelapse is enabled but we're not printing
  38. xcam_data = {"timelapse": "enable"}
  39. mqtt_client._parse_xcam_data(xcam_data)
  40. assert mqtt_client.state.timelapse is True
  41. # Flag should NOT be set since we're not printing
  42. assert mqtt_client._timelapse_during_print is False
  43. def test_timelapse_flag_persists_after_timelapse_stops(self, mqtt_client):
  44. """Verify timelapse flag stays True even after recording stops."""
  45. # Simulate print running with timelapse
  46. mqtt_client._was_running = True
  47. # Enable timelapse during print
  48. xcam_data = {"timelapse": "enable"}
  49. mqtt_client._parse_xcam_data(xcam_data)
  50. assert mqtt_client._timelapse_during_print is True
  51. # Disable timelapse (recording stops at end of print)
  52. xcam_data = {"timelapse": "disable"}
  53. mqtt_client._parse_xcam_data(xcam_data)
  54. # Flag should still be True (persists until reset)
  55. assert mqtt_client.state.timelapse is False
  56. assert mqtt_client._timelapse_during_print is True
  57. def test_timelapse_flag_from_print_data(self, mqtt_client):
  58. """Verify timelapse flag is set from print data (not just xcam)."""
  59. # Simulate print running
  60. mqtt_client._was_running = True
  61. mqtt_client.state.timelapse = False
  62. mqtt_client._timelapse_during_print = False
  63. # Manually test the timelapse parsing logic from _parse_print_data
  64. # This tests the "timelapse" field in the main print data
  65. data = {"timelapse": True}
  66. mqtt_client.state.timelapse = data["timelapse"] is True
  67. if mqtt_client.state.timelapse and mqtt_client._was_running:
  68. mqtt_client._timelapse_during_print = True
  69. assert mqtt_client._timelapse_during_print is True
  70. class TestPrintCompletionWithTimelapse:
  71. """Tests for print completion including timelapse flag."""
  72. @pytest.fixture
  73. def mqtt_client(self):
  74. """Create a BambuMQTTClient instance for testing."""
  75. from backend.app.services.bambu_mqtt import BambuMQTTClient
  76. client = BambuMQTTClient(
  77. ip_address="192.168.1.100",
  78. serial_number="TEST123",
  79. access_code="12345678",
  80. )
  81. return client
  82. def test_print_complete_includes_timelapse_flag(self, mqtt_client):
  83. """Verify print complete callback includes timelapse_was_active."""
  84. # Set up completion callback
  85. callback_data = {}
  86. def on_complete(data):
  87. callback_data.update(data)
  88. mqtt_client.on_print_complete = on_complete
  89. # Simulate a print that had timelapse active
  90. mqtt_client._was_running = True
  91. mqtt_client._completion_triggered = False
  92. mqtt_client._timelapse_during_print = True
  93. mqtt_client._previous_gcode_state = "RUNNING"
  94. mqtt_client._previous_gcode_file = "test.gcode"
  95. mqtt_client.state.subtask_name = "Test Print"
  96. # Simulate print finish
  97. mqtt_client.state.state = "FINISH"
  98. # Manually trigger the completion logic (simplified)
  99. # In real code this happens in _parse_print_data
  100. should_trigger = (
  101. mqtt_client.state.state in ("FINISH", "FAILED")
  102. and not mqtt_client._completion_triggered
  103. and mqtt_client.on_print_complete
  104. and mqtt_client._previous_gcode_state == "RUNNING"
  105. )
  106. if should_trigger:
  107. status = "completed" if mqtt_client.state.state == "FINISH" else "failed"
  108. timelapse_was_active = mqtt_client._timelapse_during_print
  109. mqtt_client._completion_triggered = True
  110. mqtt_client._was_running = False
  111. mqtt_client._timelapse_during_print = False
  112. mqtt_client.on_print_complete(
  113. {
  114. "status": status,
  115. "filename": mqtt_client._previous_gcode_file,
  116. "subtask_name": mqtt_client.state.subtask_name,
  117. "timelapse_was_active": timelapse_was_active,
  118. }
  119. )
  120. assert "timelapse_was_active" in callback_data
  121. assert callback_data["timelapse_was_active"] is True
  122. def test_print_complete_timelapse_flag_false_when_no_timelapse(self, mqtt_client):
  123. """Verify timelapse_was_active is False when no timelapse during print."""
  124. callback_data = {}
  125. def on_complete(data):
  126. callback_data.update(data)
  127. mqtt_client.on_print_complete = on_complete
  128. # Print without timelapse
  129. mqtt_client._was_running = True
  130. mqtt_client._completion_triggered = False
  131. mqtt_client._timelapse_during_print = False # No timelapse
  132. mqtt_client._previous_gcode_state = "RUNNING"
  133. mqtt_client._previous_gcode_file = "test.gcode"
  134. mqtt_client.state.subtask_name = "Test Print"
  135. mqtt_client.state.state = "FINISH"
  136. # Trigger completion
  137. timelapse_was_active = mqtt_client._timelapse_during_print
  138. mqtt_client.on_print_complete(
  139. {
  140. "status": "completed",
  141. "filename": mqtt_client._previous_gcode_file,
  142. "subtask_name": mqtt_client.state.subtask_name,
  143. "timelapse_was_active": timelapse_was_active,
  144. }
  145. )
  146. assert callback_data["timelapse_was_active"] is False
  147. def test_timelapse_flag_reset_after_completion(self, mqtt_client):
  148. """Verify _timelapse_during_print is reset after print completion."""
  149. mqtt_client._timelapse_during_print = True
  150. mqtt_client._was_running = True
  151. mqtt_client._completion_triggered = False
  152. # Simulate completion reset
  153. mqtt_client._completion_triggered = True
  154. mqtt_client._was_running = False
  155. mqtt_client._timelapse_during_print = False
  156. assert mqtt_client._timelapse_during_print is False
  157. class TestRealisticMessageFlow:
  158. """Tests that simulate realistic MQTT message sequences.
  159. These tests process messages through _process_message to test the full flow,
  160. including the order of xcam parsing vs state detection.
  161. """
  162. @pytest.fixture
  163. def mqtt_client(self):
  164. """Create a BambuMQTTClient instance for testing."""
  165. from backend.app.services.bambu_mqtt import BambuMQTTClient
  166. client = BambuMQTTClient(
  167. ip_address="192.168.1.100",
  168. serial_number="TEST123",
  169. access_code="12345678",
  170. )
  171. return client
  172. def test_timelapse_detected_at_print_start_in_same_message(self, mqtt_client):
  173. """Test that timelapse is detected when xcam and state come in same message.
  174. This is the critical race condition test - xcam data is parsed BEFORE
  175. state detection, so the timelapse flag must be set AFTER _was_running is True.
  176. """
  177. # Callbacks to track events
  178. start_callback_data = {}
  179. def on_start(data):
  180. start_callback_data.update(data)
  181. mqtt_client.on_print_start = on_start
  182. # Initial state - idle
  183. mqtt_client._was_running = False
  184. mqtt_client._timelapse_during_print = False
  185. mqtt_client._previous_gcode_state = None
  186. # Simulate first message when print starts - contains both xcam and gcode_state
  187. # This is the realistic scenario from the printer
  188. # NOTE: Real MQTT messages wrap print data inside a "print" key
  189. payload = {
  190. "print": {
  191. "gcode_state": "RUNNING",
  192. "gcode_file": "/data/Metadata/test_print.gcode",
  193. "subtask_name": "Test_Print",
  194. "xcam": {
  195. "timelapse": "enable", # Timelapse is enabled in this print
  196. "printing_monitor": True,
  197. },
  198. "mc_percent": 0,
  199. "mc_remaining_time": 3600,
  200. }
  201. }
  202. # Process the message (this is what happens in real MQTT flow)
  203. mqtt_client._process_message(payload)
  204. # Verify timelapse was detected even though xcam is parsed before state
  205. assert mqtt_client._was_running is True, "_was_running should be True after RUNNING state"
  206. assert mqtt_client.state.timelapse is True, "state.timelapse should be True"
  207. assert (
  208. mqtt_client._timelapse_during_print is True
  209. ), "timelapse_during_print should be True when timelapse is in the same message as RUNNING state"
  210. def test_timelapse_not_detected_when_disabled(self, mqtt_client):
  211. """Test that timelapse is NOT detected when disabled in xcam data."""
  212. mqtt_client.on_print_start = lambda data: None
  213. # Initial state - idle
  214. mqtt_client._was_running = False
  215. mqtt_client._timelapse_during_print = False
  216. mqtt_client._previous_gcode_state = None
  217. # Print starts without timelapse
  218. payload = {
  219. "print": {
  220. "gcode_state": "RUNNING",
  221. "gcode_file": "/data/Metadata/test_print.gcode",
  222. "subtask_name": "Test_Print",
  223. "xcam": {
  224. "timelapse": "disable", # Timelapse is disabled
  225. "printing_monitor": True,
  226. },
  227. }
  228. }
  229. mqtt_client._process_message(payload)
  230. assert mqtt_client._was_running is True
  231. assert mqtt_client.state.timelapse is False
  232. assert mqtt_client._timelapse_during_print is False
  233. def test_timelapse_detected_when_enabled_after_print_start(self, mqtt_client):
  234. """Test timelapse detected when enabled in a message after print starts."""
  235. mqtt_client.on_print_start = lambda data: None
  236. # First message - print starts without timelapse info
  237. payload_start = {
  238. "print": {
  239. "gcode_state": "RUNNING",
  240. "gcode_file": "/data/Metadata/test_print.gcode",
  241. "subtask_name": "Test_Print",
  242. }
  243. }
  244. mqtt_client._process_message(payload_start)
  245. assert mqtt_client._was_running is True
  246. assert mqtt_client._timelapse_during_print is False # Not detected yet
  247. # Second message - xcam data arrives with timelapse enabled
  248. payload_xcam = {
  249. "print": {
  250. "gcode_state": "RUNNING",
  251. "gcode_file": "/data/Metadata/test_print.gcode",
  252. "subtask_name": "Test_Print",
  253. "xcam": {
  254. "timelapse": "enable",
  255. },
  256. }
  257. }
  258. mqtt_client._process_message(payload_xcam)
  259. # Now timelapse should be detected because _was_running is already True
  260. assert mqtt_client._timelapse_during_print is True
  261. def test_print_complete_includes_timelapse_flag_full_flow(self, mqtt_client):
  262. """Test full print lifecycle with timelapse - from start to completion."""
  263. start_data = {}
  264. complete_data = {}
  265. def on_start(data):
  266. start_data.update(data)
  267. def on_complete(data):
  268. complete_data.update(data)
  269. mqtt_client.on_print_start = on_start
  270. mqtt_client.on_print_complete = on_complete
  271. # 1. Print starts with timelapse
  272. mqtt_client._process_message(
  273. {
  274. "print": {
  275. "gcode_state": "RUNNING",
  276. "gcode_file": "/data/Metadata/test.gcode",
  277. "subtask_name": "Test",
  278. "xcam": {"timelapse": "enable"},
  279. }
  280. }
  281. )
  282. assert mqtt_client._timelapse_during_print is True
  283. assert "subtask_name" in start_data
  284. # 2. Print continues (multiple messages)
  285. for _ in range(3):
  286. mqtt_client._process_message(
  287. {
  288. "print": {
  289. "gcode_state": "RUNNING",
  290. "gcode_file": "/data/Metadata/test.gcode",
  291. "subtask_name": "Test",
  292. "mc_percent": 50,
  293. }
  294. }
  295. )
  296. # Timelapse flag should still be True
  297. assert mqtt_client._timelapse_during_print is True
  298. # 3. Print completes
  299. mqtt_client._process_message(
  300. {
  301. "print": {
  302. "gcode_state": "FINISH",
  303. "gcode_file": "/data/Metadata/test.gcode",
  304. "subtask_name": "Test",
  305. }
  306. }
  307. )
  308. # Verify completion callback received timelapse flag
  309. assert "timelapse_was_active" in complete_data
  310. assert complete_data["timelapse_was_active"] is True
  311. assert complete_data["status"] == "completed"
  312. # Flags should be reset after completion
  313. assert mqtt_client._timelapse_during_print is False
  314. assert mqtt_client._was_running is False
  315. def test_print_failed_includes_timelapse_flag(self, mqtt_client):
  316. """Test that failed print also includes timelapse flag."""
  317. complete_data = {}
  318. def on_complete(data):
  319. complete_data.update(data)
  320. mqtt_client.on_print_start = lambda data: None
  321. mqtt_client.on_print_complete = on_complete
  322. # Start with timelapse
  323. mqtt_client._process_message(
  324. {
  325. "print": {
  326. "gcode_state": "RUNNING",
  327. "gcode_file": "/data/Metadata/test.gcode",
  328. "subtask_name": "Test",
  329. "xcam": {"timelapse": "enable"},
  330. }
  331. }
  332. )
  333. # Print fails
  334. mqtt_client._process_message(
  335. {
  336. "print": {
  337. "gcode_state": "FAILED",
  338. "gcode_file": "/data/Metadata/test.gcode",
  339. "subtask_name": "Test",
  340. }
  341. }
  342. )
  343. assert complete_data["timelapse_was_active"] is True
  344. assert complete_data["status"] == "failed"