test_run_estimate_plate.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. """The print-log estimate covers the plate that printed, and the run keeps it.
  2. The archive holds one plate. A later run of another plate of the same
  3. multi-plate file, dispatched directly or from the queue, has to be estimated
  4. for that plate, not for the archive's, and its print log entry names it.
  5. """
  6. import asyncio
  7. from unittest.mock import AsyncMock, MagicMock, patch
  8. import pytest
  9. from sqlalchemy import select
  10. from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
  11. from backend.app import main as main_module
  12. from backend.app.models.print_log import PrintLogEntry
  13. from backend.app.models.print_queue import PrintQueueItem
  14. from backend.app.services.bambu_ftp import DeleteResult
  15. @pytest.fixture(autouse=True)
  16. def _clean_state():
  17. main_module._active_prints.clear()
  18. main_module._print_plate_ids.clear()
  19. yield
  20. main_module._active_prints.clear()
  21. main_module._print_plate_ids.clear()
  22. async def _complete(test_engine, printer_id: int, logged: list | None = None) -> list:
  23. """Run on_print_complete against the test database; return the plates the
  24. estimates were asked for (billing first, then the print log). ``logged``
  25. gets the plates of the print log entries written, read before the
  26. background work is torn down."""
  27. real_db = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
  28. asked: list = []
  29. def _estimate(archive, full_path, plate_id=None):
  30. asked.append(plate_id)
  31. return None, None
  32. tasks_before = set(asyncio.all_tasks())
  33. with (
  34. patch("backend.app.main.async_session", real_db),
  35. patch("backend.app.core.database.async_session", real_db),
  36. patch("backend.app.main._plate_scoped_run_estimate", side_effect=_estimate),
  37. patch("backend.app.main.ws_manager") as mock_ws,
  38. patch("backend.app.main.printer_manager") as mock_pm,
  39. patch("backend.app.main.mqtt_relay") as mock_relay,
  40. patch("backend.app.main.notification_service") as mock_notif,
  41. patch("backend.app.main.smart_plug_manager") as mock_plug,
  42. patch("backend.app.main.clear_3mf_cache"),
  43. patch("backend.app.services.bambu_ftp.delete_file_async", new=AsyncMock(return_value=DeleteResult.DELETED)),
  44. ):
  45. mock_ws.send_print_complete = AsyncMock()
  46. mock_ws.broadcast = AsyncMock()
  47. mock_relay.on_print_complete = AsyncMock()
  48. mock_relay.on_queue_job_completed = AsyncMock()
  49. mock_notif.on_print_complete = AsyncMock()
  50. mock_plug.on_print_complete = AsyncMock()
  51. printer_info = MagicMock(serial_number="TEST123")
  52. printer_info.name = "Test" # name= would only name the mock
  53. mock_pm.get_printer = MagicMock(return_value=printer_info)
  54. mock_pm.get_current_print_user = MagicMock(return_value=None)
  55. mock_pm.clear_current_print_user = MagicMock()
  56. mock_pm.set_awaiting_plate_clear = MagicMock()
  57. await main_module.on_print_complete(
  58. printer_id,
  59. {"status": "completed", "filename": "multi.gcode.3mf", "subtask_name": "multi", "progress": 100},
  60. )
  61. if logged is not None:
  62. async with real_db() as db:
  63. logged.extend(await db.scalars(select(PrintLogEntry.plate_id)))
  64. for task in asyncio.all_tasks() - tasks_before:
  65. task.cancel()
  66. try:
  67. await task
  68. except (asyncio.CancelledError, Exception):
  69. pass
  70. return asked
  71. @pytest.mark.asyncio
  72. async def test_a_direct_print_is_estimated_for_its_plate(test_engine, db_session, printer_factory, archive_factory):
  73. printer = await printer_factory()
  74. archive = await archive_factory(
  75. printer.id, filename="multi.gcode.3mf", plate_id=1, status="printing", with_run=False
  76. )
  77. main_module._active_prints[(printer.id, "multi.gcode.3mf")] = archive.id
  78. main_module._print_plate_ids[(printer.id, archive.id)] = 3
  79. assert await _complete(test_engine, printer.id) == [3, 3]
  80. @pytest.mark.asyncio
  81. async def test_a_queue_run_is_estimated_for_the_queue_items_plate(
  82. test_engine, db_session, printer_factory, archive_factory
  83. ):
  84. """After a restart the in-memory plate is gone; the queue item still has it."""
  85. printer = await printer_factory()
  86. archive = await archive_factory(
  87. printer.id, filename="multi.gcode.3mf", plate_id=1, status="printing", with_run=False
  88. )
  89. db_session.add(PrintQueueItem(printer_id=printer.id, archive_id=archive.id, plate_id=2, status="printing"))
  90. await db_session.commit()
  91. main_module._active_prints[(printer.id, "multi.gcode.3mf")] = archive.id
  92. assert await _complete(test_engine, printer.id) == [2, 2]
  93. @pytest.mark.asyncio
  94. async def test_a_direct_print_logs_its_plate(test_engine, db_session, printer_factory, archive_factory):
  95. printer = await printer_factory()
  96. archive = await archive_factory(
  97. printer.id, filename="multi.gcode.3mf", plate_id=1, status="printing", with_run=False
  98. )
  99. main_module._active_prints[(printer.id, "multi.gcode.3mf")] = archive.id
  100. main_module._print_plate_ids[(printer.id, archive.id)] = 3
  101. logged: list = []
  102. await _complete(test_engine, printer.id, logged)
  103. assert logged == [3]
  104. @pytest.mark.asyncio
  105. async def test_a_queue_run_logs_the_queue_items_plate(test_engine, db_session, printer_factory, archive_factory):
  106. printer = await printer_factory()
  107. archive = await archive_factory(
  108. printer.id, filename="multi.gcode.3mf", plate_id=1, status="printing", with_run=False
  109. )
  110. db_session.add(PrintQueueItem(printer_id=printer.id, archive_id=archive.id, plate_id=2, status="printing"))
  111. await db_session.commit()
  112. main_module._active_prints[(printer.id, "multi.gcode.3mf")] = archive.id
  113. logged: list = []
  114. await _complete(test_engine, printer.id, logged)
  115. assert logged == [2]
  116. @pytest.mark.asyncio
  117. async def test_a_print_with_no_known_plate_logs_the_archives(test_engine, db_session, printer_factory, archive_factory):
  118. """Started on the printer: the archive was made for this print, its plate
  119. read off the printed file's name."""
  120. printer = await printer_factory()
  121. archive = await archive_factory(
  122. printer.id, filename="multi.gcode.3mf", plate_id=4, status="printing", with_run=False
  123. )
  124. main_module._active_prints[(printer.id, "multi.gcode.3mf")] = archive.id
  125. logged: list = []
  126. await _complete(test_engine, printer.id, logged)
  127. assert logged == [4]