- 176 unit tests covering ConfigManager, FileClassifier, EpisodeRenamer, TVDBClient, MockTVDBClient, TVDBCache, and episode_matcher CLI - Tests organized in tests/ with conftest.py shared fixtures - pytest-cov configured with 90% coverage threshold in pyproject.toml - CI workflow updated with COVERAGE_CORE=sysmon for Python 3.13 compat - .gitignore updated for coverage artifacts and venv
195 lines
7.4 KiB
Python
195 lines
7.4 KiB
Python
"""Tests for src/Matcher/file_classifier.py - FileClassifier."""
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
import pytest
|
|
from src.Matcher.file_classifier import FileClassifier, DEFAULT_VIDEO_EXTENSIONS
|
|
|
|
|
|
class TestFileClassifierInit:
|
|
def test_init_valid_folder(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
assert fc.folder_path == mock_video_files
|
|
assert len(fc.video_files) > 0
|
|
|
|
def test_init_nonexistent_folder(self, tmp_path):
|
|
with pytest.raises(FileNotFoundError, match="Folder not found"):
|
|
FileClassifier(str(tmp_path / "nope"))
|
|
|
|
def test_init_file_not_dir(self, tmp_path):
|
|
f = tmp_path / "file.txt"
|
|
f.write_text("hi")
|
|
fc = FileClassifier(str(f))
|
|
assert len(fc.video_files) == 0
|
|
|
|
def test_init_custom_extensions(self, tmp_path):
|
|
(tmp_path / "video.mp4").write_bytes(b"\x00" * 100)
|
|
(tmp_path / "video.mkv").write_bytes(b"\x00" * 100)
|
|
fc = FileClassifier(str(tmp_path), video_extensions=[".mp4"])
|
|
assert len(fc.video_files) == 1
|
|
assert fc.video_files[0].name == "video.mp4"
|
|
|
|
def test_init_fallback_ratio(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files), fallback_duration_minutes_per_gb=25.0)
|
|
assert fc.fallback_ratio == 25.0
|
|
|
|
def test_init_filters_macos_resource_fork(self, tmp_path):
|
|
(tmp_path / "._hidden.mkv").write_bytes(b"\x00" * 100)
|
|
(tmp_path / ".dot.mkv").write_bytes(b"\x00" * 100)
|
|
(tmp_path / "visible.mkv").write_bytes(b"\x00" * 100)
|
|
fc = FileClassifier(str(tmp_path))
|
|
assert len(fc.video_files) == 1
|
|
assert fc.video_files[0].name == "visible.mkv"
|
|
|
|
def test_init_deduplicates(self, tmp_path):
|
|
(tmp_path / "dup.mkv").write_bytes(b"\x00" * 100)
|
|
fc = FileClassifier(str(tmp_path), video_extensions=[".mkv"])
|
|
assert len(fc.video_files) == 1
|
|
|
|
def test_init_empty_folder(self, tmp_path):
|
|
fc = FileClassifier(str(tmp_path))
|
|
assert fc.video_files == []
|
|
|
|
|
|
class TestResolveExtensions:
|
|
def test_resolve_default(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
assert ".mkv" in fc.video_extensions
|
|
|
|
def test_resolve_custom(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files), video_extensions=["mp4", "avi"])
|
|
assert ".mp4" in fc.video_extensions
|
|
assert ".avi" in fc.video_extensions
|
|
assert ".mkv" not in fc.video_extensions
|
|
|
|
def test_resolve_env_var(self, mock_video_files, monkeypatch):
|
|
monkeypatch.setenv("VIDEO_EXTENSIONS", "webm,mp4")
|
|
fc = FileClassifier(str(mock_video_files))
|
|
assert "webm" in fc.video_extensions
|
|
assert "mp4" in fc.video_extensions
|
|
|
|
|
|
class TestResolveFallbackRatio:
|
|
def test_explicit_ratio(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files), fallback_duration_minutes_per_gb=30.0)
|
|
assert fc.fallback_ratio == 30.0
|
|
|
|
def test_env_ratio(self, mock_video_files, monkeypatch):
|
|
monkeypatch.setenv("FALLBACK_DURATION_RATIO", "33.0")
|
|
fc = FileClassifier(str(mock_video_files))
|
|
assert fc.fallback_ratio == 33.0
|
|
|
|
def test_env_ratio_invalid(self, mock_video_files, monkeypatch):
|
|
monkeypatch.setenv("FALLBACK_DURATION_RATIO", "abc")
|
|
fc = FileClassifier(str(mock_video_files))
|
|
assert fc.fallback_ratio == 45.0
|
|
|
|
|
|
class TestFileAnalysis:
|
|
def test_get_file_size(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
size = fc._get_file_size(mock_video_files / "Disc 1_t01.mkv")
|
|
assert size == int(2 * 1024)
|
|
|
|
def test_analyze_files_structure(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
assert len(fc.file_info) > 0
|
|
info = fc.file_info[0]
|
|
assert "path" in info
|
|
assert "name" in info
|
|
assert "size_bytes" in info
|
|
assert "size_mb" in info
|
|
assert "size_gb" in info
|
|
assert "duration_minutes" in info
|
|
|
|
def test_calculate_stats(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
stats = fc._calculate_stats()
|
|
assert stats["count"] == len(fc.file_info)
|
|
assert "avg_size_bytes" in stats
|
|
assert "avg_duration_minutes" in stats
|
|
assert "median_size_bytes" in stats
|
|
|
|
def test_calculate_stats_empty(self, tmp_path):
|
|
fc = FileClassifier(str(tmp_path))
|
|
stats = fc._calculate_stats()
|
|
assert stats == {}
|
|
|
|
|
|
class TestClassifyFiles:
|
|
def test_classify_no_files(self, tmp_path):
|
|
fc = FileClassifier(str(tmp_path))
|
|
episodes, extras = fc.classify_files()
|
|
assert episodes == []
|
|
assert extras == []
|
|
|
|
def test_classify_with_tvdb(self, mock_video_files, mock_tvdb_episodes):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
episodes, extras = fc.classify_files(
|
|
expected_episode_count=6, tvdb_episodes=mock_tvdb_episodes
|
|
)
|
|
assert len(episodes) <= 6
|
|
assert len(episodes) + len(extras) == len(fc.file_info)
|
|
|
|
def test_classify_by_stats(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
episodes, extras = fc.classify_files(expected_episode_count=6)
|
|
assert len(episodes) <= 6
|
|
assert len(episodes) > 0
|
|
|
|
def test_classify_extra_small_file(self, mock_video_files):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
episodes, extras = fc.classify_files()
|
|
extra_names = [e["name"] for e in extras]
|
|
assert "extra_short.mkv" in extra_names
|
|
|
|
def test_classify_expected_count(self, mock_video_files, mock_tvdb_episodes):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
episodes, extras = fc.classify_files(
|
|
expected_episode_count=3, tvdb_episodes=mock_tvdb_episodes
|
|
)
|
|
assert len(episodes) <= 3
|
|
|
|
|
|
class TestVideoDuration:
|
|
@patch("src.Matcher.file_classifier.MediaInfo", None)
|
|
def test_duration_fallback_no_mediainfo(self, mock_video_files):
|
|
fc = FileClassifier(
|
|
str(mock_video_files), fallback_duration_minutes_per_gb=45.0
|
|
)
|
|
target = mock_video_files / "Disc 1_t01.mkv"
|
|
dur = fc._get_video_duration(target)
|
|
expected = (target.stat().st_size / (1024**3)) * 45.0
|
|
assert abs(dur - expected) < 0.001
|
|
assert dur > 0
|
|
|
|
def test_duration_fallback_custom_ratio(self, mock_video_files):
|
|
with patch("src.Matcher.file_classifier.MediaInfo", None):
|
|
fc = FileClassifier(
|
|
str(mock_video_files), fallback_duration_minutes_per_gb=25.0
|
|
)
|
|
target = mock_video_files / "Disc 1_t01.mkv"
|
|
dur = fc._get_video_duration(target)
|
|
expected = (target.stat().st_size / (1024**3)) * 25.0
|
|
assert abs(dur - expected) < 0.001
|
|
assert dur > 0
|
|
|
|
|
|
class TestPrintAnalysis:
|
|
def test_print_analysis_has_files(self, mock_video_files, capsys):
|
|
fc = FileClassifier(str(mock_video_files))
|
|
fc.print_analysis()
|
|
out = capsys.readouterr().out
|
|
assert "File Analysis" in out
|
|
assert "Total files" in out
|
|
|
|
def test_print_analysis_no_files(self, tmp_path, capsys):
|
|
fc = FileClassifier(str(tmp_path))
|
|
fc.print_analysis()
|
|
out = capsys.readouterr().out
|
|
assert "No video files found" in out
|