| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- """Drying applies to "Any <model>" jobs too (SP-C1).
- The drying hold ("drying blocks the queue"), the stop before a print starts and
- queue-only drying all lived in the branch for jobs pinned to one printer. A job
- for "Any X1C" went straight to the matcher: it could start on an X1C whose AMS
- was mid-dry with the hold switched on, the dry cycle was never stopped for it,
- and queue-only drying never pre-dried anything for it.
- Now a drying printer is passed over for a model job while the hold is on (the
- job takes another printer of the model, or waits), the printer a model job
- lands on has its drying stopped like a pinned job's, and a model job counts for
- every printer of its model its creator may use when drying decides what to dry.
- """
- from types import SimpleNamespace
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from backend.app.core.printer_scope import PrinterScope
- from backend.app.models.print_queue import PrintQueueItem, PrintQueueVariant
- from backend.app.models.printer import Printer
- from backend.app.services.print_scheduler import PrintScheduler
- from backend.tests.unit.test_scheduler_model_filament_short_3137 import queue_db # noqa: F401 - fixture
- from backend.tests.unit.test_scheduler_pinned_waiting_reason_3074 import (
- _add_item,
- _item,
- _run,
- _set,
- ctx, # noqa: F401 - fixture
- )
- async def _add_printer(ctx, printer_id: int, model: str = "X1C") -> None:
- async with ctx.session_maker() as db:
- db.add(
- Printer(
- id=printer_id,
- name=f"{model}-0{printer_id}",
- serial_number=f"{model}000{printer_id}",
- ip_address=f"10.0.0.{printer_id}",
- access_code="x",
- model=model,
- is_active=True,
- )
- )
- await db.commit()
- def _dispatched_to(launched) -> dict[int, int]:
- return launched.call_args[0][1] if launched.called else {}
- class TestTheHoldCoversModelJobs:
- @pytest.mark.asyncio
- async def test_a_drying_printer_is_passed_over_for_another_of_the_model(self, ctx):
- await _set(ctx, "queue_drying_block", "true")
- await _add_printer(ctx, 2)
- item_id = await _add_item(ctx, printer_id=None, target_model="X1C")
- scheduler = PrintScheduler()
- scheduler._drying_in_progress[1] = 1.0
- launched = await _run(ctx, scheduler, idle=True)
- assert _dispatched_to(launched).get(item_id) == 2
- @pytest.mark.asyncio
- async def test_it_waits_while_every_printer_of_the_model_dries(self, ctx):
- await _set(ctx, "queue_drying_block", "true")
- item_id = await _add_item(ctx, printer_id=None, target_model="X1C")
- scheduler = PrintScheduler()
- scheduler._drying_in_progress[1] = 1.0
- launched = await _run(ctx, scheduler, idle=True)
- launched.assert_not_called()
- item = await _item(ctx, item_id)
- assert item.printer_id is None
- # Busy-shaped: the cycle ends on its own, nothing to alert about. Named
- # like a pinned job's, so the wait reads as drying, not as printing.
- assert item.waiting_reason == "Busy: X1C-01 (drying)"
- @pytest.mark.asyncio
- async def test_a_parked_timer_does_not_hold_it(self, ctx):
- await _set(ctx, "queue_drying_block", "true")
- item_id = await _add_item(ctx, printer_id=None, target_model="X1C")
- scheduler = PrintScheduler()
- scheduler._drying_in_progress[1] = 1.0
- with (
- patch.object(PrintScheduler, "_drying_is_only_parked", staticmethod(lambda printer_id: True)),
- patch.object(scheduler, "_stop_drying", AsyncMock()),
- patch.object(scheduler, "_drying_may_continue_through_print", AsyncMock(return_value=True)),
- ):
- launched = await _run(ctx, scheduler, idle=True)
- assert _dispatched_to(launched).get(item_id) == 1
- @pytest.mark.asyncio
- async def test_without_the_hold_it_may_take_a_drying_printer(self, ctx):
- item_id = await _add_item(ctx, printer_id=None, target_model="X1C")
- scheduler = PrintScheduler()
- scheduler._drying_in_progress[1] = 1.0
- with (
- patch.object(scheduler, "_stop_drying", AsyncMock()),
- patch.object(scheduler, "_drying_may_continue_through_print", AsyncMock(return_value=True)),
- ):
- launched = await _run(ctx, scheduler, idle=True)
- assert _dispatched_to(launched).get(item_id) == 1
- class TestDryingStopsForTheModelJobsPrint:
- @pytest.mark.asyncio
- async def test_the_printer_it_lands_on_stops_drying(self, ctx):
- await _add_item(ctx, printer_id=None, target_model="X1C")
- scheduler = PrintScheduler()
- scheduler._drying_in_progress[1] = 1.0
- stop = AsyncMock()
- with (
- patch.object(scheduler, "_stop_drying", stop),
- patch.object(scheduler, "_drying_may_continue_through_print", AsyncMock(return_value=False)),
- ):
- launched = await _run(ctx, scheduler, idle=True)
- launched.assert_called_once()
- stop.assert_awaited_once_with(1)
- @pytest.mark.asyncio
- async def test_drying_that_may_continue_through_the_print_is_left_alone(self, ctx):
- await _add_item(ctx, printer_id=None, target_model="X1C")
- scheduler = PrintScheduler()
- scheduler._drying_in_progress[1] = 1.0
- stop = AsyncMock()
- with (
- patch.object(scheduler, "_stop_drying", stop),
- patch.object(scheduler, "_drying_may_continue_through_print", AsyncMock(return_value=True)),
- ):
- await _run(ctx, scheduler, idle=True)
- stop.assert_not_awaited()
- async def _items(ctx) -> list[PrintQueueItem]:
- from sqlalchemy import select
- from sqlalchemy.orm import selectinload
- async with ctx.session_maker() as db:
- query = select(PrintQueueItem).options(
- selectinload(PrintQueueItem.variants).selectinload(PrintQueueVariant.library_file),
- selectinload(PrintQueueItem.archive),
- selectinload(PrintQueueItem.library_file),
- )
- return list((await db.execute(query)).scalars().all())
- async def _considered_for_drying(ctx, scheduler, *, scope=None) -> set[int]:
- """The printers auto-drying looked at past the queue gates: each one it
- reaches is asked whether it is idle (answered no, so nothing starts)."""
- reached: set[int] = set()
- def idle(pid, require_plate_clear=False):
- reached.add(pid)
- return False
- state = SimpleNamespace(state="IDLE", firmware_version="01.09.00.00", raw_data={"ams": []})
- pm = MagicMock()
- pm.get_status.return_value = state
- pm.get_model.return_value = "X1C"
- pm.is_connected.return_value = True
- items = await _items(ctx)
- async with ctx.session_maker() as db:
- with (
- patch("backend.app.services.print_scheduler.printer_manager", pm),
- patch.object(scheduler, "_is_printer_idle", idle),
- patch("backend.app.core.auth.is_auth_enabled", AsyncMock(return_value=scope is not None)),
- patch(
- "backend.app.services.print_scheduler.resolve_user_id_printer_scope",
- AsyncMock(return_value=scope),
- ),
- ):
- await scheduler._check_auto_drying(db, items, set())
- return reached
- class TestQueueOnlyDryingCountsModelJobs:
- @pytest.mark.asyncio
- async def test_a_scheduled_model_job_pre_dries_every_printer_of_its_model(self, ctx):
- from datetime import datetime, timedelta
- await _set(ctx, "queue_drying_enabled", "true")
- await _add_printer(ctx, 2)
- await _add_printer(ctx, 3, model="P1S")
- item_id = await _add_item(ctx, printer_id=None, target_model="X1C")
- async with ctx.session_maker() as db:
- item = await db.get(PrintQueueItem, item_id)
- item.scheduled_time = datetime.utcnow() + timedelta(hours=2)
- await db.commit()
- assert await _considered_for_drying(ctx, PrintScheduler()) == {1, 2}
- @pytest.mark.asyncio
- async def test_only_the_printers_its_creator_may_use(self, ctx):
- from datetime import datetime, timedelta
- await _set(ctx, "queue_drying_enabled", "true")
- await _add_printer(ctx, 2)
- item_id = await _add_item(ctx, printer_id=None, target_model="X1C")
- async with ctx.session_maker() as db:
- item = await db.get(PrintQueueItem, item_id)
- item.scheduled_time = datetime.utcnow() + timedelta(hours=2)
- item.created_by_id = 42
- await db.commit()
- reached = await _considered_for_drying(ctx, PrintScheduler(), scope=PrinterScope(frozenset({2})))
- assert reached == {2}
- @pytest.mark.asyncio
- async def test_a_cross_model_job_counts_every_model_it_was_sliced_for(self, ctx):
- from datetime import datetime, timedelta
- from backend.app.models.library import LibraryFile
- await _set(ctx, "queue_drying_enabled", "true")
- await _add_printer(ctx, 3, model="P1S")
- await _add_printer(ctx, 4, model="A1")
- item_id = await _add_item(ctx, printer_id=None, target_model=None)
- async with ctx.session_maker() as db:
- item = await db.get(PrintQueueItem, item_id)
- item.scheduled_time = datetime.utcnow() + timedelta(hours=2)
- for position, model in enumerate(("X1C", "P1S")):
- lib = LibraryFile(
- filename=f"{model}.gcode.3mf", file_path=f"/library/{model}.gcode.3mf", file_size=1, file_type="3mf"
- )
- db.add(lib)
- await db.flush()
- db.add(
- PrintQueueVariant(
- queue_item_id=item_id, position=position, library_file_id=lib.id, target_model=model
- )
- )
- await db.commit()
- assert await _considered_for_drying(ctx, PrintScheduler()) == {1, 3}
- @pytest.mark.asyncio
- async def test_an_unscheduled_model_job_pre_dries_nothing(self, ctx):
- await _set(ctx, "queue_drying_enabled", "true")
- await _add_item(ctx, printer_id=None, target_model="X1C")
- assert await _considered_for_drying(ctx, PrintScheduler()) == set()
- @pytest.mark.asyncio
- async def test_the_hold_keeps_drying_from_starting_where_a_model_job_waits(self, ctx):
- """As for a pinned job: with drying set to block the queue, a printer a
- pending job could take does not start a new cycle."""
- await _set(ctx, "ambient_drying_enabled", "true")
- await _set(ctx, "queue_drying_block", "true")
- await _add_printer(ctx, 3, model="P1S")
- await _add_item(ctx, printer_id=None, target_model="X1C")
- assert await _considered_for_drying(ctx, PrintScheduler()) == {3}
- class TestALowFilamentHoldIsNotMovedOntoADryingPrinter:
- """The #3137 release moves a held model job to another printer of the
- model: not to one that drying holds, where it would wait the whole cycle."""
- @pytest.mark.asyncio
- async def test_it_goes_to_the_printer_that_is_not_drying(self, queue_db):
- from backend.app.models.settings import Settings
- from backend.tests.unit.test_scheduler_model_filament_short_3137 import (
- _add_item as add_held,
- _get_item,
- _run as run_held,
- )
- async with queue_db.session_maker() as db:
- db.add(Settings(key="queue_drying_block", value="true"))
- await db.commit()
- item_id = await add_held(queue_db, printer_id=1, manual_start=True, filament_short=True)
- scheduler = PrintScheduler()
- scheduler._drying_in_progress[2] = 1.0
- launched = MagicMock()
- with (
- patch.object(scheduler, "_stop_drying", AsyncMock()),
- patch.object(scheduler, "_drying_may_continue_through_print", AsyncMock(return_value=True)),
- ):
- await run_held(queue_db, scheduler, short={1}, launched=launched)
- assert (await _get_item(queue_db, item_id)).printer_id == 3
|