test_preconfig_replay_failure.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """A pre-assigned slot the printer could not be told about is retried (SA-3).
  2. SpoolBuddy assigns a spool to an empty slot with an empty fingerprint; the
  3. slot is configured on the printer once filament goes in. The fingerprint was
  4. recorded even when nothing could be sent (no MQTT client while the printer
  5. reconnects), and a recorded fingerprint ends the replay, so the slot was never
  6. configured.
  7. """
  8. from unittest.mock import AsyncMock, MagicMock, patch
  9. import pytest
  10. from backend.app.models.spool_assignment import SpoolAssignment
  11. from backend.tests.integration.test_inventory_assign import _make_mock_status, spool_factory # noqa: F401
  12. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  13. async def _fingerprint_after_insert(db_session, printer_factory, spool_factory, apply: AsyncMock):
  14. from backend.app.main import on_ams_change
  15. printer = await printer_factory(name="H2D")
  16. spool = await spool_factory(slicer_filament="GFL05", material="PLA")
  17. assignment = SpoolAssignment(spool_id=spool.id, printer_id=printer.id, ams_id=2, tray_id=3)
  18. db_session.add(assignment)
  19. await db_session.commit()
  20. ams_data = [{"id": 2, "tray": [{"id": 3, "tray_type": "PLA", "tray_color": "FF0000FF", "state": 11}]}]
  21. status = _make_mock_status(ams_data=ams_data)
  22. with (
  23. patch("backend.app.api.routes.inventory.apply_spool_to_slot_via_mqtt", apply),
  24. patch("backend.app.main.printer_manager") as pm,
  25. patch("backend.app.main.mqtt_relay") as relay,
  26. patch("backend.app.main.ws_manager") as ws,
  27. ):
  28. pm.get_printer.return_value = MagicMock(name="H2D", serial_number="0948BB540200427")
  29. pm.get_status.return_value = status
  30. pm.get_client.return_value = MagicMock()
  31. pm.get_model.return_value = "H2D"
  32. relay.on_ams_change = AsyncMock()
  33. ws.send_printer_status = AsyncMock()
  34. ws.broadcast = AsyncMock()
  35. await on_ams_change(printer.id, ams_data)
  36. await db_session.refresh(assignment)
  37. apply.assert_awaited_once()
  38. return assignment.fingerprint_type, assignment.fingerprint_color
  39. async def test_a_slot_the_printer_could_not_be_told_about_is_retried(
  40. async_client, db_session, printer_factory, spool_factory
  41. ):
  42. """No MQTT client (reconnecting): nothing was sent."""
  43. apply = AsyncMock(return_value=False)
  44. assert await _fingerprint_after_insert(db_session, printer_factory, spool_factory, apply) == (None, None)
  45. async def test_a_configuration_that_raised_is_not_retried(async_client, db_session, printer_factory, spool_factory):
  46. """It may have sent part of the commands, and would raise again on every
  47. AMS change, mid-print included (remain% steps count as changes)."""
  48. apply = AsyncMock(side_effect=RuntimeError("preset lookup failed"))
  49. assert await _fingerprint_after_insert(db_session, printer_factory, spool_factory, apply) == ("PLA", "FF0000FF")
  50. async def test_a_configured_slot_records_its_fingerprint(async_client, db_session, printer_factory, spool_factory):
  51. apply = AsyncMock(return_value=True)
  52. assert await _fingerprint_after_insert(db_session, printer_factory, spool_factory, apply) == ("PLA", "FF0000FF")
  53. async def _replay(db_session, printer_factory, spool_factory, *, state, tray_now=255, extruder_slots=None):
  54. """One AMS change for a pre-assigned AMS 2 slot 3 (global tray 11) that now holds filament."""
  55. from backend.app.main import on_ams_change
  56. printer = await printer_factory(name="A1 Mini")
  57. spool = await spool_factory(slicer_filament="GFL99", material="PLA")
  58. assignment = SpoolAssignment(spool_id=spool.id, printer_id=printer.id, ams_id=2, tray_id=3)
  59. db_session.add(assignment)
  60. await db_session.commit()
  61. ams_data = [{"id": 2, "tray": [{"id": 3, "tray_type": "PLA", "tray_color": "FF0000FF", "state": 11}]}]
  62. status = _make_mock_status(ams_data=ams_data)
  63. status.state = state
  64. status.tray_now = tray_now
  65. status.extruder_slots = extruder_slots or {}
  66. apply = AsyncMock(return_value=True)
  67. with (
  68. patch("backend.app.api.routes.inventory.apply_spool_to_slot_via_mqtt", apply),
  69. patch("backend.app.main.printer_manager") as pm,
  70. patch("backend.app.main.mqtt_relay") as relay,
  71. patch("backend.app.main.ws_manager") as ws,
  72. ):
  73. pm.get_printer.return_value = MagicMock(name="A1 Mini", serial_number="0309CA000000001")
  74. pm.get_status.return_value = status
  75. pm.get_client.return_value = MagicMock()
  76. pm.get_model.return_value = "A1 Mini"
  77. relay.on_ams_change = AsyncMock()
  78. ws.send_printer_status = AsyncMock()
  79. ws.broadcast = AsyncMock()
  80. await on_ams_change(printer.id, ams_data)
  81. await db_session.refresh(assignment)
  82. return apply, assignment
  83. class TestReplayIntoTheSlotFeedingAPrint:
  84. """#3434: a spool swapped in after a runout is assigned while the print runs
  85. from that slot. The printer refuses a setting for it then, and the replay
  86. sent one per AMS change for the rest of the print."""
  87. async def test_held_while_the_slot_feeds_the_print(self, async_client, db_session, printer_factory, spool_factory):
  88. apply, assignment = await _replay(db_session, printer_factory, spool_factory, state="RUNNING", tray_now=11)
  89. apply.assert_not_awaited()
  90. # Nothing recorded, so the first AMS change after the print still replays.
  91. assert assignment.fingerprint_type is None
  92. async def test_held_for_a_slot_an_extruder_is_fed_from(
  93. self, async_client, db_session, printer_factory, spool_factory
  94. ):
  95. """Dual nozzle: tray_now names the other hotend's slot."""
  96. from backend.app.services.bambu_mqtt import ExtruderSlot
  97. slots = {0: ExtruderSlot(ams_id=0, slot_id=0, has_filament=True), 1: ExtruderSlot(2, 3, True)}
  98. apply, _ = await _replay(
  99. db_session, printer_factory, spool_factory, state="RUNNING", tray_now=0, extruder_slots=slots
  100. )
  101. apply.assert_not_awaited()
  102. async def test_another_slot_is_configured_mid_print(self, async_client, db_session, printer_factory, spool_factory):
  103. apply, assignment = await _replay(db_session, printer_factory, spool_factory, state="RUNNING", tray_now=0)
  104. apply.assert_awaited_once()
  105. assert assignment.fingerprint_type == "PLA"
  106. async def test_configured_once_the_print_is_over(self, async_client, db_session, printer_factory, spool_factory):
  107. apply, assignment = await _replay(db_session, printer_factory, spool_factory, state="FINISH", tray_now=11)
  108. apply.assert_awaited_once()
  109. assert assignment.fingerprint_type == "PLA"