test_ams_remain_sync_while_printing.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """The AMS remain% weight sync stays off while the printer prints.
  2. Usage is deducted precisely at completion from the 3MF; raising weight_used
  3. from remain% during the print as well charges it twice (#880). The guard used
  4. to be only the usage tracker's in-memory session, which a restart with a
  5. stale persisted session never registers.
  6. """
  7. from unittest.mock import AsyncMock, MagicMock, patch
  8. import pytest
  9. from sqlalchemy import event
  10. from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
  11. from sqlalchemy.orm.attributes import set_committed_value
  12. from backend.app.models.spool import Spool
  13. from backend.app.models.spool_assignment import SpoolAssignment
  14. from backend.app.services.usage_tracker import _active_sessions
  15. def _loaded_before_the_hand_edit(spool, _context):
  16. set_committed_value(spool, "weight_locked", False)
  17. set_committed_value(spool, "weight_used", 100)
  18. @pytest.fixture(autouse=True)
  19. def _no_sessions():
  20. _active_sessions.clear()
  21. yield
  22. _active_sessions.clear()
  23. async def _weight_after_ams_push(
  24. test_engine,
  25. db_session,
  26. printer_factory,
  27. state: str,
  28. sent: list[str] | None = None,
  29. *,
  30. weight_used: float = 100,
  31. weight_locked: bool = False,
  32. loaded_as_unlocked: bool = False,
  33. ) -> float:
  34. """``loaded_as_unlocked``: the sync loads the spool unlocked at 100 g, as
  35. it was before a hand edit that committed right after that load."""
  36. from backend.app.main import on_ams_change
  37. printer = await printer_factory(name="X1C")
  38. spool = Spool(material="PLA", label_weight=1000, weight_used=weight_used, weight_locked=weight_locked)
  39. db_session.add(spool)
  40. await db_session.flush()
  41. db_session.add(
  42. SpoolAssignment(
  43. spool_id=spool.id,
  44. printer_id=printer.id,
  45. ams_id=0,
  46. tray_id=0,
  47. fingerprint_color="FF0000FF",
  48. fingerprint_type="PLA",
  49. )
  50. )
  51. await db_session.commit()
  52. spool_id = spool.id
  53. ams_data = [{"id": 0, "tray": [{"id": 0, "tray_type": "PLA", "tray_color": "FF0000FF", "remain": 50}]}]
  54. status = MagicMock()
  55. status.raw_data = {"ams": {"ams": ams_data}}
  56. status.state = state
  57. status.connected = True
  58. real_db = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
  59. with (
  60. patch("backend.app.main.async_session", real_db),
  61. patch("backend.app.core.database.async_session", real_db),
  62. patch("backend.app.main.printer_manager") as mock_pm,
  63. patch("backend.app.main.mqtt_relay") as mock_relay,
  64. patch("backend.app.main.ws_manager") as mock_ws,
  65. ):
  66. mock_pm.get_printer.return_value = MagicMock(name="X1C", serial_number="REMAINTEST")
  67. mock_pm.get_status.return_value = status
  68. mock_pm.get_client.return_value = MagicMock()
  69. mock_pm.get_model.return_value = "X1C"
  70. mock_relay.on_ams_change = AsyncMock()
  71. mock_ws.send_printer_status = AsyncMock()
  72. mock_ws.broadcast = AsyncMock()
  73. if loaded_as_unlocked:
  74. event.listen(Spool, "load", _loaded_before_the_hand_edit)
  75. try:
  76. await on_ams_change(printer.id, ams_data)
  77. finally:
  78. if loaded_as_unlocked:
  79. event.remove(Spool, "load", _loaded_before_the_hand_edit)
  80. if sent is not None:
  81. sent.extend(c.args[0]["type"] for c in mock_ws.broadcast.call_args_list)
  82. db_session.expire_all()
  83. return (await db_session.get(Spool, spool_id)).weight_used
  84. @pytest.mark.asyncio
  85. @pytest.mark.integration
  86. @pytest.mark.parametrize("state", ["RUNNING", "PAUSE"])
  87. async def test_no_sync_while_printing_without_a_tracker_session(test_engine, db_session, printer_factory, state):
  88. assert await _weight_after_ams_push(test_engine, db_session, printer_factory, state) == 100
  89. @pytest.mark.asyncio
  90. @pytest.mark.integration
  91. async def test_syncs_when_idle(test_engine, db_session, printer_factory):
  92. assert await _weight_after_ams_push(test_engine, db_session, printer_factory, "IDLE") == 500
  93. @pytest.mark.asyncio
  94. @pytest.mark.integration
  95. async def test_a_synced_weight_is_announced(test_engine, db_session, printer_factory):
  96. """WU-9: other open tabs refresh weights only on inventory_changed."""
  97. sent: list[str] = []
  98. assert await _weight_after_ams_push(test_engine, db_session, printer_factory, "IDLE", sent) == 500
  99. assert "inventory_changed" in sent
  100. @pytest.mark.asyncio
  101. @pytest.mark.integration
  102. async def test_a_weight_set_by_hand_meanwhile_is_kept(test_engine, db_session, printer_factory):
  103. """WL-6: the sync decided on the spool as it had loaded it. A weight set by
  104. hand (and so locked) after that load was overwritten, with the lock left set."""
  105. sent: list[str] = []
  106. weight = await _weight_after_ams_push(
  107. test_engine,
  108. db_session,
  109. printer_factory,
  110. "IDLE",
  111. sent,
  112. weight_used=300,
  113. weight_locked=True,
  114. loaded_as_unlocked=True,
  115. )
  116. assert weight == 300
  117. assert "inventory_changed" not in sent, "nothing changed, so nothing to announce"
  118. @pytest.mark.asyncio
  119. @pytest.mark.integration
  120. async def test_no_sync_while_a_finished_print_is_being_charged(test_engine, db_session, printer_factory):
  121. """WU-3: the printer reports FINISH and the tracker has taken the session,
  122. but the usage is not booked yet."""
  123. with patch("backend.app.services.usage_tracker.printer_is_charging", lambda printer_id: True):
  124. weight = await _weight_after_ams_push(test_engine, db_session, printer_factory, "FINISH")
  125. assert weight == 100