test_inventory_ams_weight_sync_lock.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. """The built-in inventory's AMS weight sync, the weight lock and a printing printer.
  2. A weight set by hand or measured on the SpoolBuddy scale is locked, so the
  3. automatic remain% sync leaves it alone. "Sync Weights from AMS" is the explicit
  4. override: it also updates locked spools and releases the lock. It leaves the
  5. spools of a printing printer alone, whose usage the print's completion charges.
  6. """
  7. from datetime import datetime, timezone
  8. from types import SimpleNamespace
  9. from unittest.mock import MagicMock, patch
  10. import pytest
  11. from httpx import AsyncClient
  12. from sqlalchemy import select
  13. from backend.app.models.spool import Spool
  14. from backend.app.models.spool_assignment import SpoolAssignment
  15. from backend.app.models.spoolbuddy_device import SpoolBuddyDevice
  16. async def _assigned_spool(db_session, printer_factory, *, locked: bool) -> int:
  17. printer = await printer_factory()
  18. spool = Spool(material="PLA", label_weight=1000, core_weight=250, weight_used=100, weight_locked=locked)
  19. db_session.add(spool)
  20. await db_session.flush()
  21. db_session.add(SpoolAssignment(spool_id=spool.id, printer_id=printer.id, ams_id=0, tray_id=0))
  22. await db_session.commit()
  23. return spool.id
  24. def _printer(state: str, remain: int = 50, connected: bool = True):
  25. return SimpleNamespace(
  26. state=state, connected=connected, raw_data={"ams": [{"id": 0, "tray": [{"id": 0, "remain": remain}]}]}
  27. )
  28. async def _sync(async_client, state):
  29. with patch("backend.app.services.printer_manager.printer_manager") as pm:
  30. pm.get_status = MagicMock(return_value=state)
  31. return await async_client.post("/api/v1/inventory/sync-ams-weights")
  32. @pytest.mark.asyncio
  33. @pytest.mark.integration
  34. async def test_the_sync_updates_a_locked_spool_and_releases_the_lock(
  35. async_client: AsyncClient, db_session, printer_factory
  36. ):
  37. spool_id = await _assigned_spool(db_session, printer_factory, locked=True)
  38. response = await _sync(async_client, _printer("IDLE"))
  39. assert response.status_code == 200
  40. assert response.json()["synced"] == 1
  41. spool = await db_session.get(Spool, spool_id)
  42. await db_session.refresh(spool)
  43. assert (spool.weight_used, spool.weight_locked) == (500, False)
  44. @pytest.mark.asyncio
  45. @pytest.mark.integration
  46. @pytest.mark.parametrize("gcode_state", ["PREPARE", "RUNNING", "PAUSE"])
  47. async def test_a_printing_printers_spool_is_left_alone(
  48. async_client: AsyncClient, db_session, printer_factory, gcode_state
  49. ):
  50. spool_id = await _assigned_spool(db_session, printer_factory, locked=False)
  51. response = await _sync(async_client, _printer(gcode_state))
  52. assert (response.json()["synced"], response.json()["printing"]) == (0, 1)
  53. spool = await db_session.get(Spool, spool_id)
  54. await db_session.refresh(spool)
  55. assert spool.weight_used == 100
  56. @pytest.mark.asyncio
  57. @pytest.mark.integration
  58. async def test_weighing_on_the_scale_locks_the_spool(async_client: AsyncClient, db_session):
  59. db_session.add(
  60. SpoolBuddyDevice(
  61. device_id="sb-1",
  62. hostname="spoolbuddy-1",
  63. ip_address="10.0.0.1",
  64. last_seen=datetime.now(timezone.utc),
  65. )
  66. )
  67. spool = Spool(material="PLA", label_weight=1000, core_weight=250, weight_used=100)
  68. db_session.add(spool)
  69. await db_session.commit()
  70. response = await async_client.post(
  71. "/api/v1/spoolbuddy/scale/update-spool-weight", json={"spool_id": spool.id, "weight_grams": 600}
  72. )
  73. assert response.status_code == 200
  74. await db_session.refresh(spool)
  75. assert (spool.weight_used, spool.weight_locked) == (650, True)
  76. @pytest.mark.asyncio
  77. @pytest.mark.integration
  78. async def test_a_print_whose_completion_is_still_being_charged_counts_as_printing(
  79. async_client: AsyncClient, db_session, printer_factory
  80. ):
  81. """The printer already reports FINISH; the usage session is still open
  82. until completion has charged the print."""
  83. from backend.app.services.usage_tracker import PrintSession, _active_sessions
  84. spool_id = await _assigned_spool(db_session, printer_factory, locked=False)
  85. printer_id = await db_session.scalar(select(SpoolAssignment.printer_id).where(SpoolAssignment.spool_id == spool_id))
  86. _active_sessions[printer_id] = PrintSession(
  87. printer_id=printer_id, print_name="x", started_at=datetime.now(timezone.utc)
  88. )
  89. try:
  90. response = await _sync(async_client, _printer("FINISH"))
  91. finally:
  92. _active_sessions.pop(printer_id, None)
  93. assert (response.json()["synced"], response.json()["printing"]) == (0, 1)
  94. spool = await db_session.get(Spool, spool_id)
  95. await db_session.refresh(spool)
  96. assert spool.weight_used == 100
  97. @pytest.mark.asyncio
  98. @pytest.mark.integration
  99. async def test_a_disconnected_printers_last_report_is_not_used(async_client: AsyncClient, db_session, printer_factory):
  100. spool_id = await _assigned_spool(db_session, printer_factory, locked=False)
  101. response = await _sync(async_client, _printer("unknown", connected=False))
  102. assert response.json()["synced"] == 0
  103. spool = await db_session.get(Spool, spool_id)
  104. await db_session.refresh(spool)
  105. assert spool.weight_used == 100