test_pause_load_paths_2984.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. """Every print-start path that binds the print's archive also reads its slicer
  2. pauses (#2984), like the skip-objects list beside it."""
  3. import io
  4. import zipfile
  5. from contextlib import ExitStack
  6. from unittest.mock import AsyncMock, MagicMock, patch
  7. import pytest
  8. from sqlalchemy import select
  9. from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
  10. from backend.app.core.config import settings as app_settings
  11. from backend.app.models.archive import PrintArchive
  12. from backend.tests.integration.test_print_start_name_match_ap2 import ( # noqa: F401
  13. DISPATCH,
  14. SUBTASK,
  15. _clear_print_state,
  16. _start,
  17. _stuck_archive,
  18. )
  19. async def _start_from_cache(test_engine, printer_id: int, cached) -> None:
  20. """``_start`` with the 3MF already pulled by the cover endpoint (#972)."""
  21. session_maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
  22. state = MagicMock(sdcard=True, sdcard_reported=True, subtask_id="222", progress=0.0)
  23. with ExitStack() as stack:
  24. for target, value in {
  25. "backend.app.main.async_session": session_maker,
  26. "backend.app.core.database.async_session": session_maker,
  27. "backend.app.main.get_cached_3mf": MagicMock(return_value=cached),
  28. "backend.app.main.cache_3mf_download": MagicMock(),
  29. "backend.app.main._record_energy_start": AsyncMock(),
  30. "backend.app.main._send_print_start_notification": AsyncMock(),
  31. "backend.app.main._maybe_start_layer_timelapse": MagicMock(),
  32. "backend.app.main._capture_timelapse_baseline_at_start": AsyncMock(),
  33. "backend.app.main._store_spoolman_print_data": AsyncMock(),
  34. "backend.app.main._schedule_archive_cost_estimate": MagicMock(),
  35. "backend.app.services.usage_tracker.on_print_start": AsyncMock(),
  36. }.items():
  37. stack.enter_context(patch(target, value))
  38. for name in ("notification_service", "smart_plug_manager", "ws_manager", "mqtt_relay"):
  39. mock = stack.enter_context(patch(f"backend.app.main.{name}"))
  40. for method in (
  41. "on_print_start",
  42. "send_print_start",
  43. "send_archive_created",
  44. "send_archive_updated",
  45. "on_archive_created",
  46. ):
  47. setattr(mock, method, AsyncMock())
  48. pm = stack.enter_context(patch("backend.app.main.printer_manager"))
  49. pm.get_status = MagicMock(return_value=state)
  50. pm.get_printer = MagicMock(return_value=MagicMock(serial_number="TESTAP2"))
  51. pm.get_client = MagicMock(return_value=None)
  52. from backend.app.main import on_print_start
  53. await on_print_start(
  54. printer_id, {"filename": DISPATCH, "subtask_name": SUBTASK, "raw_data": {"subtask_id": "222"}}
  55. )
  56. @pytest.mark.asyncio
  57. @pytest.mark.integration
  58. async def test_resumed_on_its_subtask_id(test_engine, db_session, printer_factory):
  59. printer = await printer_factory()
  60. archive_id = await _stuck_archive(db_session, printer.id, "222")
  61. with patch("backend.app.main._load_pauses_from_3mf") as load:
  62. await _start(test_engine, printer.id, "222")
  63. archive = await db_session.get(PrintArchive, archive_id)
  64. load.assert_called_once()
  65. assert load.call_args.args == (app_settings.base_dir / archive.file_path, printer.id)
  66. # As a resumption: what was warned about and passed is kept.
  67. assert set(load.call_args.kwargs) == {"warned_layers", "passed_through_layer"}
  68. @pytest.mark.asyncio
  69. @pytest.mark.integration
  70. async def test_resumed_by_name(test_engine, db_session, printer_factory):
  71. printer = await printer_factory()
  72. archive_id = await _stuck_archive(db_session, printer.id, None)
  73. with patch("backend.app.main._load_pauses_from_3mf") as load:
  74. await _start(test_engine, printer.id, "222")
  75. archive = await db_session.get(PrintArchive, archive_id)
  76. load.assert_called_once()
  77. assert load.call_args.args == (app_settings.base_dir / archive.file_path, printer.id)
  78. # As a resumption: what was warned about and passed is kept.
  79. assert set(load.call_args.kwargs) == {"warned_layers", "passed_through_layer"}
  80. @pytest.mark.asyncio
  81. @pytest.mark.integration
  82. async def test_a_new_archive_from_the_downloaded_3mf(test_engine, db_session, printer_factory, tmp_path):
  83. printer = await printer_factory()
  84. downloaded = tmp_path / f"{SUBTASK}.gcode.3mf"
  85. buf = io.BytesIO()
  86. with zipfile.ZipFile(buf, "w") as zf:
  87. zf.writestr("Metadata/plate_1.gcode", "M73 P0 R10\nM73 L2\nM400 U1\n")
  88. downloaded.write_bytes(buf.getvalue())
  89. printer_id = printer.id
  90. with (
  91. patch("backend.app.main._load_pauses_from_3mf") as load,
  92. # The archive is copied under archive_dir; keep it out of the real one.
  93. patch.object(app_settings, "base_dir", tmp_path),
  94. patch.object(app_settings, "archive_dir", tmp_path / "archive"),
  95. ):
  96. await _start_from_cache(test_engine, printer_id, downloaded)
  97. expected_base = app_settings.base_dir
  98. db_session.expire_all()
  99. archive = (await db_session.execute(select(PrintArchive).where(PrintArchive.printer_id == printer_id))).scalar_one()
  100. # The archived copy, not the download: that one may be removed meanwhile.
  101. assert archive.file_path and expected_base / archive.file_path != downloaded
  102. assert (expected_base / archive.file_path).is_file()
  103. load.assert_called_once_with(expected_base / archive.file_path, printer_id)