test_completion_keeps_printer_charging.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. """A printer counts as printing until its finished print's usage is booked (WU-3, AS-5).
  2. The AMS remain% weight sync stays off while a print runs, because completion
  3. charges the print exactly from its 3MF (#880). The tracker takes the print's
  4. session first and only then works out and charges the usage, several awaits
  5. later. The printer already reports FINISH, so an AMS update in that window
  6. raised the spool's weight from remain% and the tracker then added the print
  7. on top: charged twice.
  8. A completion with neither file name nor subtask name returned before any of
  9. this, leaving the session open, which kept the weight sync off until the next
  10. print. It now closes the session; it charges nothing, having no print to
  11. charge (AS-5).
  12. """
  13. from datetime import datetime, timezone
  14. from unittest.mock import AsyncMock, patch
  15. import pytest
  16. from backend.app.services import usage_tracker
  17. from backend.app.services.usage_tracker import PrintSession, _active_sessions, printer_is_charging
  18. from backend.tests.integration.test_print_complete_end_to_end import ( # noqa: F401
  19. _clean,
  20. _complete,
  21. _no_finish_photo,
  22. queued_print,
  23. )
  24. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  25. @pytest.fixture(autouse=True)
  26. def _no_sessions_or_marks():
  27. _active_sessions.clear()
  28. usage_tracker._completing.clear()
  29. yield
  30. _active_sessions.clear()
  31. usage_tracker._completing.clear()
  32. def _open_session(printer_id: int) -> None:
  33. _active_sessions[printer_id] = PrintSession(
  34. printer_id=printer_id, print_name="part", started_at=datetime.now(timezone.utc)
  35. )
  36. async def _complete_watching(test_engine, printer_id: int, **extra) -> list:
  37. """Complete the print; the tracker, as the real one, takes the session first.
  38. Returns the archive each charge was made for; asserts the printer still
  39. counted as printing when it was made."""
  40. seen: list = []
  41. async def tracker(pid, data, printer_manager, db, **kwargs):
  42. _active_sessions.pop(pid, None)
  43. assert printer_is_charging(pid), "the AMS sync could charge the print as well"
  44. seen.append(kwargs.get("archive_id"))
  45. return []
  46. with patch("backend.app.services.usage_tracker.on_print_complete", tracker):
  47. await _complete(test_engine, printer_id, "completed", extra=extra or None)
  48. return seen
  49. async def test_the_printer_counts_as_printing_until_the_usage_is_booked(test_engine, queued_print):
  50. _open_session(queued_print["printer"])
  51. assert await _complete_watching(test_engine, queued_print["printer"]) == [queued_print["archive"]]
  52. assert not printer_is_charging(queued_print["printer"])
  53. async def test_a_completion_without_a_file_name_closes_the_session_and_charges_nothing(
  54. test_engine, db_session, queued_print
  55. ):
  56. """Which print ended can't be told: a printing archive is almost always an
  57. earlier print's, which the reconnect reconcile closes and charges. Closing
  58. the session is what ends the stuck weight sync."""
  59. from backend.app.models.archive import PrintArchive
  60. _open_session(queued_print["printer"])
  61. charges = await _complete_watching(test_engine, queued_print["printer"], filename="", subtask_name="")
  62. assert charges == []
  63. assert not printer_is_charging(queued_print["printer"]), "the weight sync stayed off"
  64. db_session.expire_all()
  65. assert (await db_session.get(PrintArchive, queued_print["archive"])).status == "printing"
  66. async def test_a_completion_that_fails_does_not_leave_the_printer_printing(test_engine, queued_print):
  67. with (
  68. patch("backend.app.main._recover_fallback_from_cache_before_eviction", AsyncMock(side_effect=RuntimeError)),
  69. pytest.raises(RuntimeError),
  70. ):
  71. await _complete(test_engine, queued_print["printer"], "completed")
  72. assert not printer_is_charging(queued_print["printer"])
  73. async def test_a_new_print_clears_a_stale_mark():
  74. from types import SimpleNamespace
  75. from unittest.mock import MagicMock
  76. usage_tracker._completing.add(5)
  77. printer_manager = MagicMock()
  78. printer_manager.get_status.return_value = SimpleNamespace(raw_data=None)
  79. await usage_tracker.on_print_start(5, {}, printer_manager)
  80. assert not printer_is_charging(5)
  81. async def test_booking_the_usage_ends_the_mark(db_session):
  82. """The rest of the completion (SD cleanup, notifications) can take a while;
  83. the sync need not wait for it once the print is charged."""
  84. usage_tracker.mark_completing(5)
  85. await usage_tracker.discard_session(db_session, 5)
  86. assert not printer_is_charging(5)
  87. async def test_a_deleted_printer_is_forgotten():
  88. usage_tracker.mark_completing(5)
  89. usage_tracker.forget_printer(5)
  90. assert not printer_is_charging(5)