test_kprofile_save_confirmation_g7.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. """A K-profile save says whether the printer confirmed it.
  2. The printer acknowledges K-profile writes, but firmware that predates the ack
  3. never answers, so a missing answer is not a refusal. The routes reported such a
  4. write as saved all the same. They now say it was sent and not confirmed.
  5. An edit on a single-nozzle printer deletes the profile and adds it again: if
  6. the add did not land, the profile is gone. That edit now reads the table back.
  7. """
  8. from types import SimpleNamespace
  9. from unittest.mock import AsyncMock, MagicMock, patch
  10. import pytest
  11. from httpx import AsyncClient
  12. from backend.app.services.bambu_mqtt import CALI_ACK_UNCONFIRMED, KProfile
  13. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  14. PROFILE = {
  15. "slot_id": 0,
  16. "extruder_id": 0,
  17. "nozzle_id": "HS00-0.4",
  18. "nozzle_diameter": "0.4",
  19. "filament_id": "GFA00",
  20. "name": "Bambu PLA Basic",
  21. "k_value": "0.020000",
  22. }
  23. def _client(ack, table=None):
  24. client = MagicMock()
  25. client.state = SimpleNamespace(connected=True)
  26. client._is_dual_nozzle = False
  27. client.set_kprofile.return_value = "11"
  28. client.delete_kprofile.return_value = "10"
  29. client.set_kprofiles_batch.return_value = "12"
  30. client.await_cali_ack = AsyncMock(return_value=ack)
  31. client.get_kprofiles = AsyncMock(return_value=table or [])
  32. return client
  33. def _stored(**overrides) -> KProfile:
  34. fields = {
  35. "slot_id": 4,
  36. "extruder_id": 0,
  37. "nozzle_id": "HS00-0.4",
  38. "nozzle_diameter": "0.4",
  39. "filament_id": "GFA00",
  40. "name": "Bambu PLA Basic",
  41. "k_value": "0.020000",
  42. }
  43. fields.update(overrides)
  44. return KProfile(**fields)
  45. async def _post(async_client: AsyncClient, printer_factory, client, path="", body=None, method="post"):
  46. printer = await printer_factory(model="X1C")
  47. with (
  48. patch("backend.app.api.routes.kprofiles.printer_manager") as pm,
  49. patch("backend.app.api.routes.kprofiles.asyncio.sleep", AsyncMock()),
  50. ):
  51. pm.get_client.return_value = client
  52. url = f"/api/v1/printers/{printer.id}/kprofiles/{path}"
  53. if method == "delete":
  54. return await async_client.request("DELETE", url, json=body)
  55. return await async_client.post(url, json=body)
  56. async def test_a_confirmed_save_says_so(async_client, printer_factory):
  57. response = await _post(async_client, printer_factory, _client((True, "")), body=PROFILE)
  58. assert response.status_code == 200
  59. assert response.json()["confirmed"] is True
  60. async def test_a_save_the_printer_did_not_answer_is_unconfirmed(async_client, printer_factory):
  61. response = await _post(async_client, printer_factory, _client((True, CALI_ACK_UNCONFIRMED)), body=PROFILE)
  62. assert response.status_code == 200
  63. assert response.json()["confirmed"] is False
  64. @pytest.mark.parametrize(
  65. ("path", "body", "method"),
  66. [
  67. ("batch", [PROFILE], "post"),
  68. ("", {**PROFILE, "slot_id": 4, "setting_id": None}, "delete"),
  69. ],
  70. )
  71. async def test_batch_and_delete_say_it_too(async_client, printer_factory, path, body, method):
  72. response = await _post(
  73. async_client, printer_factory, _client((True, CALI_ACK_UNCONFIRMED)), path=path, body=body, method=method
  74. )
  75. assert response.status_code == 200
  76. assert response.json()["confirmed"] is False
  77. class TestASingleNozzleEditIsReadBack:
  78. EDIT = {**PROFILE, "slot_id": 4, "k_value": "0.025000"}
  79. async def test_the_edited_profile_in_the_table_confirms_it(self, async_client, printer_factory):
  80. client = _client((True, ""), table=[_stored(slot_id=5, k_value="0.025000")])
  81. # The delete was answered, the add was not: the table settles it.
  82. client.await_cali_ack = AsyncMock(side_effect=[(True, ""), (True, CALI_ACK_UNCONFIRMED)])
  83. response = await _post(async_client, printer_factory, client, body=self.EDIT)
  84. assert response.status_code == 200
  85. assert response.json()["confirmed"] is True
  86. async def test_a_lost_profile_is_an_error(self, async_client, printer_factory):
  87. """The delete landed, the add did not: the table has other profiles but not this one."""
  88. client = _client((True, ""), table=[_stored(slot_id=2, name="Other", filament_id="GFB00")])
  89. response = await _post(async_client, printer_factory, client, body=self.EDIT)
  90. assert response.status_code == 502
  91. assert "not in the printer's table" in response.json()["detail"]
  92. async def test_the_old_value_still_in_the_table_is_an_error(self, async_client, printer_factory):
  93. client = _client((True, ""), table=[_stored(slot_id=4, k_value="0.020000")])
  94. response = await _post(async_client, printer_factory, client, body=self.EDIT)
  95. assert response.status_code == 502
  96. async def test_an_unreadable_table_leaves_it_to_the_ack(self, async_client, printer_factory):
  97. """An empty answer is also what a read that timed out returns."""
  98. client = _client((True, CALI_ACK_UNCONFIRMED), table=[])
  99. response = await _post(async_client, printer_factory, client, body=self.EDIT)
  100. assert response.status_code == 200
  101. assert response.json()["confirmed"] is False
  102. async def test_an_unanswered_delete_leaves_it_unconfirmed(self, async_client, printer_factory):
  103. """The edited profile landed, but the old one may still be there too."""
  104. client = _client((True, ""), table=[_stored(slot_id=5, k_value="0.025000")])
  105. client.await_cali_ack = AsyncMock(side_effect=[(True, CALI_ACK_UNCONFIRMED), (True, "")])
  106. response = await _post(async_client, printer_factory, client, body=self.EDIT)
  107. assert response.status_code == 200
  108. assert response.json()["confirmed"] is False