Refactor test configurations and enhance testing framework

- Replace TempConfig with DummyConfig across tests for consistency
- Introduce unit tests workflow configuration
- Add pytest configuration for standardized test discovery
- Implement comprehensive tests for config loading and downloader behavior
- Clean up unused temp_config.py and related references
This commit is contained in:
dark_zoul committed 2025-11-24 11:21:32 +02:00
1 parent 21ba87c501
commit b5ee8dbf3b
13 files changed
+169 -19

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
name: Unit tests
on:
push:
branches: [ main, Next ]
pull_request:
branches: [ main, Next ]
jobs:
unit:
name: Run unit tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: "https://gitea.com/actions/checkout@v5"
- name: Create venv and install project
run: |
set -euo pipefail
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
# Install project (editable) and test deps
python -m pip install -e .[test] || python -m pip install -e .
python -m pip install pytest
- name: Run tests
run: |
set -euo pipefail
. .venv/bin/activate
pytest -q
+1
View File
@@ -4,6 +4,7 @@
#Custom for this project #Custom for this project
config/yt-playlist-config.json config/yt-playlist-config.json
/tmp* /tmp*
*.code-workspace
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
+5
View File
@@ -0,0 +1,5 @@
[pytest]
testpaths = tests
# Collect all standardized tests using the conventional pattern
python_files = test_*.py
addopts = -q
+9
View File
@@ -0,0 +1,9 @@
import pytest
from tests.dummy_config import DummyConfig
@pytest.fixture
def dummy_config():
"""Return a fresh DummyConfig instance for tests to customize."""
return DummyConfig()
@@ -1,7 +1,7 @@
import os import os
class TempConfig: class DummyConfig:
"""Small test configuration object used by unit and integration tests. """Small test configuration object used by unit and integration tests.
Adjust attributes via environment variables where appropriate. Adjust attributes via environment variables where appropriate.
+3 -3
View File
@@ -47,7 +47,7 @@ if bin_dir.exists():
print(f"Using local aria2c at: {aria2c_path}") print(f"Using local aria2c at: {aria2c_path}")
from ytplaylist.downloader import PlaylistDownloader from ytplaylist.downloader import PlaylistDownloader
from tests.temp_config import TempConfig from tests.dummy_config import DummyConfig
logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s')
@@ -60,7 +60,7 @@ if not playlist_url:
print(f"Using playlist URL: {playlist_url}") print(f"Using playlist URL: {playlist_url}")
cfg_base = TempConfig() cfg_base = DummyConfig()
# ensure yt-dlp exists # ensure yt-dlp exists
import shutil as _sh import shutil as _sh
@@ -76,7 +76,7 @@ root_tmp.mkdir(parents=True, exist_ok=True)
failed = False failed = False
for mode in MODES: for mode in MODES:
print(f"\n=== Running mode: {mode} ===") print(f"\n=== Running mode: {mode} ===")
cfg = TempConfig() cfg = DummyConfig()
# Allow enabling verbose subprocess output from CI by setting YTPL_DEBUG=1 # Allow enabling verbose subprocess output from CI by setting YTPL_DEBUG=1
cfg.debug = bool(os.getenv("YTPL_DEBUG", "0") == "1") cfg.debug = bool(os.getenv("YTPL_DEBUG", "0") == "1")
cfg.download_mode = mode cfg.download_mode = mode
+15 -15
View File
@@ -1,23 +1,23 @@
import logging import logging
from ytplaylist.manager import PlaylistManager from ytplaylist.manager import PlaylistManager
from tests.temp_config import TempConfig from tests.dummy_config import DummyConfig
class TestConfig(TempConfig): def test_run_with_prune_disabled():
playlists = [{"url": None, "save_path": "./tmp_test", "archive": "archive.txt"}]
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s") logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s")
print('--- Running with prune=False ---') cfg = DummyConfig()
cfg=TestConfig() cfg.playlists = [{"url": None, "save_path": "tests/tmp_test", "archive": "archive.txt"}]
m = PlaylistManager(cfg, debug=False) m = PlaylistManager(cfg, debug=False)
# should complete without raising
m.run() m.run()
print('Run complete prune=False')
print('\n--- Running with prune=True, non_interactive=True ---')
cfg2=TestConfig() def test_run_with_prune_enabled_non_interactive():
cfg2.prune=True logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s")
cfg2.non_interactive=True cfg = DummyConfig()
m2=PlaylistManager(cfg2, debug=False) cfg.playlists = [{"url": None, "save_path": "tests/tmp_test", "archive": "archive.txt"}]
m2.run() cfg.prune = True
print('Run complete prune=True non_interactive=True') cfg.non_interactive = True
m = PlaylistManager(cfg, debug=False)
# should complete without raising
m.run()
+30
View File
@@ -0,0 +1,30 @@
import json
import shutil
from pathlib import Path
from ytplaylist.config import ConfigLoader
def test_config_loader_reads_properties(tmp_path, monkeypatch):
# create a minimal config file with known binary names that exist on PATH
cfg = {
"playlists": [{"url": "https://www.youtube.com/playlist?list=FAKE", "save_path": "./tmp", "archive": "archive.txt"}],
"yt_dlp_path": "python",
"ffmpeg_path": "python",
"aria2c_path": "python",
"max_parallel_downloads": 3,
"aria2c_connections": 2,
}
p = tmp_path / "yt-playlist-config.json"
p.write_text(json.dumps(cfg), encoding="utf-8")
# Use absolute path so ConfigLoader doesn't try to create ./config
loader = ConfigLoader(str(p))
assert loader.playlists == cfg["playlists"]
assert loader.yt_dlp_path == "python"
assert loader.ffmpeg_path == "python"
assert loader.aria2c_path == "python"
assert loader.max_parallel_downloads == 3
assert loader.aria2c_connections == 2
@@ -0,0 +1,24 @@
from pathlib import Path
from ytplaylist.downloader import PlaylistDownloader
from tests.dummy_config import DummyConfig
def test_sanitize_title_and_get_file_path(tmp_path):
cfg = DummyConfig()
playlist = {"url": None, "save_path": str(tmp_path)}
dl = PlaylistDownloader(cfg, playlist, 0)
# illegal chars should be replaced and trimmed; fallback_id used when title becomes empty
title = ' My: <>:"/\\|?*Title '
safe = dl.sanitize_title(title, "ABC123")
# ensure no illegal characters remain
assert all(c not in safe for c in dl.illegal_chars)
# empty title should return fallback id
assert dl.sanitize_title(" ", "FALLBACK") == "FALLBACK"
# get_file_path uses save_path and zero-padded index
path = dl.get_file_path(5, "SongName")
assert isinstance(path, Path)
assert path.name.startswith("005 - SongName")
+26
View File
@@ -0,0 +1,26 @@
import logging
from tests.dummy_config import DummyConfig
from ytplaylist.manager import PlaylistManager
def test_manager_warns_and_sleeps(monkeypatch, caplog):
# Avoid actually sleeping during the test
slept = {"called": False}
def fake_sleep(sec):
slept["called"] = True
# monkeypatch the sleep used inside the manager module
monkeypatch.setattr("ytplaylist.manager.time.sleep", fake_sleep)
caplog.set_level(logging.WARNING)
cfg = DummyConfig()
cfg.max_parallel_downloads = 11
cfg.aria2c_connections = 10
cfg.playlists = []
m = PlaylistManager(cfg, debug=False)
m.run()
assert slept["called"] is True
assert any("may overload your network" in rec.getMessage() for rec in caplog.records)
@@ -0,0 +1,23 @@
import logging
from ytplaylist.manager import PlaylistManager
from tests.dummy_config import DummyConfig
def test_run_with_prune_disabled():
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s")
cfg = DummyConfig()
cfg.playlists = [{"url": None, "save_path": "tests/tmp_test", "archive": "archive.txt"}]
m = PlaylistManager(cfg, debug=False)
# should complete without raising
m.run()
def test_run_with_prune_enabled_non_interactive():
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(message)s")
cfg = DummyConfig()
cfg.playlists = [{"url": None, "save_path": "tests/tmp_test", "archive": "archive.txt"}]
cfg.prune = True
cfg.non_interactive = True
m = PlaylistManager(cfg, debug=False)
# should complete without raising
m.run()
View File
Whitespace-only changes.
View File
Whitespace-only changes.