background_tasks.py 1.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243
  1. """Patching ``spawn_background_task`` without leaking the coroutine.
  2. Every caller builds its coroutine as a *call argument*::
  3. spawn_background_task(self._watchdog_print_start(...), name=...)
  4. so the coroutine object is constructed whether or not the replacement ever
  5. schedules it. A bare ``MagicMock`` then parks it in ``call_args`` and it is
  6. finalised, never awaited, during some *later* test's garbage collection —
  7. surfacing as a ``PytestUnraisableExceptionWarning`` attributed to whichever
  8. unrelated test happened to be running at the time. That makes the report
  9. useless for finding the leak and, because it depends on GC timing and test
  10. order, it appears and disappears between runs of the same suite.
  11. Closing the coroutine mirrors what the real helper does — take ownership of it
  12. — while still keeping the work from running.
  13. ``DEFAULT`` rather than the ``close()`` return: the mock's return value stands
  14. in for the ``asyncio.Task``, and the queue-pool path in ``_process_queue``
  15. calls ``task.add_done_callback(...)`` on it. Returning ``None`` from the
  16. side-effect would replace the usual ``MagicMock`` return with ``None`` and
  17. break that caller.
  18. """
  19. from unittest.mock import DEFAULT, patch
  20. SCHEDULER_TARGET = "backend.app.services.print_scheduler.spawn_background_task"
  21. MAIN_TARGET = "backend.app.main.spawn_background_task"
  22. def close_and_default(coro, **kwargs):
  23. """Take ownership of ``coro`` the way the real helper would, then stand down."""
  24. coro.close()
  25. return DEFAULT
  26. def discarding_spawn_patch(target: str = SCHEDULER_TARGET):
  27. """``patch`` for ``spawn_background_task`` that closes what it is handed.
  28. A drop-in for ``patch(target, MagicMock())`` — still a mock, so call
  29. assertions work — that does not leave an un-awaited coroutine behind.
  30. """
  31. return patch(target, side_effect=close_and_default)