test_spoolman_failed_run_cost.py 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. """A failed or stopped print in Spoolman mode is logged with its own cost.
  2. The run's cost came only from the built-in tracker's results, or from the
  3. estimate for a completed print, so a failed Spoolman print had none in Print
  4. History. Its Spoolman charges are priced as they are made, and that is the
  5. run's cost, as with the built-in inventory.
  6. """
  7. import asyncio
  8. from types import SimpleNamespace
  9. from unittest.mock import AsyncMock, MagicMock, patch
  10. import pytest
  11. from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
  12. from backend.app import main as main_module
  13. from backend.app.services.bambu_ftp import DeleteResult
  14. @pytest.fixture(autouse=True)
  15. def _clean_state():
  16. main_module._active_prints.clear()
  17. main_module._print_plate_ids.clear()
  18. yield
  19. main_module._active_prints.clear()
  20. main_module._print_plate_ids.clear()
  21. async def _complete(test_engine, printer_id: int) -> list:
  22. """Run on_print_complete against the test database; return the plates the
  23. estimates were asked for (billing first, then the print log)."""
  24. real_db = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
  25. asked: list = []
  26. def _estimate(archive, full_path, plate_id=None):
  27. asked.append(plate_id)
  28. return None, None
  29. tasks_before = set(asyncio.all_tasks())
  30. with (
  31. patch("backend.app.main.async_session", real_db),
  32. patch("backend.app.core.database.async_session", real_db),
  33. patch("backend.app.main._plate_scoped_run_estimate", side_effect=_estimate),
  34. patch("backend.app.main.ws_manager") as mock_ws,
  35. patch("backend.app.main.printer_manager") as mock_pm,
  36. patch("backend.app.main.mqtt_relay") as mock_relay,
  37. patch("backend.app.main.notification_service") as mock_notif,
  38. patch("backend.app.main.smart_plug_manager") as mock_plug,
  39. patch("backend.app.main.clear_3mf_cache"),
  40. patch("backend.app.services.bambu_ftp.delete_file_async", new=AsyncMock(return_value=DeleteResult.DELETED)),
  41. ):
  42. mock_ws.send_print_complete = AsyncMock()
  43. mock_ws.broadcast = AsyncMock()
  44. mock_relay.on_print_complete = AsyncMock()
  45. mock_relay.on_queue_job_completed = AsyncMock()
  46. mock_notif.on_print_complete = AsyncMock()
  47. mock_plug.on_print_complete = AsyncMock()
  48. mock_pm.get_printer = MagicMock(return_value=SimpleNamespace(name="Test", serial_number="TEST123"))
  49. mock_pm.get_current_print_user = MagicMock(return_value=None)
  50. mock_pm.clear_current_print_user = MagicMock()
  51. mock_pm.set_awaiting_plate_clear = MagicMock()
  52. await main_module.on_print_complete(
  53. printer_id,
  54. {"status": "failed", "filename": "multi.gcode.3mf", "subtask_name": "multi", "progress": 40},
  55. )
  56. for task in asyncio.all_tasks() - tasks_before:
  57. task.cancel()
  58. try:
  59. await task
  60. except (asyncio.CancelledError, Exception):
  61. pass
  62. return asked
  63. @pytest.mark.asyncio
  64. async def test_a_failed_spoolman_print_is_logged_with_what_its_spools_cost(
  65. test_engine, db_session, printer_factory, archive_factory
  66. ):
  67. printer = await printer_factory()
  68. archive = await archive_factory(printer.id, filename="multi.gcode.3mf", status="printing", with_run=False)
  69. archive_id = archive.id
  70. main_module._active_prints[(printer.id, "multi.gcode.3mf")] = archive_id
  71. with (
  72. patch("backend.app.main._cleanup_spoolman_tracking", AsyncMock(return_value=3.5)),
  73. patch("backend.app.services.print_log.write_log_entry", new_callable=AsyncMock) as logged,
  74. patch("backend.app.services.print_log.record_archive_wear", new_callable=AsyncMock),
  75. ):
  76. await _complete(test_engine, printer.id)
  77. logged.assert_awaited_once()
  78. assert (logged.await_args.kwargs["archive_id"], logged.await_args.kwargs["cost"]) == (archive_id, 3.5)