metube/app/tests/test_bg_tasks.py
Alex Shnitman 3ea4732c5d fix: harden download lifecycle, subscriptions, and UI robustness
Addresses a full-project review. Backend correctness and availability:

- ytdl: cancel() only SIGKILLs the child's process group when the child
  actually became its own group leader, so a race (or failed setpgrp)
  can no longer kill the whole server; kill the group on cancel and on
  shutdown to avoid orphaned ffmpeg children
- ytdl: dedicated ThreadPoolExecutor for download supervision so active
  downloads can't starve extract_info / live probes on the default pool
- ytdl/main/subscriptions: route fire-and-forget tasks through a
  bg_tasks helper that keeps a strong ref and logs failures
- subscriptions: run flat-playlist extraction in an executor and check
  feeds with bounded concurrency so one slow feed can't block the loop;
  set last_checked on failure so broken feeds aren't retried every 60s
- main: validate ids on /start & /delete and numeric env vars at startup;
  return 400 (not 500) on bad subscriptions/update input; serve /history
  from memory; move get_custom_dirs off the event loop; restrict t=
  stripping to YouTube hosts; drop double percent-decode in state guard
- dl_formats/ytdl: enforce requested caption format via
  FFmpegSubtitlesConvertor and strip VTT header metadata only in the
  pre-cue region so real dialogue is preserved
- ytdl: throttle progress events, dedup adds against pending, clear
  filename/size on error and reject out-of-dir trashcan deletes, pin
  fork start-method on Linux only

Frontend:

- retry deletes the done record only after a successful re-add
- surface HTTP errors for delete/start and reset the deleting flag
- ignore late 'updated' events for rows no longer in the queue
- track table rows by map key; FileSizePipe uses base-1024

Also: HTTPS-aware Docker healthcheck, dead-code removal, and shared
helpers for path-containment and yt-dlp option merging. Adds/updates
unit tests throughout (250 backend tests passing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 08:08:14 +03:00

55 lines
1.6 KiB
Python

"""Tests for the ``bg_tasks.create_task`` strong-reference/logging helper."""
from __future__ import annotations
import asyncio
import logging
import pytest
import bg_tasks
@pytest.mark.asyncio
async def test_create_task_removes_itself_from_registry_on_success():
async def _ok():
return 42
task = bg_tasks.create_task(_ok(), name="ok_task")
assert task in bg_tasks._TASKS
result = await task
assert result == 42
assert task not in bg_tasks._TASKS
@pytest.mark.asyncio
async def test_create_task_logs_unhandled_exception(caplog):
async def _boom():
raise ValueError("kaboom")
with caplog.at_level(logging.ERROR, logger="bg_tasks"):
task = bg_tasks.create_task(_boom(), name="boom_task")
with pytest.raises(ValueError):
await task
# Let the done-callback (scheduled via call_soon) run.
await asyncio.sleep(0)
assert task not in bg_tasks._TASKS
assert any("boom_task" in record.message for record in caplog.records)
@pytest.mark.asyncio
async def test_create_task_does_not_log_on_cancellation(caplog):
async def _sleep_forever():
await asyncio.sleep(10)
with caplog.at_level(logging.ERROR, logger="bg_tasks"):
task = bg_tasks.create_task(_sleep_forever(), name="cancel_task")
await asyncio.sleep(0)
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
await asyncio.sleep(0)
assert task not in bg_tasks._TASKS
assert not any("cancel_task" in record.message for record in caplog.records)