test_bambu_mqtt.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. """
  2. Tests for the BambuMQTTClient service.
  3. These tests focus on timelapse tracking during prints.
  4. """
  5. import pytest
  6. from unittest.mock import MagicMock, patch
  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. "status": status,
  114. "filename": mqtt_client._previous_gcode_file,
  115. "subtask_name": mqtt_client.state.subtask_name,
  116. "timelapse_was_active": timelapse_was_active,
  117. })
  118. assert "timelapse_was_active" in callback_data
  119. assert callback_data["timelapse_was_active"] is True
  120. def test_print_complete_timelapse_flag_false_when_no_timelapse(self, mqtt_client):
  121. """Verify timelapse_was_active is False when no timelapse during print."""
  122. callback_data = {}
  123. def on_complete(data):
  124. callback_data.update(data)
  125. mqtt_client.on_print_complete = on_complete
  126. # Print without timelapse
  127. mqtt_client._was_running = True
  128. mqtt_client._completion_triggered = False
  129. mqtt_client._timelapse_during_print = False # No timelapse
  130. mqtt_client._previous_gcode_state = "RUNNING"
  131. mqtt_client._previous_gcode_file = "test.gcode"
  132. mqtt_client.state.subtask_name = "Test Print"
  133. mqtt_client.state.state = "FINISH"
  134. # Trigger completion
  135. timelapse_was_active = mqtt_client._timelapse_during_print
  136. mqtt_client.on_print_complete({
  137. "status": "completed",
  138. "filename": mqtt_client._previous_gcode_file,
  139. "subtask_name": mqtt_client.state.subtask_name,
  140. "timelapse_was_active": timelapse_was_active,
  141. })
  142. assert callback_data["timelapse_was_active"] is False
  143. def test_timelapse_flag_reset_after_completion(self, mqtt_client):
  144. """Verify _timelapse_during_print is reset after print completion."""
  145. mqtt_client._timelapse_during_print = True
  146. mqtt_client._was_running = True
  147. mqtt_client._completion_triggered = False
  148. # Simulate completion reset
  149. mqtt_client._completion_triggered = True
  150. mqtt_client._was_running = False
  151. mqtt_client._timelapse_during_print = False
  152. assert mqtt_client._timelapse_during_print is False
  153. class TestRealisticMessageFlow:
  154. """Tests that simulate realistic MQTT message sequences.
  155. These tests process messages through _process_message to test the full flow,
  156. including the order of xcam parsing vs state detection.
  157. """
  158. @pytest.fixture
  159. def mqtt_client(self):
  160. """Create a BambuMQTTClient instance for testing."""
  161. from backend.app.services.bambu_mqtt import BambuMQTTClient
  162. client = BambuMQTTClient(
  163. ip_address="192.168.1.100",
  164. serial_number="TEST123",
  165. access_code="12345678",
  166. )
  167. return client
  168. def test_timelapse_detected_at_print_start_in_same_message(self, mqtt_client):
  169. """Test that timelapse is detected when xcam and state come in same message.
  170. This is the critical race condition test - xcam data is parsed BEFORE
  171. state detection, so the timelapse flag must be set AFTER _was_running is True.
  172. """
  173. # Callbacks to track events
  174. start_callback_data = {}
  175. def on_start(data):
  176. start_callback_data.update(data)
  177. mqtt_client.on_print_start = on_start
  178. # Initial state - idle
  179. mqtt_client._was_running = False
  180. mqtt_client._timelapse_during_print = False
  181. mqtt_client._previous_gcode_state = None
  182. # Simulate first message when print starts - contains both xcam and gcode_state
  183. # This is the realistic scenario from the printer
  184. # NOTE: Real MQTT messages wrap print data inside a "print" key
  185. payload = {
  186. "print": {
  187. "gcode_state": "RUNNING",
  188. "gcode_file": "/data/Metadata/test_print.gcode",
  189. "subtask_name": "Test_Print",
  190. "xcam": {
  191. "timelapse": "enable", # Timelapse is enabled in this print
  192. "printing_monitor": True,
  193. },
  194. "mc_percent": 0,
  195. "mc_remaining_time": 3600,
  196. }
  197. }
  198. # Process the message (this is what happens in real MQTT flow)
  199. mqtt_client._process_message(payload)
  200. # Verify timelapse was detected even though xcam is parsed before state
  201. assert mqtt_client._was_running is True, "_was_running should be True after RUNNING state"
  202. assert mqtt_client.state.timelapse is True, "state.timelapse should be True"
  203. assert mqtt_client._timelapse_during_print is True, (
  204. "timelapse_during_print should be True when timelapse is in the same message as RUNNING state"
  205. )
  206. def test_timelapse_not_detected_when_disabled(self, mqtt_client):
  207. """Test that timelapse is NOT detected when disabled in xcam data."""
  208. mqtt_client.on_print_start = lambda data: None
  209. # Initial state - idle
  210. mqtt_client._was_running = False
  211. mqtt_client._timelapse_during_print = False
  212. mqtt_client._previous_gcode_state = None
  213. # Print starts without timelapse
  214. payload = {
  215. "print": {
  216. "gcode_state": "RUNNING",
  217. "gcode_file": "/data/Metadata/test_print.gcode",
  218. "subtask_name": "Test_Print",
  219. "xcam": {
  220. "timelapse": "disable", # Timelapse is disabled
  221. "printing_monitor": True,
  222. },
  223. }
  224. }
  225. mqtt_client._process_message(payload)
  226. assert mqtt_client._was_running is True
  227. assert mqtt_client.state.timelapse is False
  228. assert mqtt_client._timelapse_during_print is False
  229. def test_timelapse_detected_when_enabled_after_print_start(self, mqtt_client):
  230. """Test timelapse detected when enabled in a message after print starts."""
  231. mqtt_client.on_print_start = lambda data: None
  232. # First message - print starts without timelapse info
  233. payload_start = {
  234. "print": {
  235. "gcode_state": "RUNNING",
  236. "gcode_file": "/data/Metadata/test_print.gcode",
  237. "subtask_name": "Test_Print",
  238. }
  239. }
  240. mqtt_client._process_message(payload_start)
  241. assert mqtt_client._was_running is True
  242. assert mqtt_client._timelapse_during_print is False # Not detected yet
  243. # Second message - xcam data arrives with timelapse enabled
  244. payload_xcam = {
  245. "print": {
  246. "gcode_state": "RUNNING",
  247. "gcode_file": "/data/Metadata/test_print.gcode",
  248. "subtask_name": "Test_Print",
  249. "xcam": {
  250. "timelapse": "enable",
  251. },
  252. }
  253. }
  254. mqtt_client._process_message(payload_xcam)
  255. # Now timelapse should be detected because _was_running is already True
  256. assert mqtt_client._timelapse_during_print is True
  257. def test_print_complete_includes_timelapse_flag_full_flow(self, mqtt_client):
  258. """Test full print lifecycle with timelapse - from start to completion."""
  259. start_data = {}
  260. complete_data = {}
  261. def on_start(data):
  262. start_data.update(data)
  263. def on_complete(data):
  264. complete_data.update(data)
  265. mqtt_client.on_print_start = on_start
  266. mqtt_client.on_print_complete = on_complete
  267. # 1. Print starts with timelapse
  268. mqtt_client._process_message({
  269. "print": {
  270. "gcode_state": "RUNNING",
  271. "gcode_file": "/data/Metadata/test.gcode",
  272. "subtask_name": "Test",
  273. "xcam": {"timelapse": "enable"},
  274. }
  275. })
  276. assert mqtt_client._timelapse_during_print is True
  277. assert "subtask_name" in start_data
  278. # 2. Print continues (multiple messages)
  279. for _ in range(3):
  280. mqtt_client._process_message({
  281. "print": {
  282. "gcode_state": "RUNNING",
  283. "gcode_file": "/data/Metadata/test.gcode",
  284. "subtask_name": "Test",
  285. "mc_percent": 50,
  286. }
  287. })
  288. # Timelapse flag should still be True
  289. assert mqtt_client._timelapse_during_print is True
  290. # 3. Print completes
  291. mqtt_client._process_message({
  292. "print": {
  293. "gcode_state": "FINISH",
  294. "gcode_file": "/data/Metadata/test.gcode",
  295. "subtask_name": "Test",
  296. }
  297. })
  298. # Verify completion callback received timelapse flag
  299. assert "timelapse_was_active" in complete_data
  300. assert complete_data["timelapse_was_active"] is True
  301. assert complete_data["status"] == "completed"
  302. # Flags should be reset after completion
  303. assert mqtt_client._timelapse_during_print is False
  304. assert mqtt_client._was_running is False
  305. def test_print_failed_includes_timelapse_flag(self, mqtt_client):
  306. """Test that failed print also includes timelapse flag."""
  307. complete_data = {}
  308. def on_complete(data):
  309. complete_data.update(data)
  310. mqtt_client.on_print_start = lambda data: None
  311. mqtt_client.on_print_complete = on_complete
  312. # Start with timelapse
  313. mqtt_client._process_message({
  314. "print": {
  315. "gcode_state": "RUNNING",
  316. "gcode_file": "/data/Metadata/test.gcode",
  317. "subtask_name": "Test",
  318. "xcam": {"timelapse": "enable"},
  319. }
  320. })
  321. # Print fails
  322. mqtt_client._process_message({
  323. "print": {
  324. "gcode_state": "FAILED",
  325. "gcode_file": "/data/Metadata/test.gcode",
  326. "subtask_name": "Test",
  327. }
  328. })
  329. assert complete_data["timelapse_was_active"] is True
  330. assert complete_data["status"] == "failed"