EpisodeMatcher/tests/test_file_classifier.py
Jarian f1aa2c4cda test: add test suite with 90% coverage
- 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
2026-07-06 14:31:22 +00:00

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