| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- """A printer's time offline is not counted as runtime (DB-C5).
- The runtime counter adds the time since its last tick while a printer is
- RUNNING. When a printer dropped off mid-print and came back hours later with a
- print running, that first tick added the whole gap: hours-based maintenance
- came due early. The timestamp is now cleared when a printer this process saw
- connected goes offline. A Bambuddy restart is still counted from the stored
- timestamp, as before -- those printers have not been seen connected yet.
- """
- import asyncio
- from datetime import datetime, timedelta
- from types import SimpleNamespace
- from unittest.mock import patch
- import pytest
- from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
- from backend.app import main as main_module
- from backend.app.models.printer import Printer
- pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
- @pytest.fixture(autouse=True)
- def _forget_seen():
- main_module._runtime_seen_connected.clear()
- yield
- main_module._runtime_seen_connected.clear()
- async def _ticks(test_engine, states: list) -> None:
- """Run the tracker for one tick per state, then stop it."""
- maker = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
- feed = iter(states)
- current = {"state": None}
- sleeps = {"n": 0}
- async def fake_sleep(_seconds):
- sleeps["n"] += 1
- if sleeps["n"] > len(states):
- raise asyncio.CancelledError
- current["state"] = next(feed)
- with (
- patch.object(main_module, "async_session", maker),
- patch.object(main_module.asyncio, "sleep", fake_sleep),
- patch.object(main_module.printer_manager, "get_status", lambda _pid: current["state"]),
- ):
- try:
- await main_module.track_printer_runtime()
- except asyncio.CancelledError:
- pass
- async def _printer(db_session, printer_factory, last_update):
- printer = await printer_factory()
- printer.runtime_seconds = 1000
- printer.last_runtime_update = last_update
- await db_session.commit()
- return printer
- async def test_a_disconnect_clears_the_timestamp(test_engine, db_session, printer_factory):
- printer = await _printer(db_session, printer_factory, datetime.utcnow() - timedelta(seconds=60))
- running = SimpleNamespace(connected=True, state="RUNNING")
- offline = SimpleNamespace(connected=False, state="RUNNING")
- await _ticks(test_engine, [running, offline])
- await db_session.refresh(printer)
- assert printer.last_runtime_update is None
- async def test_a_restart_still_counts_the_stored_timestamp(test_engine, db_session, printer_factory):
- stored = datetime.utcnow() - timedelta(minutes=30)
- printer = await _printer(db_session, printer_factory, stored)
- # Still connecting after the restart, then printing.
- connecting = SimpleNamespace(connected=False, state="unknown")
- running = SimpleNamespace(connected=True, state="RUNNING")
- await _ticks(test_engine, [connecting, running])
- await db_session.refresh(printer)
- assert printer.runtime_seconds >= 1000 + 29 * 60
- async def test_a_printer_still_offline_after_the_startup_grace_is_not_credited(
- test_engine, db_session, printer_factory, monkeypatch
- ):
- """It dropped off before the restart and never came back during it: the
- stored timestamp is from before the gap, not from a print that ran on."""
- monkeypatch.setattr(main_module, "RUNTIME_STARTUP_GRACE_SECONDS", 0)
- printer = await _printer(db_session, printer_factory, datetime.utcnow() - timedelta(hours=10))
- offline = SimpleNamespace(connected=False, state="unknown")
- await _ticks(test_engine, [offline])
- await db_session.refresh(printer)
- assert printer.last_runtime_update is None
- async def test_a_clear_that_failed_is_tried_again(test_engine, db_session, printer_factory):
- printer = await _printer(db_session, printer_factory, datetime.utcnow() - timedelta(seconds=60))
- running = SimpleNamespace(connected=True, state="RUNNING")
- offline = SimpleNamespace(connected=False, state="RUNNING")
- real_get = AsyncSession.get
- calls = {"n": 0}
- async def flaky_get(self, *args, **kwargs):
- calls["n"] += 1
- if calls["n"] == 1:
- raise RuntimeError("database is locked")
- return await real_get(self, *args, **kwargs)
- with patch.object(AsyncSession, "get", flaky_get):
- await _ticks(test_engine, [running, offline, offline])
- await db_session.refresh(printer)
- assert printer.last_runtime_update is None
|