refactor: use one tmp directory for tests
This commit is contained in:
parent
f33dc84c57
commit
fc6bc88e61
18 changed files with 230 additions and 110 deletions
22
app/conftest.py
Normal file
22
app/conftest.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from app.tests.helpers import cleanup_test_run_root, get_test_run_root, get_test_system_temp_root
|
||||
|
||||
|
||||
def pytest_configure(config) -> None:
|
||||
temp_root = get_test_system_temp_root()
|
||||
for env_name in ("TMPDIR", "TEMP", "TMP"):
|
||||
os.environ[env_name] = str(temp_root)
|
||||
|
||||
tempfile.tempdir = None
|
||||
|
||||
if getattr(config.option, "basetemp", None) is None:
|
||||
config.option.basetemp = str(get_test_run_root() / "pytest")
|
||||
|
||||
|
||||
def pytest_unconfigure(config) -> None:
|
||||
del config
|
||||
cleanup_test_run_root()
|
||||
|
|
@ -14,6 +14,7 @@ from app.library.config import Config
|
|||
from app.library.encoder import Encoder
|
||||
from app.features.conditions.repository import ConditionsRepository
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
|
|
@ -21,7 +22,7 @@ async def repo():
|
|||
ConditionsRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
||||
store = SqliteStore(db_path=":memory:")
|
||||
store = SqliteStore(db_path=make_in_memory_db_path("conditions-repository"))
|
||||
await store.get_connection()
|
||||
|
||||
# Create repository
|
||||
|
|
@ -30,10 +31,7 @@ async def repo():
|
|||
yield repository
|
||||
|
||||
# Cleanup - close connections properly
|
||||
if store._conn:
|
||||
await store._conn.close()
|
||||
if store._engine:
|
||||
await store._engine.dispose()
|
||||
await store.close()
|
||||
|
||||
# Reset singletons
|
||||
ConditionsRepository._reset_singleton()
|
||||
|
|
|
|||
|
|
@ -8,26 +8,24 @@ import pytest_asyncio
|
|||
from app.features.dl_fields.repository import DLFieldsRepository
|
||||
from app.features.dl_fields.service import DLFields
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def repo(tmp_path):
|
||||
async def repo():
|
||||
"""Provide a fresh repository instance with initialized database for each test."""
|
||||
DLFieldsRepository._reset_singleton()
|
||||
DLFields._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
||||
store = SqliteStore(db_path=":memory:")
|
||||
store = SqliteStore(db_path=make_in_memory_db_path("dl-fields-service"))
|
||||
await store.get_connection()
|
||||
|
||||
repository = DLFieldsRepository.get_instance()
|
||||
|
||||
yield repository
|
||||
|
||||
if store._conn:
|
||||
await store._conn.close()
|
||||
if store._engine:
|
||||
await store._engine.dispose()
|
||||
await store.close()
|
||||
|
||||
DLFieldsRepository._reset_singleton()
|
||||
DLFields._reset_singleton()
|
||||
|
|
|
|||
|
|
@ -7,25 +7,23 @@ import pytest_asyncio
|
|||
|
||||
from app.features.dl_fields.repository import DLFieldsRepository
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def repo(tmp_path):
|
||||
async def repo():
|
||||
"""Provide a fresh repository instance with initialized database for each test."""
|
||||
DLFieldsRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
||||
store = SqliteStore(db_path=str(":memory:"))
|
||||
store = SqliteStore(db_path=make_in_memory_db_path("dl-fields-repository"))
|
||||
await store.get_connection()
|
||||
|
||||
repository = DLFieldsRepository.get_instance()
|
||||
|
||||
yield repository
|
||||
|
||||
if store._conn:
|
||||
await store._conn.close()
|
||||
if store._engine:
|
||||
await store._engine.dispose()
|
||||
await store.close()
|
||||
|
||||
DLFieldsRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
|
|
|||
|
|
@ -7,25 +7,23 @@ import pytest_asyncio
|
|||
|
||||
from app.features.notifications.repository import NotificationsRepository
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def repo(tmp_path):
|
||||
async def repo():
|
||||
"""Provide a fresh repository instance with initialized database for each test."""
|
||||
NotificationsRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
||||
store = SqliteStore(db_path=":memory:")
|
||||
store = SqliteStore(db_path=make_in_memory_db_path("notifications-repository"))
|
||||
await store.get_connection()
|
||||
|
||||
repository = NotificationsRepository.get_instance()
|
||||
|
||||
yield repository
|
||||
|
||||
if store._conn:
|
||||
await store._conn.close()
|
||||
if store._engine:
|
||||
await store._engine.dispose()
|
||||
await store.close()
|
||||
|
||||
NotificationsRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from app.features.presets.repository import PresetsRepository
|
|||
from app.features.presets.router import presets_list
|
||||
from app.library.encoder import Encoder
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
|
|
@ -19,16 +20,13 @@ async def repo():
|
|||
PresetsRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
||||
store = SqliteStore(db_path=":memory:")
|
||||
store = SqliteStore(db_path=make_in_memory_db_path("presets-repository"))
|
||||
await store.get_connection()
|
||||
|
||||
repository = PresetsRepository.get_instance()
|
||||
yield repository
|
||||
|
||||
if store._conn:
|
||||
await store._conn.close()
|
||||
if store._engine:
|
||||
await store._engine.dispose()
|
||||
await store.close()
|
||||
|
||||
PresetsRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import subprocess # type: ignore
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from secrets import token_hex
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
|
||||
from aiohttp import web
|
||||
|
|
@ -68,21 +68,21 @@ class Segments:
|
|||
self.attempted: set[str] = set()
|
||||
"The set of attempted codecs."
|
||||
|
||||
async def build_ffmpeg_args(self, file: Path, s_codec: str) -> list[str]:
|
||||
def _make_stream_input(self, file: Path) -> Path:
|
||||
while True:
|
||||
stream_input = Path(tempfile.gettempdir()).joinpath(f"ytptube_stream.{token_hex(8)}")
|
||||
try:
|
||||
stream_input.symlink_to(file, target_is_directory=False)
|
||||
return stream_input
|
||||
except FileExistsError:
|
||||
continue
|
||||
|
||||
async def build_ffmpeg_args(self, file: Path, s_codec: str, *, stream_input: Path | None = None) -> list[str]:
|
||||
try:
|
||||
ff: FFProbeResult = await ffprobe(file)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
tmpFile: Path = Path(tempfile.gettempdir()).joinpath(
|
||||
f"ytptube_stream.{hashlib.sha256(str(file).encode()).hexdigest()}"
|
||||
)
|
||||
|
||||
if not tmpFile.exists():
|
||||
try:
|
||||
tmpFile.symlink_to(file, target_is_directory=False)
|
||||
except FileExistsError:
|
||||
pass
|
||||
input_path = stream_input or file
|
||||
|
||||
startTime: str = f"{0:.6f}" if self.index == 0 else f"{self.duration * self.index:.6f}"
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ class Segments:
|
|||
# hardware/global input options must come before -i
|
||||
*input_args,
|
||||
"-i",
|
||||
f"file:{tmpFile}",
|
||||
f"file:{input_path}",
|
||||
"-map_metadata",
|
||||
"-1",
|
||||
]
|
||||
|
|
@ -240,22 +240,27 @@ class Segments:
|
|||
else:
|
||||
codecs: list[str] = [codec, *list(encoder_fallback_chain(codec))]
|
||||
|
||||
for s_codec in codecs:
|
||||
if s_codec in self.attempted:
|
||||
continue
|
||||
stream_input = self._make_stream_input(file)
|
||||
try:
|
||||
for s_codec in codecs:
|
||||
if s_codec in self.attempted:
|
||||
continue
|
||||
|
||||
ffmpeg_args: list[str] = await self.build_ffmpeg_args(file, s_codec)
|
||||
_, rc, client_disconnected, stderr_text = await self._run(resp, file, ffmpeg_args)
|
||||
ffmpeg_args: list[str] = await self.build_ffmpeg_args(file, s_codec, stream_input=stream_input)
|
||||
_, rc, client_disconnected, stderr_text = await self._run(resp, file, ffmpeg_args)
|
||||
|
||||
if 0 == rc:
|
||||
Segments._cached_vcodec = s_codec
|
||||
Segments._cache_initialized = True
|
||||
return
|
||||
if 0 == rc:
|
||||
Segments._cached_vcodec = s_codec
|
||||
Segments._cache_initialized = True
|
||||
return
|
||||
|
||||
if client_disconnected:
|
||||
return
|
||||
if client_disconnected:
|
||||
return
|
||||
|
||||
if 0 != rc:
|
||||
err: str = stderr_text[:500] if stderr_text else "no error output"
|
||||
LOG.warning(f"transcoding has failed (cmd={ffmpeg_args}) (rc={rc}): {err}. Trying fallbacks.")
|
||||
self.attempted.add(s_codec)
|
||||
if 0 != rc:
|
||||
err: str = stderr_text[:500] if stderr_text else "no error output"
|
||||
LOG.warning(f"transcoding has failed (cmd={ffmpeg_args}) (rc={rc}): {err}. Trying fallbacks.")
|
||||
self.attempted.add(s_codec)
|
||||
finally:
|
||||
if stream_input.is_symlink():
|
||||
stream_input.unlink()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.tests.helpers import make_test_temp_dir
|
||||
|
||||
|
||||
class TestFFProbe:
|
||||
"""Test the ffprobe module functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test files."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("ffprobe"))
|
||||
self.test_file = Path(self.temp_dir) / "test_video.mp4"
|
||||
self.test_file.touch()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from app.features.streaming.library.segments import Segments
|
||||
from app.tests.helpers import get_test_system_temp_root
|
||||
|
||||
|
||||
class DummyFF:
|
||||
|
|
@ -22,6 +21,10 @@ class DummyFF:
|
|||
return self._a
|
||||
|
||||
|
||||
def _ffmpeg_input_path(args: list[str]) -> Path:
|
||||
return Path(args[args.index("-i") + 1].removeprefix("file:"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_ffmpeg_args_video_and_audio(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Create a dummy media file
|
||||
|
|
@ -53,8 +56,7 @@ async def test_build_ffmpeg_args_video_and_audio(tmp_path: Path, monkeypatch: py
|
|||
assert captured_args, "ffmpeg was not invoked"
|
||||
args = captured_args[0]
|
||||
|
||||
# Compute expected symlink path used by Segments
|
||||
tmpFile = Path(tempfile.gettempdir()).joinpath(f"ytptube_stream.{hashlib.sha256(str(media).encode()).hexdigest()}")
|
||||
tmp_file = _ffmpeg_input_path(args)
|
||||
|
||||
# Start time is duration * index with 6 decimals for non-zero index
|
||||
assert "-ss" in args
|
||||
|
|
@ -64,7 +66,9 @@ async def test_build_ffmpeg_args_video_and_audio(tmp_path: Path, monkeypatch: py
|
|||
assert args[args.index("-t") + 1] == f"{5.5:.6f}"
|
||||
# Input uses file:<symlink>
|
||||
assert "-i" in args
|
||||
assert args[args.index("-i") + 1] == f"file:{tmpFile}"
|
||||
assert tmp_file.parent == get_test_system_temp_root()
|
||||
assert tmp_file.name.startswith("ytptube_stream.")
|
||||
assert not tmp_file.exists()
|
||||
# Includes video and audio mapping and codecs
|
||||
assert "-map" in args
|
||||
assert "0:v:0" in args
|
||||
|
|
@ -338,6 +342,9 @@ async def test_stream_gpu_fallback_switches_codec(monkeypatch: pytest.MonkeyPatc
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_normal_flow(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
media = tmp_path / "file.mp4"
|
||||
media.write_bytes(b"data")
|
||||
|
||||
async def fake_ffprobe(_file: Path):
|
||||
return DummyFF(v=True, a=True)
|
||||
|
||||
|
|
@ -346,16 +353,21 @@ async def test_stream_normal_flow(monkeypatch: pytest.MonkeyPatch, tmp_path: Pat
|
|||
# Process that yields two chunks and then EOF
|
||||
proc = _FakeProc([b"abc", b"def", b""])
|
||||
|
||||
async def fake_create_subprocess_exec(*_args: Any, **_kwargs: Any):
|
||||
captured_args: list[list[str]] = []
|
||||
|
||||
async def fake_create_subprocess_exec(*args: Any, **_kwargs: Any):
|
||||
captured_args.append(list(args[1:]))
|
||||
return proc
|
||||
|
||||
monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec)
|
||||
|
||||
seg = Segments(download_path=str(tmp_path), index=0, duration=1.0, vconvert=True, aconvert=True)
|
||||
resp = _FakeResp()
|
||||
await seg.stream(tmp_path / "file.mp4", resp)
|
||||
await seg.stream(media, resp)
|
||||
|
||||
assert bytes(resp.data) == b"abcdef"
|
||||
assert not _ffmpeg_input_path(captured_args[0]).exists()
|
||||
assert media.exists()
|
||||
# EOF behavior may differ; don't require True
|
||||
|
||||
|
||||
|
|
@ -368,7 +380,10 @@ async def test_stream_client_reset(monkeypatch: pytest.MonkeyPatch, tmp_path: Pa
|
|||
|
||||
proc = _FakeProc([b"abc", b"def"]) # will attempt to write and fail
|
||||
|
||||
async def fake_create_subprocess_exec(*_args: Any, **_kwargs: Any):
|
||||
captured_args: list[list[str]] = []
|
||||
|
||||
async def fake_create_subprocess_exec(*args: Any, **_kwargs: Any):
|
||||
captured_args.append(list(args[1:]))
|
||||
return proc
|
||||
|
||||
monkeypatch.setattr("asyncio.create_subprocess_exec", fake_create_subprocess_exec)
|
||||
|
|
@ -380,6 +395,7 @@ async def test_stream_client_reset(monkeypatch: pytest.MonkeyPatch, tmp_path: Pa
|
|||
|
||||
# Should not write EOF due to client disconnect
|
||||
assert resp.eof is False
|
||||
assert not _ffmpeg_input_path(captured_args[0]).exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from app.features.tasks.definitions.router import (
|
|||
from app.library.encoder import Encoder
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.main import EventBus
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
def _sample_definition(name: str = "example", *, priority: int = 0) -> dict:
|
||||
|
|
@ -49,17 +50,14 @@ async def repo() -> AsyncGenerator[TaskDefinitionsRepository, None]:
|
|||
TaskDefinitionsRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
||||
store = SqliteStore(db_path=":memory:")
|
||||
store = SqliteStore(db_path=make_in_memory_db_path("task-definitions-repository"))
|
||||
await store.get_connection()
|
||||
|
||||
repository = TaskDefinitionsRepository.get_instance()
|
||||
|
||||
yield repository
|
||||
|
||||
if store._conn:
|
||||
await store._conn.close()
|
||||
if store._engine:
|
||||
await store._engine.dispose()
|
||||
await store.close()
|
||||
|
||||
TaskDefinitionsRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
|
|
|||
|
|
@ -7,25 +7,23 @@ import pytest_asyncio
|
|||
|
||||
from app.features.tasks.repository import TasksRepository
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def repo(tmp_path):
|
||||
async def repo():
|
||||
"""Provide a fresh repository instance with initialized database for each test."""
|
||||
TasksRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
||||
store = SqliteStore(db_path=str(":memory:"))
|
||||
store = SqliteStore(db_path=make_in_memory_db_path("tasks-repository"))
|
||||
await store.get_connection()
|
||||
|
||||
repository = TasksRepository.get_instance()
|
||||
|
||||
yield repository
|
||||
|
||||
if store._conn:
|
||||
await store._conn.close()
|
||||
if store._engine:
|
||||
await store._engine.dispose()
|
||||
await store.close()
|
||||
|
||||
TasksRepository._reset_singleton()
|
||||
SqliteStore._reset_singleton()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -20,6 +19,7 @@ from app.features.ytdlp.utils import (
|
|||
archive_add,
|
||||
archive_read,
|
||||
)
|
||||
from app.tests.helpers import make_test_temp_dir
|
||||
|
||||
|
||||
class CaptureHandler(logging.Handler):
|
||||
|
|
@ -549,7 +549,7 @@ class TestArchiveFunctions:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test archive file."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("ytdlp-archive"))
|
||||
self.archive_file = Path(self.temp_dir) / "archive.txt"
|
||||
|
||||
def teardown_method(self):
|
||||
|
|
|
|||
64
app/tests/helpers.py
Normal file
64
app/tests/helpers.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import contextlib
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import uuid4
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
_TEST_RUN_ROOT = Path(gettempdir()) / "ytptube-tests" / uuid4().hex
|
||||
_TEST_SYSTEM_TEMP_ROOT = _TEST_RUN_ROOT / "tmp"
|
||||
|
||||
|
||||
def _slugify(name: str) -> str:
|
||||
return "".join(char if char.isalnum() else "-" for char in name).strip("-") or "tmp"
|
||||
|
||||
|
||||
def get_test_run_root() -> Path:
|
||||
return _TEST_RUN_ROOT
|
||||
|
||||
|
||||
def get_test_system_temp_root() -> Path:
|
||||
_TEST_SYSTEM_TEMP_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
return _TEST_SYSTEM_TEMP_ROOT
|
||||
|
||||
|
||||
def cleanup_test_run_root() -> None:
|
||||
shutil.rmtree(_TEST_RUN_ROOT, ignore_errors=True)
|
||||
|
||||
|
||||
def make_in_memory_db_path(name: str) -> str:
|
||||
"""Return a unique named in-memory SQLite path for test isolation."""
|
||||
slug = _slugify(name)
|
||||
return f":memory:{slug}-{uuid4().hex}"
|
||||
|
||||
|
||||
def make_test_disk_path(*parts: str) -> Path:
|
||||
"""Return a per-run temp path for tests that must write to disk."""
|
||||
_TEST_RUN_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
path = _TEST_RUN_ROOT.joinpath(*parts)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def make_test_temp_dir(name: str) -> Path:
|
||||
path = make_test_disk_path(f"{_slugify(name)}-{uuid4().hex}")
|
||||
path.mkdir(parents=True, exist_ok=False)
|
||||
return path
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def temporary_test_dir(name: str) -> Iterator[Path]:
|
||||
path = make_test_temp_dir(name)
|
||||
try:
|
||||
yield path
|
||||
finally:
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
|
||||
atexit.register(cleanup_test_run_root)
|
||||
|
|
@ -5,14 +5,13 @@ from dataclasses import asdict
|
|||
from datetime import UTC, datetime
|
||||
from email.utils import formatdate
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from app.library.DataStore import DataStore, StoreType
|
||||
from app.library.ItemDTO import ItemDTO
|
||||
from app.library.operations import Operation
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
async def reset_sqlite_store() -> None:
|
||||
|
|
@ -32,7 +31,7 @@ async def reset_sqlite_store() -> None:
|
|||
async def make_db(data: int = 0) -> SqliteStore:
|
||||
"""Create a named in-memory database with test data."""
|
||||
await reset_sqlite_store()
|
||||
ins = SqliteStore.get_instance(db_path=f":memory:test-datastore-{uuid4().hex}")
|
||||
ins = SqliteStore.get_instance(db_path=make_in_memory_db_path("test-datastore"))
|
||||
await ins.get_connection()
|
||||
|
||||
base_time = datetime.now(UTC)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import json
|
||||
from datetime import UTC, datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
|
@ -9,6 +8,7 @@ from app.library.DataStore import DataStore, StoreType
|
|||
from app.library.downloads import Download
|
||||
from app.library.ItemDTO import ItemDTO
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
async def reset_sqlite_store() -> None:
|
||||
|
|
@ -28,7 +28,7 @@ async def reset_sqlite_store() -> None:
|
|||
async def make_db(data: int = 100) -> SqliteStore:
|
||||
"""Create a named in-memory database with test data."""
|
||||
await reset_sqlite_store()
|
||||
db_path = f":memory:test-datastore-pagination-{uuid4().hex}"
|
||||
db_path = make_in_memory_db_path("test-datastore-pagination")
|
||||
ins = SqliteStore.get_instance(db_path=db_path)
|
||||
await ins.get_connection()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
from datetime import UTC, datetime, timedelta
|
||||
import os
|
||||
from tempfile import mkstemp
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -9,14 +7,13 @@ from sqlalchemy import text
|
|||
from app.library.ItemDTO import ItemDTO
|
||||
from app.library.operations import Operation
|
||||
from app.library.sqlite_store import SqliteStore
|
||||
from app.tests.helpers import make_in_memory_db_path
|
||||
|
||||
|
||||
async def make_store() -> SqliteStore:
|
||||
"""Create an isolated temporary SqliteStore instance with schema."""
|
||||
"""Create an isolated named in-memory SqliteStore instance with schema."""
|
||||
SqliteStore._reset_singleton()
|
||||
fd, db_path = mkstemp(prefix="ytptube-sqlite-store-", suffix=".db")
|
||||
os.close(fd)
|
||||
store = SqliteStore.get_instance(db_path=db_path)
|
||||
store = SqliteStore.get_instance(db_path=make_in_memory_db_path("sqlite-store"))
|
||||
await store.get_connection()
|
||||
assert store._engine is not None, "Engine should be initialized after _ensure_conn"
|
||||
return store
|
||||
|
|
@ -38,7 +35,7 @@ def make_item(idx: int, *, status: str = "finished", cli: str = "", download_ski
|
|||
async def test_sessionmaker_returns_valid_sessionmaker() -> None:
|
||||
"""Test that sessionmaker() returns a working async_sessionmaker."""
|
||||
SqliteStore._reset_singleton()
|
||||
store = SqliteStore.get_instance(db_path=":memory:")
|
||||
store = SqliteStore.get_instance(db_path=make_in_memory_db_path("sessionmaker"))
|
||||
|
||||
# Ensure connection is initialized
|
||||
await store.get_connection()
|
||||
|
|
@ -58,7 +55,7 @@ async def test_sessionmaker_returns_valid_sessionmaker() -> None:
|
|||
async def test_sessionmaker_raises_before_init() -> None:
|
||||
"""Test that sessionmaker() raises error before connection initialization."""
|
||||
SqliteStore._reset_singleton()
|
||||
store = SqliteStore.get_instance(db_path=":memory:")
|
||||
store = SqliteStore.get_instance(db_path=make_in_memory_db_path("sessionmaker-before-init"))
|
||||
|
||||
with pytest.raises(RuntimeError, match="Database connection not initialized"):
|
||||
store.sessionmaker()
|
||||
|
|
@ -70,7 +67,7 @@ async def test_sessionmaker_raises_before_init() -> None:
|
|||
async def test_sqlalchemy_engine_disposed_on_close() -> None:
|
||||
"""Test that SQLAlchemy engine is properly disposed on close."""
|
||||
SqliteStore._reset_singleton()
|
||||
store = SqliteStore.get_instance(db_path=":memory:")
|
||||
store = SqliteStore.get_instance(db_path=make_in_memory_db_path("engine-close"))
|
||||
|
||||
await store.get_connection()
|
||||
assert store._engine is not None, "Engine should be created"
|
||||
|
|
|
|||
32
app/tests/test_test_helpers.py
Normal file
32
app/tests/test_test_helpers.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from app.tests.helpers import (
|
||||
get_test_run_root,
|
||||
get_test_system_temp_root,
|
||||
make_test_disk_path,
|
||||
make_test_temp_dir,
|
||||
)
|
||||
|
||||
|
||||
def test_make_test_disk_path_uses_test_run_root() -> None:
|
||||
path = make_test_disk_path("artifacts", "example.txt")
|
||||
|
||||
assert path.parent.exists()
|
||||
assert path.is_relative_to(get_test_run_root())
|
||||
|
||||
|
||||
def test_make_test_temp_dir_creates_directory() -> None:
|
||||
path = make_test_temp_dir("helpers")
|
||||
|
||||
assert path.exists()
|
||||
assert path.is_dir()
|
||||
assert path.is_relative_to(get_test_run_root())
|
||||
|
||||
|
||||
def test_tmp_path_runs_under_custom_temp_root(tmp_path: Path) -> None:
|
||||
expected_root = get_test_run_root() / "pytest"
|
||||
|
||||
assert tmp_path.is_relative_to(expected_root)
|
||||
assert get_test_system_temp_root().is_relative_to(get_test_run_root())
|
||||
|
|
@ -2,7 +2,6 @@ import asyncio
|
|||
import copy
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
|
@ -43,6 +42,7 @@ from app.library.Utils import (
|
|||
validate_uuid,
|
||||
)
|
||||
from app.routes.api.logs import _read_logfile, _tail_log
|
||||
from app.tests.helpers import make_test_temp_dir, temporary_test_dir
|
||||
|
||||
|
||||
class TestTimedLruCache:
|
||||
|
|
@ -283,7 +283,7 @@ class TestCalcDownloadPath:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test directory."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("calc-download-path"))
|
||||
self.base_path = Path(self.temp_dir)
|
||||
|
||||
def teardown_method(self):
|
||||
|
|
@ -977,7 +977,7 @@ class TestDeleteDir:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test directory."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("delete-dir"))
|
||||
self.test_dir = Path(self.temp_dir) / "test_delete"
|
||||
self.test_dir.mkdir()
|
||||
(self.test_dir / "file.txt").write_text("test content")
|
||||
|
|
@ -1007,7 +1007,7 @@ class TestListFolders:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test directory structure."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("list-folders"))
|
||||
self.base = Path(self.temp_dir)
|
||||
(self.base / "folder1").mkdir()
|
||||
(self.base / "folder2").mkdir()
|
||||
|
|
@ -1237,8 +1237,7 @@ class TestGetFileSidecar:
|
|||
|
||||
def test_get_file_sidecar_with_files(self):
|
||||
"""Test getting sidecar files when they exist."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_path = Path(temp_dir)
|
||||
with temporary_test_dir("file-sidecar") as base_path:
|
||||
video_file = base_path / "video.mp4"
|
||||
srt_file = base_path / "video.srt"
|
||||
nfo_file = base_path / "video.nfo"
|
||||
|
|
@ -1252,8 +1251,7 @@ class TestGetFileSidecar:
|
|||
|
||||
def test_get_file_sidecar_no_files(self):
|
||||
"""Test getting sidecar files when none exist."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_path = Path(temp_dir)
|
||||
with temporary_test_dir("file-sidecar-empty") as base_path:
|
||||
video_file = base_path / "video.mp4"
|
||||
video_file.write_text("video content")
|
||||
|
||||
|
|
@ -1266,7 +1264,7 @@ class TestCheckId:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test files."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("check-id"))
|
||||
self.test_dir = Path(self.temp_dir)
|
||||
|
||||
def teardown_method(self):
|
||||
|
|
@ -1347,7 +1345,7 @@ class TestGetPossibleImages:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test directory with images."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("possible-images"))
|
||||
self.test_dir = Path(self.temp_dir)
|
||||
|
||||
# Create some test image files
|
||||
|
|
@ -1408,7 +1406,7 @@ class TestGetFile:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test files."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("get-file"))
|
||||
self.download_path = Path(self.temp_dir)
|
||||
|
||||
def teardown_method(self):
|
||||
|
|
@ -1472,7 +1470,7 @@ class TestGetFiles:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test directory structure."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("get-files"))
|
||||
self.base_path = Path(self.temp_dir)
|
||||
|
||||
# Create test files and directories
|
||||
|
|
@ -1504,7 +1502,7 @@ class TestReadLogfile:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test log file."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("read-logfile"))
|
||||
self.log_file = Path(self.temp_dir) / "test.log"
|
||||
|
||||
def teardown_method(self):
|
||||
|
|
@ -1540,7 +1538,7 @@ class TestTailLog:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test log file."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("tail-log"))
|
||||
self.log_file = Path(self.temp_dir) / "test.log"
|
||||
|
||||
def teardown_method(self):
|
||||
|
|
@ -1571,7 +1569,7 @@ class TestLoadCookies:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test cookie file."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("load-cookies"))
|
||||
self.cookie_file = Path(self.temp_dir) / "cookies.txt"
|
||||
|
||||
def teardown_method(self):
|
||||
|
|
@ -1640,7 +1638,7 @@ class TestLoadModules:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test module structure."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("load-modules"))
|
||||
self.root_path = Path(self.temp_dir)
|
||||
self.module_dir = self.root_path / "test_modules"
|
||||
self.module_dir.mkdir()
|
||||
|
|
@ -1885,7 +1883,7 @@ class TestCreateCookiesFile:
|
|||
|
||||
def setup_method(self):
|
||||
"""Set up test environment."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_dir = str(make_test_temp_dir("create-cookies-file"))
|
||||
self.test_path = Path(self.temp_dir)
|
||||
|
||||
def teardown_method(self):
|
||||
|
|
|
|||
Loading…
Reference in a new issue