test_printer_manager_tray_carryover_2876.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """Dropping a printer's client must not forget what filament it had (#2876).
  2. The queue's smart-plug step reads a printer's last known trays to decide
  3. which machine is worth switching on for a job. It gets there through
  4. ``_power_on_and_wait``, which calls ``connect_printer`` in a retry loop --
  5. and that replaces the client object, taking its status with it. Before
  6. this, every attempt to wake a printer that did not come back erased the
  7. reading the next attempt depends on, so a farm that had been power-cycled
  8. a few times knew nothing about itself and went back to switching machines
  9. on one at a time.
  10. The record is kept beside the clients rather than inside one: it answers
  11. "what did this printer last have loaded", which is not the same question
  12. as "what is it reporting now", and nothing that displays or merges live
  13. status may confuse the two.
  14. """
  15. from __future__ import annotations
  16. from types import SimpleNamespace
  17. from unittest.mock import MagicMock, patch
  18. import pytest
  19. from backend.app.services.bambu_mqtt import PrinterState
  20. from backend.app.services.printer_manager import PrinterManager
  21. AMS = [{"id": 0, "tray": [{"tray_type": "PLA", "tray_color": "161616FF"}]}]
  22. VT_TRAY = [{"id": 254, "tray_type": "ASA", "tray_color": "F98C36FF"}]
  23. @pytest.fixture
  24. def manager():
  25. return PrinterManager()
  26. def _printer():
  27. return SimpleNamespace(
  28. id=1,
  29. name="X1C-1",
  30. ip_address="10.0.0.1",
  31. access_code="x",
  32. serial_number="X1C0001",
  33. model="X1C",
  34. )
  35. def _seed(manager, raw_data):
  36. """Put a client holding ``raw_data`` where the manager will replace it."""
  37. client = MagicMock()
  38. client.state = PrinterState()
  39. client.state.raw_data = raw_data
  40. manager._clients[1] = client
  41. return client
  42. async def _connect(manager):
  43. """Run ``connect_printer`` against a fake MQTT client, return that client."""
  44. created = MagicMock()
  45. created.state = PrinterState()
  46. with patch("backend.app.services.printer_manager.BambuMQTTClient", return_value=created):
  47. await manager.connect_printer(_printer())
  48. return created
  49. @pytest.mark.asyncio
  50. async def test_the_loaded_filament_survives_a_reconnect(manager):
  51. _seed(manager, {"ams": AMS, "vt_tray": VT_TRAY})
  52. await _connect(manager)
  53. assert manager.last_known_trays(1) == {"ams": AMS, "vt_tray": VT_TRAY}
  54. def test_the_loaded_filament_survives_a_plain_disconnect(manager):
  55. _seed(manager, {"ams": AMS})
  56. manager.disconnect_printer(1)
  57. assert manager.last_known_trays(1) == {"ams": AMS}
  58. @pytest.mark.asyncio
  59. async def test_the_new_client_starts_empty(manager):
  60. """The record is history, not status.
  61. Writing it into the reconnecting client's ``raw_data`` would put it in
  62. front of everything that reads live status -- and the AMS merge is
  63. additive, so an AMS unit unplugged while the printer was off would be
  64. merged straight back in and never leave again.
  65. """
  66. _seed(manager, {"ams": AMS, "vt_tray": VT_TRAY})
  67. created = await _connect(manager)
  68. assert created.state.raw_data == {}
  69. @pytest.mark.asyncio
  70. async def test_nothing_but_the_trays_is_kept(manager):
  71. """A stale temperature or print state read as current would be worse
  72. than nothing; what is physically loaded survives a power cycle."""
  73. _seed(manager, {"ams": AMS, "bed_temper": 60.0, "gcode_state": "RUNNING", "layer_num": 42})
  74. await _connect(manager)
  75. assert manager.last_known_trays(1) == {"ams": AMS}
  76. @pytest.mark.asyncio
  77. async def test_a_first_connection_has_nothing_to_remember(manager):
  78. await _connect(manager)
  79. assert manager.last_known_trays(1) == {}
  80. @pytest.mark.asyncio
  81. async def test_an_empty_reading_is_not_remembered_as_a_reading(manager):
  82. """A printer that reported no trays leaves no trays behind.
  83. The queue reads an absent record as "we have never heard" and switches
  84. the printer on anyway; storing empty lists would say the same thing in
  85. a shape that is harder to tell from a real answer.
  86. """
  87. _seed(manager, {"ams": [], "vt_tray": []})
  88. await _connect(manager)
  89. assert manager.last_known_trays(1) == {}
  90. @pytest.mark.asyncio
  91. async def test_a_later_reading_replaces_the_remembered_one(manager):
  92. _seed(manager, {"vt_tray": VT_TRAY})
  93. await _connect(manager)
  94. _seed(manager, {"vt_tray": [{"id": 254, "tray_type": "PETG", "tray_color": "00FF00FF"}]})
  95. await _connect(manager)
  96. assert manager.last_known_trays(1)["vt_tray"][0]["tray_type"] == "PETG"