test_request_topic_probe_2953.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. """The request-topic probe must not latch on a single unexplained drop (#2953).
  2. Bambuddy subscribes to the printer's own request topic to intercept the
  3. ``ams_mapping`` a slicer sends with a print. A1-class printers refuse: their
  4. broker kills the TCP connection instead of returning a SUBACK failure, so the
  5. only signal is "we subscribed and then got disconnected". That signal is
  6. circumstantial. Every other reason a connection can drop inside the same few
  7. seconds -- a network blip, the printer rebooting, the container being stopped
  8. mid-probe -- looks exactly the same.
  9. Latching on the first one costs ams_mapping capture for the rest of the
  10. process on a printer that supports the topic perfectly well, and every Studio
  11. print after that is charged to a spool picked by tray position. Requiring the
  12. drop to repeat costs a printer that genuinely refuses one extra reconnect.
  13. """
  14. from types import SimpleNamespace
  15. import pytest
  16. from backend.app.services.bambu_mqtt import BambuMQTTClient
  17. @pytest.fixture(autouse=True)
  18. def _clear_class_state():
  19. BambuMQTTClient._request_topic_cache.clear()
  20. BambuMQTTClient._request_topic_probe_failures.clear()
  21. yield
  22. BambuMQTTClient._request_topic_cache.clear()
  23. BambuMQTTClient._request_topic_probe_failures.clear()
  24. def _client(serial="SER2953"):
  25. client = BambuMQTTClient(ip_address="10.0.0.9", serial_number=serial, access_code="12345678")
  26. client._stale_reconnecting = False
  27. client._last_message_time = 0.0
  28. client.last_connect_error = None
  29. return client
  30. def _mid_probe(client):
  31. """Put the client where it is right after subscribing to the request topic."""
  32. import time
  33. client._request_topic_sub_mid = 7
  34. client._request_topic_sub_time = time.time()
  35. client._request_topic_confirmed = False
  36. def _drop(client):
  37. client._on_disconnect(None, None, disconnect_flags=None, rc=SimpleNamespace(is_failure=True))
  38. def test_one_drop_keeps_the_request_topic_enabled():
  39. """A blip, as far as we can tell. Try again on the next connection."""
  40. client = _client()
  41. _mid_probe(client)
  42. _drop(client)
  43. assert client._request_topic_supported is True
  44. assert BambuMQTTClient._request_topic_cache.get("SER2953") is None
  45. assert BambuMQTTClient._request_topic_probe_failures["SER2953"] == 1
  46. def test_a_second_drop_disables_it():
  47. """The A1's answer: same response every time. Stop asking, and stop
  48. causing a reconnect on every connection."""
  49. client = _client()
  50. _mid_probe(client)
  51. _drop(client)
  52. _mid_probe(client)
  53. _drop(client)
  54. assert client._request_topic_supported is False
  55. assert BambuMQTTClient._request_topic_cache["SER2953"] is False
  56. def test_a_new_client_for_a_disabled_printer_does_not_re_probe():
  57. """Unchanged: once disabled, later instances skip the subscription
  58. entirely rather than reopening the reconnect loop."""
  59. client = _client()
  60. _mid_probe(client)
  61. _drop(client)
  62. _mid_probe(client)
  63. _drop(client)
  64. assert _client()._request_topic_supported is False
  65. def test_a_successful_suback_clears_the_count():
  66. """A printer that answered once has answered. A later isolated drop must
  67. start from zero, not from a half-spent budget."""
  68. client = _client()
  69. _mid_probe(client)
  70. _drop(client)
  71. assert BambuMQTTClient._request_topic_probe_failures["SER2953"] == 1
  72. client._request_topic_sub_mid = 7
  73. client._on_subscribe(None, None, 7, [SimpleNamespace(is_failure=False, value=0, getName=lambda: "ok")])
  74. assert BambuMQTTClient._request_topic_cache["SER2953"] is True
  75. assert "SER2953" not in BambuMQTTClient._request_topic_probe_failures
  76. def test_a_suback_rejection_still_disables_immediately():
  77. """A SUBACK failure is the broker answering the question, not evidence
  78. about it. One is enough."""
  79. client = _client()
  80. client._request_topic_sub_mid = 7
  81. client._on_subscribe(None, None, 7, [SimpleNamespace(is_failure=True, value=135, getName=lambda: "Not authorized")])
  82. assert client._request_topic_supported is False
  83. assert BambuMQTTClient._request_topic_cache["SER2953"] is False
  84. def test_a_disconnect_we_asked_for_is_not_evidence():
  85. """Shutting the container down mid-probe used to count against the
  86. printer. ``disconnect()`` sets the event before closing the socket."""
  87. import threading
  88. client = _client()
  89. _mid_probe(client)
  90. client._disconnection_event = threading.Event()
  91. _drop(client)
  92. assert client._request_topic_supported is True
  93. assert "SER2953" not in BambuMQTTClient._request_topic_probe_failures