test_spoolman_assignment_verification.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """A Spoolman slot assignment is checked against the printer's read-back (#2582).
  2. The built-in assign registers what it pushed, so the AMS pushes that follow
  3. can confirm the tray took it and a "not confirmed" notice appears when it did
  4. not. The Spoolman assign and link published the same commands and never
  5. registered anything: a printer that ignored them left the slot on its old
  6. filament with no notice, and usage went to the wrong spool.
  7. """
  8. from unittest.mock import AsyncMock, MagicMock, patch
  9. import pytest
  10. from httpx import AsyncClient
  11. SPOOL = {
  12. "id": 10,
  13. "filament": {"id": 1, "name": "PLA Basic", "material": "PLA", "color_hex": "FF0000", "weight": 1000},
  14. "remaining_weight": 800.0,
  15. "used_weight": 200.0,
  16. "archived": False,
  17. "extra": {},
  18. }
  19. @pytest.fixture
  20. async def printer_id(db_session, printer_factory):
  21. from backend.app.models.settings import Settings
  22. db_session.add(Settings(key="spoolman_enabled", value="true"))
  23. db_session.add(Settings(key="spoolman_url", value="http://localhost:7912"))
  24. await db_session.commit()
  25. return (await printer_factory()).id
  26. def _spoolman_client():
  27. client = MagicMock()
  28. client.base_url = "http://localhost:7912"
  29. client.health_check = AsyncMock(return_value=True)
  30. client.get_spool = AsyncMock(return_value=SPOOL)
  31. client.get_spools = AsyncMock(return_value=[])
  32. client.merge_spool_extra = AsyncMock(return_value={"id": 10, "extra": {}})
  33. client.get_distinct_locations = AsyncMock(return_value=[])
  34. client.has_tag_api = AsyncMock(return_value=False)
  35. client.add_native_tags = AsyncMock(return_value=0)
  36. client.ensure_tag_extra_field = AsyncMock(return_value=True)
  37. client.ensure_extra_field = AsyncMock(return_value=True)
  38. return client
  39. def _printer_client():
  40. mqtt = MagicMock()
  41. mqtt.ams_set_filament_setting = MagicMock()
  42. mqtt.extrusion_cali_sel = MagicMock()
  43. mqtt.register_assignment_verification = MagicMock()
  44. return mqtt
  45. @pytest.mark.asyncio
  46. @pytest.mark.integration
  47. async def test_an_assignment_is_registered_for_read_back(async_client: AsyncClient, printer_id):
  48. mqtt = _printer_client()
  49. with (
  50. patch("backend.app.api.routes.spoolman_inventory._get_client", AsyncMock(return_value=_spoolman_client())),
  51. patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm,
  52. ):
  53. pm.get_client = MagicMock(return_value=mqtt)
  54. pm.get_status = MagicMock(return_value=None)
  55. response = await async_client.post(
  56. "/api/v1/spoolman/inventory/slot-assignments",
  57. json={"spoolman_spool_id": 10, "printer_id": printer_id, "ams_id": 0, "tray_id": 1},
  58. )
  59. assert response.status_code == 200
  60. pushed = mqtt.ams_set_filament_setting.call_args.kwargs
  61. mqtt.register_assignment_verification.assert_called_once_with(
  62. ams_id=0, tray_id=1, tray_info_idx=pushed["tray_info_idx"], tray_color=pushed["tray_color"], cali_idx=-1
  63. )
  64. @pytest.mark.asyncio
  65. @pytest.mark.integration
  66. async def test_a_link_with_a_slot_is_registered_for_read_back(async_client: AsyncClient, printer_id):
  67. mqtt = _printer_client()
  68. with (
  69. patch("backend.app.api.routes.spoolman.get_spoolman_client", AsyncMock(return_value=_spoolman_client())),
  70. patch("backend.app.api.routes.spoolman.printer_manager") as pm,
  71. ):
  72. pm.get_client = MagicMock(return_value=mqtt)
  73. pm.get_status = MagicMock(return_value=None)
  74. pm.get_model = MagicMock(return_value="X1C")
  75. response = await async_client.post(
  76. "/api/v1/spoolman/spools/10/link",
  77. json={"tray_uuid": "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4", "printer_id": printer_id, "ams_id": 0, "tray_id": 2},
  78. )
  79. assert response.status_code == 200, response.text
  80. pushed = mqtt.ams_set_filament_setting.call_args.kwargs
  81. # No stored K-profile and none live: the link leaves the K-profile alone,
  82. # so only the filament is checked.
  83. mqtt.register_assignment_verification.assert_called_once_with(
  84. ams_id=0, tray_id=2, tray_info_idx=pushed["tray_info_idx"], tray_color=pushed["tray_color"], cali_idx=None
  85. )
  86. @pytest.mark.asyncio
  87. @pytest.mark.integration
  88. async def test_an_assignment_to_an_empty_slot_is_not_checked(async_client: AsyncClient, printer_id):
  89. """An empty slot echoes nothing back until a spool goes in, so a check
  90. could only time out into a false "not confirmed"."""
  91. from types import SimpleNamespace
  92. mqtt = _printer_client()
  93. state = SimpleNamespace(
  94. raw_data={"ams": [{"id": 0, "tray": [{"id": 1, "state": 9}]}]},
  95. nozzles=[],
  96. ams_extruder_map={},
  97. ams_switch_inlet=None,
  98. kprofiles=[],
  99. )
  100. with (
  101. patch("backend.app.api.routes.spoolman_inventory._get_client", AsyncMock(return_value=_spoolman_client())),
  102. patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm,
  103. ):
  104. pm.get_client = MagicMock(return_value=mqtt)
  105. pm.get_status = MagicMock(return_value=state)
  106. response = await async_client.post(
  107. "/api/v1/spoolman/inventory/slot-assignments",
  108. json={"spoolman_spool_id": 10, "printer_id": printer_id, "ams_id": 0, "tray_id": 1},
  109. )
  110. assert response.status_code == 200
  111. mqtt.ams_set_filament_setting.assert_called_once()
  112. mqtt.register_assignment_verification.assert_not_called()