Rework Tagger fork into Curator movie-library manager (PySide6 GUI, pool index, ČSFD import)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Tests package
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Konfigurace pytest - sdílené fixtures a nastavení pro všechny testy
|
||||
"""
|
||||
import pytest
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def session_temp_dir():
|
||||
"""Session-wide dočasný adresář"""
|
||||
temp_dir = Path(tempfile.mkdtemp())
|
||||
yield temp_dir
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_config_files():
|
||||
"""Automaticky vyčistí config.json soubory po každém testu"""
|
||||
yield
|
||||
# Cleanup po testu
|
||||
config_file = Path("config.json")
|
||||
if config_file.exists():
|
||||
try:
|
||||
config_file.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,413 @@
|
||||
import pytest
|
||||
import json
|
||||
from src.core.config import (
|
||||
load_global_config, save_global_config, DEFAULT_GLOBAL_CONFIG,
|
||||
load_folder_config, save_folder_config, DEFAULT_FOLDER_CONFIG,
|
||||
get_folder_config_path, folder_has_config, FOLDER_CONFIG_NAME,
|
||||
load_config, save_config # Legacy functions
|
||||
)
|
||||
|
||||
|
||||
class TestGlobalConfig:
|
||||
"""Testy pro globální config"""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
"""Fixture pro dočasný globální config soubor"""
|
||||
config_path = tmp_path / "config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
def test_default_global_config_structure(self):
|
||||
"""Test struktury defaultní globální konfigurace"""
|
||||
assert "window_geometry" in DEFAULT_GLOBAL_CONFIG
|
||||
assert "window_maximized" in DEFAULT_GLOBAL_CONFIG
|
||||
assert "last_folder" in DEFAULT_GLOBAL_CONFIG
|
||||
assert "sidebar_width" in DEFAULT_GLOBAL_CONFIG
|
||||
assert "recent_folders" in DEFAULT_GLOBAL_CONFIG
|
||||
assert DEFAULT_GLOBAL_CONFIG["window_geometry"] == "1200x800"
|
||||
assert DEFAULT_GLOBAL_CONFIG["window_maximized"] is False
|
||||
assert DEFAULT_GLOBAL_CONFIG["last_folder"] is None
|
||||
|
||||
def test_load_global_config_nonexistent_file(self, temp_global_config):
|
||||
"""Test načtení globální konfigurace když soubor neexistuje"""
|
||||
config = load_global_config()
|
||||
assert config == DEFAULT_GLOBAL_CONFIG
|
||||
|
||||
def test_save_global_config(self, temp_global_config):
|
||||
"""Test uložení globální konfigurace"""
|
||||
test_config = {
|
||||
"window_geometry": "800x600",
|
||||
"window_maximized": True,
|
||||
"last_folder": "/home/user/documents",
|
||||
"sidebar_width": 300,
|
||||
"recent_folders": ["/path1", "/path2"],
|
||||
}
|
||||
|
||||
save_global_config(test_config)
|
||||
|
||||
assert temp_global_config.exists()
|
||||
with open(temp_global_config, "r", encoding="utf-8") as f:
|
||||
saved_data = json.load(f)
|
||||
assert saved_data == test_config
|
||||
|
||||
def test_load_global_config_existing_file(self, temp_global_config):
|
||||
"""Test načtení existující globální konfigurace"""
|
||||
test_config = {
|
||||
"window_geometry": "1920x1080",
|
||||
"window_maximized": False,
|
||||
"last_folder": "/test/path",
|
||||
"sidebar_width": 250,
|
||||
"recent_folders": [],
|
||||
"pool_dir": None,
|
||||
"filmoteka_dir": None,
|
||||
"copyasis_folders": ["Seriály"],
|
||||
}
|
||||
|
||||
save_global_config(test_config)
|
||||
loaded_config = load_global_config()
|
||||
|
||||
assert loaded_config == test_config
|
||||
|
||||
def test_load_global_config_merges_defaults(self, temp_global_config):
|
||||
"""Test že chybějící klíče jsou doplněny z defaultů"""
|
||||
partial_config = {"window_geometry": "800x600"}
|
||||
|
||||
with open(temp_global_config, "w", encoding="utf-8") as f:
|
||||
json.dump(partial_config, f)
|
||||
|
||||
loaded = load_global_config()
|
||||
assert loaded["window_geometry"] == "800x600"
|
||||
assert loaded["window_maximized"] == DEFAULT_GLOBAL_CONFIG["window_maximized"]
|
||||
assert loaded["sidebar_width"] == DEFAULT_GLOBAL_CONFIG["sidebar_width"]
|
||||
|
||||
def test_global_config_corrupted_file(self, temp_global_config):
|
||||
"""Test načtení poškozeného global config souboru"""
|
||||
with open(temp_global_config, "w") as f:
|
||||
f.write("{ invalid json }")
|
||||
|
||||
config = load_global_config()
|
||||
assert config == DEFAULT_GLOBAL_CONFIG
|
||||
|
||||
def test_global_config_utf8_encoding(self, temp_global_config):
|
||||
"""Test UTF-8 encoding s českými znaky"""
|
||||
test_config = {
|
||||
**DEFAULT_GLOBAL_CONFIG,
|
||||
"last_folder": "/cesta/s/českými/znaky",
|
||||
"recent_folders": ["/složka/čeština"],
|
||||
}
|
||||
|
||||
save_global_config(test_config)
|
||||
loaded_config = load_global_config()
|
||||
|
||||
assert loaded_config["last_folder"] == "/cesta/s/českými/znaky"
|
||||
assert loaded_config["recent_folders"] == ["/složka/čeština"]
|
||||
|
||||
def test_global_config_returns_new_dict(self, temp_global_config):
|
||||
"""Test že load_global_config vrací nový dictionary"""
|
||||
config1 = load_global_config()
|
||||
config2 = load_global_config()
|
||||
|
||||
assert config1 is not config2
|
||||
assert config1 == config2
|
||||
|
||||
def test_global_config_recent_folders(self, temp_global_config):
|
||||
"""Test ukládání recent_folders"""
|
||||
folders = ["/path/one", "/path/two", "/path/three"]
|
||||
test_config = {**DEFAULT_GLOBAL_CONFIG, "recent_folders": folders}
|
||||
|
||||
save_global_config(test_config)
|
||||
loaded = load_global_config()
|
||||
|
||||
assert loaded["recent_folders"] == folders
|
||||
assert len(loaded["recent_folders"]) == 3
|
||||
|
||||
|
||||
class TestFolderConfig:
|
||||
"""Testy pro složkový config"""
|
||||
|
||||
def test_default_folder_config_structure(self):
|
||||
"""Test struktury defaultní složkové konfigurace"""
|
||||
assert "ignore_patterns" in DEFAULT_FOLDER_CONFIG
|
||||
assert "custom_tags" in DEFAULT_FOLDER_CONFIG
|
||||
assert "recursive" in DEFAULT_FOLDER_CONFIG
|
||||
assert isinstance(DEFAULT_FOLDER_CONFIG["ignore_patterns"], list)
|
||||
assert isinstance(DEFAULT_FOLDER_CONFIG["custom_tags"], dict)
|
||||
assert DEFAULT_FOLDER_CONFIG["recursive"] is True
|
||||
|
||||
def test_get_folder_config_path(self, tmp_path):
|
||||
"""Test získání cesty ke složkovému configu"""
|
||||
path = get_folder_config_path(tmp_path)
|
||||
assert path == tmp_path / FOLDER_CONFIG_NAME
|
||||
assert path.name == ".Curator.!ftag"
|
||||
|
||||
def test_load_folder_config_nonexistent(self, tmp_path):
|
||||
"""Test načtení neexistujícího složkového configu"""
|
||||
config = load_folder_config(tmp_path)
|
||||
assert config == DEFAULT_FOLDER_CONFIG
|
||||
|
||||
def test_save_folder_config(self, tmp_path):
|
||||
"""Test uložení složkového configu"""
|
||||
test_config = {
|
||||
"ignore_patterns": ["*.tmp", "*.log"],
|
||||
"custom_tags": {"Projekt": ["Web", "API"]},
|
||||
"recursive": False,
|
||||
}
|
||||
|
||||
save_folder_config(tmp_path, test_config)
|
||||
|
||||
config_path = get_folder_config_path(tmp_path)
|
||||
assert config_path.exists()
|
||||
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
saved_data = json.load(f)
|
||||
assert saved_data == test_config
|
||||
|
||||
def test_load_folder_config_existing(self, tmp_path):
|
||||
"""Test načtení existujícího složkového configu"""
|
||||
test_config = {
|
||||
"ignore_patterns": ["*.pyc"],
|
||||
"custom_tags": {},
|
||||
"recursive": True,
|
||||
"hardlink_output_dir": None,
|
||||
"hardlink_categories": None,
|
||||
}
|
||||
|
||||
save_folder_config(tmp_path, test_config)
|
||||
loaded = load_folder_config(tmp_path)
|
||||
|
||||
assert loaded == test_config
|
||||
|
||||
def test_load_folder_config_merges_defaults(self, tmp_path):
|
||||
"""Test že chybějící klíče jsou doplněny z defaultů"""
|
||||
partial_config = {"ignore_patterns": ["*.tmp"]}
|
||||
|
||||
config_path = get_folder_config_path(tmp_path)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(partial_config, f)
|
||||
|
||||
loaded = load_folder_config(tmp_path)
|
||||
assert loaded["ignore_patterns"] == ["*.tmp"]
|
||||
assert loaded["custom_tags"] == DEFAULT_FOLDER_CONFIG["custom_tags"]
|
||||
assert loaded["recursive"] == DEFAULT_FOLDER_CONFIG["recursive"]
|
||||
|
||||
def test_folder_has_config_true(self, tmp_path):
|
||||
"""Test folder_has_config když config existuje"""
|
||||
save_folder_config(tmp_path, DEFAULT_FOLDER_CONFIG)
|
||||
assert folder_has_config(tmp_path) is True
|
||||
|
||||
def test_folder_has_config_false(self, tmp_path):
|
||||
"""Test folder_has_config když config neexistuje"""
|
||||
assert folder_has_config(tmp_path) is False
|
||||
|
||||
def test_folder_config_ignore_patterns(self, tmp_path):
|
||||
"""Test ukládání ignore patterns"""
|
||||
patterns = ["*.tmp", "*.log", "*.cache", "*/node_modules/*", "*.pyc"]
|
||||
test_config = {**DEFAULT_FOLDER_CONFIG, "ignore_patterns": patterns}
|
||||
|
||||
save_folder_config(tmp_path, test_config)
|
||||
loaded = load_folder_config(tmp_path)
|
||||
|
||||
assert loaded["ignore_patterns"] == patterns
|
||||
assert len(loaded["ignore_patterns"]) == 5
|
||||
|
||||
def test_folder_config_custom_tags(self, tmp_path):
|
||||
"""Test ukládání custom tagů"""
|
||||
custom_tags = {
|
||||
"Projekt": ["Frontend", "Backend", "API"],
|
||||
"Stav": ["Hotovo", "Rozpracováno"],
|
||||
}
|
||||
test_config = {**DEFAULT_FOLDER_CONFIG, "custom_tags": custom_tags}
|
||||
|
||||
save_folder_config(tmp_path, test_config)
|
||||
loaded = load_folder_config(tmp_path)
|
||||
|
||||
assert loaded["custom_tags"] == custom_tags
|
||||
|
||||
def test_folder_config_corrupted_file(self, tmp_path):
|
||||
"""Test načtení poškozeného folder config souboru"""
|
||||
config_path = get_folder_config_path(tmp_path)
|
||||
with open(config_path, "w") as f:
|
||||
f.write("{ invalid json }")
|
||||
|
||||
config = load_folder_config(tmp_path)
|
||||
assert config == DEFAULT_FOLDER_CONFIG
|
||||
|
||||
def test_folder_config_utf8_encoding(self, tmp_path):
|
||||
"""Test UTF-8 v folder configu"""
|
||||
test_config = {
|
||||
"ignore_patterns": ["*.čeština"],
|
||||
"custom_tags": {"Štítky": ["Červená", "Žlutá"]},
|
||||
"recursive": True,
|
||||
}
|
||||
|
||||
save_folder_config(tmp_path, test_config)
|
||||
loaded = load_folder_config(tmp_path)
|
||||
|
||||
assert loaded["ignore_patterns"] == ["*.čeština"]
|
||||
assert loaded["custom_tags"]["Štítky"] == ["Červená", "Žlutá"]
|
||||
|
||||
def test_multiple_folders_independent_configs(self, tmp_path):
|
||||
"""Test že různé složky mají nezávislé configy"""
|
||||
folder1 = tmp_path / "folder1"
|
||||
folder2 = tmp_path / "folder2"
|
||||
folder1.mkdir()
|
||||
folder2.mkdir()
|
||||
|
||||
config1 = {**DEFAULT_FOLDER_CONFIG, "ignore_patterns": ["*.txt"]}
|
||||
config2 = {**DEFAULT_FOLDER_CONFIG, "ignore_patterns": ["*.jpg"]}
|
||||
|
||||
save_folder_config(folder1, config1)
|
||||
save_folder_config(folder2, config2)
|
||||
|
||||
loaded1 = load_folder_config(folder1)
|
||||
loaded2 = load_folder_config(folder2)
|
||||
|
||||
assert loaded1["ignore_patterns"] == ["*.txt"]
|
||||
assert loaded2["ignore_patterns"] == ["*.jpg"]
|
||||
|
||||
|
||||
class TestLegacyFunctions:
|
||||
"""Testy pro zpětnou kompatibilitu"""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
"""Fixture pro dočasný globální config soubor"""
|
||||
config_path = tmp_path / "config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
def test_load_config_legacy(self, temp_global_config):
|
||||
"""Test že load_config funguje jako alias pro load_global_config"""
|
||||
test_config = {**DEFAULT_GLOBAL_CONFIG, "last_folder": "/test"}
|
||||
|
||||
save_global_config(test_config)
|
||||
loaded = load_config()
|
||||
|
||||
assert loaded["last_folder"] == "/test"
|
||||
|
||||
def test_save_config_legacy(self, temp_global_config):
|
||||
"""Test že save_config funguje jako alias pro save_global_config"""
|
||||
test_config = {**DEFAULT_GLOBAL_CONFIG, "last_folder": "/legacy"}
|
||||
|
||||
save_config(test_config)
|
||||
loaded = load_global_config()
|
||||
|
||||
assert loaded["last_folder"] == "/legacy"
|
||||
|
||||
|
||||
class TestConfigEdgeCases:
|
||||
"""Testy pro edge cases"""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
"""Fixture pro dočasný globální config soubor"""
|
||||
config_path = tmp_path / "config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
def test_config_path_with_spaces(self, temp_global_config):
|
||||
"""Test s cestou obsahující mezery"""
|
||||
test_config = {
|
||||
**DEFAULT_GLOBAL_CONFIG,
|
||||
"last_folder": "/path/with spaces/in name"
|
||||
}
|
||||
|
||||
save_global_config(test_config)
|
||||
loaded = load_global_config()
|
||||
|
||||
assert loaded["last_folder"] == "/path/with spaces/in name"
|
||||
|
||||
def test_config_long_path(self, temp_global_config):
|
||||
"""Test s dlouhou cestou"""
|
||||
long_path = "/very/long/path/" + "subdir/" * 50 + "final"
|
||||
test_config = {**DEFAULT_GLOBAL_CONFIG, "last_folder": long_path}
|
||||
|
||||
save_global_config(test_config)
|
||||
loaded = load_global_config()
|
||||
|
||||
assert loaded["last_folder"] == long_path
|
||||
|
||||
def test_config_many_recent_folders(self, temp_global_config):
|
||||
"""Test s velkým počtem recent folders"""
|
||||
folders = [f"/path/folder{i}" for i in range(100)]
|
||||
test_config = {**DEFAULT_GLOBAL_CONFIG, "recent_folders": folders}
|
||||
|
||||
save_global_config(test_config)
|
||||
loaded = load_global_config()
|
||||
|
||||
assert len(loaded["recent_folders"]) == 100
|
||||
|
||||
def test_folder_config_special_characters_in_patterns(self, tmp_path):
|
||||
"""Test se speciálními znaky v patterns"""
|
||||
test_config = {
|
||||
**DEFAULT_FOLDER_CONFIG,
|
||||
"ignore_patterns": ["*.tmp", "file[0-9].txt", "test?.log"]
|
||||
}
|
||||
|
||||
save_folder_config(tmp_path, test_config)
|
||||
loaded = load_folder_config(tmp_path)
|
||||
|
||||
assert loaded["ignore_patterns"] == test_config["ignore_patterns"]
|
||||
|
||||
def test_config_json_formatting(self, temp_global_config):
|
||||
"""Test že config je uložen ve správném JSON formátu s indentací"""
|
||||
test_config = {**DEFAULT_GLOBAL_CONFIG}
|
||||
|
||||
save_global_config(test_config)
|
||||
|
||||
with open(temp_global_config, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Mělo by být naformátováno s indentací
|
||||
assert " " in content
|
||||
|
||||
def test_config_ensure_ascii_false(self, temp_global_config):
|
||||
"""Test že ensure_ascii=False funguje správně"""
|
||||
test_config = {
|
||||
**DEFAULT_GLOBAL_CONFIG,
|
||||
"last_folder": "/cesta/čeština"
|
||||
}
|
||||
|
||||
save_global_config(test_config)
|
||||
|
||||
with open(temp_global_config, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
assert "čeština" in content
|
||||
assert "\\u" not in content # Nemělo by být escapováno
|
||||
|
||||
def test_config_overwrite(self, temp_global_config):
|
||||
"""Test přepsání existující konfigurace"""
|
||||
config1 = {**DEFAULT_GLOBAL_CONFIG, "last_folder": "/path1"}
|
||||
config2 = {**DEFAULT_GLOBAL_CONFIG, "last_folder": "/path2"}
|
||||
|
||||
save_global_config(config1)
|
||||
save_global_config(config2)
|
||||
|
||||
loaded = load_global_config()
|
||||
assert loaded["last_folder"] == "/path2"
|
||||
|
||||
def test_folder_config_recursive_false(self, tmp_path):
|
||||
"""Test nastavení recursive na False"""
|
||||
test_config = {**DEFAULT_FOLDER_CONFIG, "recursive": False}
|
||||
|
||||
save_folder_config(tmp_path, test_config)
|
||||
loaded = load_folder_config(tmp_path)
|
||||
|
||||
assert loaded["recursive"] is False
|
||||
|
||||
def test_empty_folder_config(self, tmp_path):
|
||||
"""Test prázdného folder configu"""
|
||||
config_path = get_folder_config_path(tmp_path)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump({}, f)
|
||||
|
||||
loaded = load_folder_config(tmp_path)
|
||||
# Mělo by doplnit všechny defaulty
|
||||
assert loaded["ignore_patterns"] == []
|
||||
assert loaded["custom_tags"] == {}
|
||||
assert loaded["recursive"] is True
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Tests for constants module."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.constants import (
|
||||
APP_NAME,
|
||||
APP_TITLE,
|
||||
APP_VERSION,
|
||||
ENV_DEBUG,
|
||||
get_debug_mode,
|
||||
get_version,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_version()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_version_returns_string() -> None:
|
||||
"""get_version() should return a string."""
|
||||
assert isinstance(get_version(), str)
|
||||
|
||||
|
||||
def test_get_version_semver_format() -> None:
|
||||
"""get_version() should return a semver-like string X.Y.Z."""
|
||||
version = get_version()
|
||||
assert re.match(r"^\d+\.\d+\.\d+", version), f"Not semver: {version!r}"
|
||||
|
||||
|
||||
def test_get_version_fallback_when_toml_missing(tmp_path: Path) -> None:
|
||||
"""get_version() returns '0.0.0-unknown' when pyproject.toml and _version.py are both missing."""
|
||||
missing = tmp_path / "nonexistent.toml"
|
||||
with patch("src.constants._PYPROJECT_PATH", missing):
|
||||
result = get_version()
|
||||
# Either fallback _version.py exists (from previous run) or returns unknown
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_get_version_unknown_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_version() returns '0.0.0-unknown' when all sources are unavailable."""
|
||||
missing = tmp_path / "nonexistent.toml"
|
||||
monkeypatch.setattr("src.constants._PYPROJECT_PATH", missing)
|
||||
|
||||
# Patch _version import to also fail
|
||||
with patch("src.constants.Path.write_text", side_effect=OSError):
|
||||
with patch.dict("sys.modules", {"src._version": None}):
|
||||
result = get_version()
|
||||
|
||||
assert isinstance(result, str)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_debug_mode()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_debug_mode_returns_bool() -> None:
|
||||
"""get_debug_mode() should always return a bool."""
|
||||
assert isinstance(get_debug_mode(), bool)
|
||||
|
||||
|
||||
def test_get_debug_mode_true(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_debug_mode() returns True when ENV_DEBUG=true."""
|
||||
monkeypatch.setenv("ENV_DEBUG", "true")
|
||||
assert get_debug_mode() is True
|
||||
|
||||
|
||||
def test_get_debug_mode_true_variants(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_debug_mode() accepts '1' and 'yes' as truthy values."""
|
||||
for value in ("1", "yes", "YES", "True", "TRUE"):
|
||||
monkeypatch.setenv("ENV_DEBUG", value)
|
||||
assert get_debug_mode() is True, f"Expected True for ENV_DEBUG={value!r}"
|
||||
|
||||
|
||||
def test_get_debug_mode_false(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_debug_mode() returns False when ENV_DEBUG=false."""
|
||||
monkeypatch.setenv("ENV_DEBUG", "false")
|
||||
assert get_debug_mode() is False
|
||||
|
||||
|
||||
def test_get_debug_mode_false_when_unset(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""get_debug_mode() returns False when ENV_DEBUG is not set."""
|
||||
monkeypatch.delenv("ENV_DEBUG", raising=False)
|
||||
assert get_debug_mode() is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module-level constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_env_debug_is_bool() -> None:
|
||||
"""ENV_DEBUG should be a bool."""
|
||||
assert isinstance(ENV_DEBUG, bool)
|
||||
|
||||
|
||||
def test_app_version_is_string() -> None:
|
||||
"""APP_VERSION should be a string."""
|
||||
assert isinstance(APP_VERSION, str)
|
||||
|
||||
|
||||
def test_app_version_semver_format() -> None:
|
||||
"""APP_VERSION should follow semver format X.Y.Z."""
|
||||
assert re.match(r"^\d+\.\d+\.\d+", APP_VERSION), f"Not semver: {APP_VERSION!r}"
|
||||
|
||||
|
||||
def test_app_name_value() -> None:
|
||||
"""APP_NAME should be 'Curator'."""
|
||||
assert APP_NAME == "Curator"
|
||||
|
||||
|
||||
def test_app_title_contains_name_and_version() -> None:
|
||||
"""APP_TITLE should contain APP_NAME and APP_VERSION."""
|
||||
assert APP_NAME in APP_TITLE
|
||||
assert APP_VERSION in APP_TITLE
|
||||
|
||||
|
||||
def test_app_title_dev_suffix_when_debug(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""APP_TITLE ends with '-DEV' when ENV_DEBUG is True."""
|
||||
import src.constants as consts
|
||||
|
||||
monkeypatch.setenv("ENV_DEBUG", "true")
|
||||
monkeypatch.setattr(consts, "ENV_DEBUG", True)
|
||||
title = f"{consts.APP_NAME} v{consts.APP_VERSION}" + ("-DEV" if True else "")
|
||||
assert title.endswith("-DEV")
|
||||
|
||||
|
||||
def test_app_title_no_dev_suffix_when_not_debug(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""APP_TITLE does not end with '-DEV' when ENV_DEBUG is False."""
|
||||
import src.constants as consts
|
||||
|
||||
monkeypatch.setattr(consts, "ENV_DEBUG", False)
|
||||
title = f"{consts.APP_NAME} v{consts.APP_VERSION}" + ("-DEV" if False else "")
|
||||
assert not title.endswith("-DEV")
|
||||
@@ -0,0 +1,286 @@
|
||||
"""Tests for CSFD.cz scraper module."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.core.csfd import (
|
||||
CSFDMovie,
|
||||
fetch_movie,
|
||||
search_movies,
|
||||
fetch_movie_by_id,
|
||||
_extract_csfd_id,
|
||||
_parse_duration,
|
||||
_extract_json_ld,
|
||||
_extract_rating,
|
||||
_extract_poster,
|
||||
_extract_plot,
|
||||
_extract_genres,
|
||||
_extract_origin_info,
|
||||
_check_dependencies,
|
||||
)
|
||||
|
||||
|
||||
# Sample HTML for testing
|
||||
SAMPLE_JSON_LD = """
|
||||
{
|
||||
"@type": "Movie",
|
||||
"name": "Test Movie",
|
||||
"director": [{"@type": "Person", "name": "Test Director"}],
|
||||
"actor": [{"@type": "Person", "name": "Actor 1"}, {"@type": "Person", "name": "Actor 2"}],
|
||||
"aggregateRating": {"ratingValue": 85.5, "ratingCount": 1000},
|
||||
"duration": "PT120M",
|
||||
"description": "A test movie description."
|
||||
}
|
||||
"""
|
||||
|
||||
SAMPLE_HTML = """
|
||||
<html>
|
||||
<head>
|
||||
<script type="application/ld+json">%s</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="film-rating-average">85%%</div>
|
||||
<div class="genres">
|
||||
<a href="/zanry/1/">Drama</a> /
|
||||
<a href="/zanry/2/">Thriller</a>
|
||||
</div>
|
||||
<div class="origin">Česko, 2020, 120 min</div>
|
||||
<div class="film-poster">
|
||||
<img src="//image.example.com/poster.jpg">
|
||||
</div>
|
||||
<div class="plot-full"><p>Full plot description.</p></div>
|
||||
</body>
|
||||
</html>
|
||||
""" % SAMPLE_JSON_LD
|
||||
|
||||
|
||||
class TestCSFDMovie:
|
||||
"""Tests for CSFDMovie dataclass."""
|
||||
|
||||
def test_csfd_movie_basic(self):
|
||||
"""Test basic CSFDMovie creation."""
|
||||
movie = CSFDMovie(title="Test", url="https://csfd.cz/film/123/")
|
||||
assert movie.title == "Test"
|
||||
assert movie.url == "https://csfd.cz/film/123/"
|
||||
assert movie.year is None
|
||||
assert movie.genres == []
|
||||
assert movie.rating is None
|
||||
|
||||
def test_csfd_movie_full(self):
|
||||
"""Test CSFDMovie with all fields."""
|
||||
movie = CSFDMovie(
|
||||
title="Test Movie",
|
||||
url="https://csfd.cz/film/123/",
|
||||
year=2020,
|
||||
genres=["Drama", "Thriller"],
|
||||
directors=["Director 1"],
|
||||
actors=["Actor 1", "Actor 2"],
|
||||
rating=85,
|
||||
rating_count=1000,
|
||||
duration=120,
|
||||
country="Česko",
|
||||
poster_url="https://image.example.com/poster.jpg",
|
||||
plot="A test movie.",
|
||||
csfd_id=123
|
||||
)
|
||||
assert movie.year == 2020
|
||||
assert movie.genres == ["Drama", "Thriller"]
|
||||
assert movie.rating == 85
|
||||
assert movie.duration == 120
|
||||
assert movie.csfd_id == 123
|
||||
|
||||
def test_csfd_movie_str(self):
|
||||
"""Test CSFDMovie string representation."""
|
||||
movie = CSFDMovie(
|
||||
title="Test Movie",
|
||||
url="https://csfd.cz/film/123/",
|
||||
year=2020,
|
||||
genres=["Drama"],
|
||||
directors=["Director 1"],
|
||||
rating=85
|
||||
)
|
||||
s = str(movie)
|
||||
assert "Test Movie (2020)" in s
|
||||
assert "85%" in s
|
||||
assert "Drama" in s
|
||||
assert "Director 1" in s
|
||||
|
||||
def test_csfd_movie_str_minimal(self):
|
||||
"""Test CSFDMovie string with minimal data."""
|
||||
movie = CSFDMovie(title="Test", url="https://csfd.cz/film/123/")
|
||||
s = str(movie)
|
||||
assert "Test" in s
|
||||
|
||||
|
||||
class TestHelperFunctions:
|
||||
"""Tests for helper functions."""
|
||||
|
||||
def test_extract_csfd_id_valid(self):
|
||||
"""Test extracting CSFD ID from valid URL."""
|
||||
assert _extract_csfd_id("https://www.csfd.cz/film/9423-pane-vy-jste-vdova/") == 9423
|
||||
assert _extract_csfd_id("https://www.csfd.cz/film/123456/") == 123456
|
||||
assert _extract_csfd_id("/film/999/prehled/") == 999
|
||||
|
||||
def test_extract_csfd_id_invalid(self):
|
||||
"""Test extracting CSFD ID from invalid URL."""
|
||||
assert _extract_csfd_id("https://www.csfd.cz/") is None
|
||||
assert _extract_csfd_id("not-a-url") is None
|
||||
|
||||
def test_parse_duration_valid(self):
|
||||
"""Test parsing ISO 8601 duration."""
|
||||
assert _parse_duration("PT97M") == 97
|
||||
assert _parse_duration("PT120M") == 120
|
||||
assert _parse_duration("PT60M") == 60
|
||||
|
||||
def test_parse_duration_invalid(self):
|
||||
"""Test parsing invalid duration."""
|
||||
assert _parse_duration("") is None
|
||||
assert _parse_duration("invalid") is None
|
||||
assert _parse_duration("PT") is None
|
||||
|
||||
|
||||
class TestHTMLExtraction:
|
||||
"""Tests for HTML extraction functions."""
|
||||
|
||||
@pytest.fixture
|
||||
def soup(self):
|
||||
"""Create BeautifulSoup object from sample HTML."""
|
||||
from bs4 import BeautifulSoup
|
||||
return BeautifulSoup(SAMPLE_HTML, "html.parser")
|
||||
|
||||
def test_extract_json_ld(self, soup):
|
||||
"""Test extracting data from JSON-LD."""
|
||||
data = _extract_json_ld(soup)
|
||||
assert data["title"] == "Test Movie"
|
||||
assert data["directors"] == ["Test Director"]
|
||||
assert data["actors"] == ["Actor 1", "Actor 2"]
|
||||
assert data["rating"] == 86 # Rounded from 85.5
|
||||
assert data["rating_count"] == 1000
|
||||
assert data["duration"] == 120
|
||||
|
||||
def test_extract_rating(self, soup):
|
||||
"""Test extracting rating from HTML."""
|
||||
rating = _extract_rating(soup)
|
||||
assert rating == 85
|
||||
|
||||
def test_extract_genres(self, soup):
|
||||
"""Test extracting genres from HTML."""
|
||||
genres = _extract_genres(soup)
|
||||
assert "Drama" in genres
|
||||
assert "Thriller" in genres
|
||||
|
||||
def test_extract_poster(self, soup):
|
||||
"""Test extracting poster URL."""
|
||||
poster = _extract_poster(soup)
|
||||
assert poster == "https://image.example.com/poster.jpg"
|
||||
|
||||
def test_extract_plot(self, soup):
|
||||
"""Test extracting plot."""
|
||||
plot = _extract_plot(soup)
|
||||
assert plot == "Full plot description."
|
||||
|
||||
def test_extract_origin_info(self, soup):
|
||||
"""Test extracting origin info (comma-separated legacy format)."""
|
||||
info = _extract_origin_info(soup)
|
||||
assert info["country"] == "Česko"
|
||||
assert info["year"] == 2020
|
||||
assert info["duration"] == 120
|
||||
|
||||
def test_extract_origin_info_bullet_format(self):
|
||||
"""Test current CSFD format with inline bullet spans (no commas)."""
|
||||
from bs4 import BeautifulSoup
|
||||
html = (
|
||||
'<div class="origin">USA <span class="bullet"></span>'
|
||||
'<span>1999 <span class="bullet"></span> </span>'
|
||||
'136 min (Alternativní 131 min)</div>'
|
||||
)
|
||||
info = _extract_origin_info(BeautifulSoup(html, "html.parser"))
|
||||
assert info["country"] == "USA"
|
||||
assert info["year"] == 1999
|
||||
assert info["duration"] == 136
|
||||
|
||||
def test_extract_json_ld_year_from_date_created(self):
|
||||
"""Year is taken from JSON-LD dateCreated when present."""
|
||||
from bs4 import BeautifulSoup
|
||||
html = (
|
||||
'<script type="application/ld+json">'
|
||||
'{"@type": "Movie", "name": "Matrix", "dateCreated": 1999}'
|
||||
'</script>'
|
||||
)
|
||||
data = _extract_json_ld(BeautifulSoup(html, "html.parser"))
|
||||
assert data["year"] == 1999
|
||||
|
||||
|
||||
class TestFetchMovie:
|
||||
"""Tests for fetch_movie function."""
|
||||
|
||||
@patch("src.core.csfd.requests")
|
||||
def test_fetch_movie_success(self, mock_requests):
|
||||
"""Test successful movie fetch."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.text = SAMPLE_HTML
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_requests.get.return_value = mock_response
|
||||
|
||||
movie = fetch_movie("https://www.csfd.cz/film/123-test/")
|
||||
|
||||
assert movie.title == "Test Movie"
|
||||
assert movie.csfd_id == 123
|
||||
assert movie.rating == 86
|
||||
assert "Drama" in movie.genres
|
||||
mock_requests.get.assert_called_once()
|
||||
|
||||
@patch("src.core.csfd.requests")
|
||||
def test_fetch_movie_network_error(self, mock_requests):
|
||||
"""Test network error handling."""
|
||||
import requests as real_requests
|
||||
mock_requests.get.side_effect = real_requests.RequestException("Network error")
|
||||
|
||||
with pytest.raises(real_requests.RequestException):
|
||||
fetch_movie("https://www.csfd.cz/film/123/")
|
||||
|
||||
|
||||
class TestSearchMovies:
|
||||
"""Tests for search_movies function."""
|
||||
|
||||
@patch("src.core.csfd.requests")
|
||||
def test_search_movies(self, mock_requests):
|
||||
"""Test movie search."""
|
||||
search_html = """
|
||||
<html><body>
|
||||
<a href="/film/123-test/" class="film-title-name">Test Movie</a>
|
||||
<a href="/film/456-another/" class="film-title-name">Another Movie</a>
|
||||
</body></html>
|
||||
"""
|
||||
mock_response = MagicMock()
|
||||
mock_response.text = search_html
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_requests.get.return_value = mock_response
|
||||
mock_requests.utils.quote = lambda x: x
|
||||
|
||||
results = search_movies("test", limit=10)
|
||||
|
||||
assert len(results) >= 1
|
||||
assert any(m.csfd_id == 123 for m in results)
|
||||
|
||||
|
||||
class TestFetchMovieById:
|
||||
"""Tests for fetch_movie_by_id function."""
|
||||
|
||||
@patch("src.core.csfd.fetch_movie")
|
||||
def test_fetch_by_id(self, mock_fetch):
|
||||
"""Test fetching movie by ID."""
|
||||
mock_fetch.return_value = CSFDMovie(title="Test", url="https://csfd.cz/film/9423/")
|
||||
|
||||
movie = fetch_movie_by_id(9423)
|
||||
|
||||
mock_fetch.assert_called_once_with("https://www.csfd.cz/film/9423/")
|
||||
assert movie.title == "Test"
|
||||
|
||||
|
||||
class TestDependencyCheck:
|
||||
"""Tests for dependency checking."""
|
||||
|
||||
def test_dependencies_available(self):
|
||||
"""Test that dependencies are available (they should be in test env)."""
|
||||
# Should not raise
|
||||
_check_dependencies()
|
||||
@@ -0,0 +1,265 @@
|
||||
import pytest
|
||||
import json
|
||||
from pathlib import Path
|
||||
from src.core.file import File
|
||||
from src.core.tag import Tag
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
|
||||
class TestFile:
|
||||
"""Testy pro třídu File"""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self, tmp_path):
|
||||
"""Fixture pro dočasný adresář"""
|
||||
return tmp_path
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
"""Fixture pro TagManager"""
|
||||
return TagManager()
|
||||
|
||||
@pytest.fixture
|
||||
def test_file(self, temp_dir):
|
||||
"""Fixture pro testovací soubor"""
|
||||
test_file = temp_dir / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
return test_file
|
||||
|
||||
def test_file_creation(self, test_file, tag_manager):
|
||||
"""Test vytvoření File objektu"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
assert file_obj.file_path == test_file
|
||||
assert file_obj.filename == "test.txt"
|
||||
assert file_obj.new == True
|
||||
|
||||
def test_file_metadata_filename(self, test_file, tag_manager):
|
||||
"""Test názvu metadata souboru"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
expected = test_file.parent / ".test.txt.!tag"
|
||||
assert file_obj.metadata_filename == expected
|
||||
|
||||
def test_file_initial_tags(self, test_file, tag_manager):
|
||||
"""Test že nový soubor má tag Stav/Nové"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
assert len(file_obj.tags) == 1
|
||||
assert file_obj.tags[0].full_path == "Stav/Nové"
|
||||
|
||||
def test_file_metadata_saved(self, test_file, tag_manager):
|
||||
"""Test že metadata jsou uložena při vytvoření"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
assert file_obj.metadata_filename.exists()
|
||||
|
||||
def test_file_save_metadata(self, test_file, tag_manager):
|
||||
"""Test uložení metadat"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.new = False
|
||||
file_obj.ignored = True
|
||||
file_obj.save_metadata()
|
||||
|
||||
# Načtení a kontrola
|
||||
with open(file_obj.metadata_filename, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert data["new"] == False
|
||||
assert data["ignored"] == True
|
||||
|
||||
def test_file_load_metadata(self, test_file, tag_manager):
|
||||
"""Test načtení metadat"""
|
||||
# Vytvoření a uložení metadat
|
||||
file_obj = File(test_file, tag_manager)
|
||||
tag = tag_manager.add_tag("Video", "HD")
|
||||
file_obj.tags.append(tag)
|
||||
file_obj.date = "2025-01-15"
|
||||
file_obj.save_metadata()
|
||||
|
||||
# Vytvoření nového objektu - měl by načíst metadata
|
||||
file_obj2 = File(test_file, tag_manager)
|
||||
assert len(file_obj2.tags) == 2 # Stav/Nové + Video/HD
|
||||
assert file_obj2.date == "2025-01-15"
|
||||
|
||||
# Kontrola že tagy obsahují správné hodnoty
|
||||
tag_paths = {tag.full_path for tag in file_obj2.tags}
|
||||
assert "Video/HD" in tag_paths
|
||||
assert "Stav/Nové" in tag_paths
|
||||
|
||||
def test_file_set_date(self, test_file, tag_manager):
|
||||
"""Test nastavení data"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.set_date("2025-12-25")
|
||||
assert file_obj.date == "2025-12-25"
|
||||
|
||||
# Kontrola že bylo uloženo
|
||||
with open(file_obj.metadata_filename, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
assert data["date"] == "2025-12-25"
|
||||
|
||||
def test_file_set_date_to_none(self, test_file, tag_manager):
|
||||
"""Test smazání data"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.set_date("2025-12-25")
|
||||
file_obj.set_date(None)
|
||||
assert file_obj.date is None
|
||||
|
||||
def test_file_set_date_empty_string(self, test_file, tag_manager):
|
||||
"""Test nastavení prázdného řetězce jako datum"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.set_date("2025-12-25")
|
||||
file_obj.set_date("")
|
||||
assert file_obj.date is None
|
||||
|
||||
def test_file_add_tag_object(self, test_file, tag_manager):
|
||||
"""Test přidání Tag objektu"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
tag = Tag("Video", "4K")
|
||||
file_obj.add_tag(tag)
|
||||
|
||||
assert tag in file_obj.tags
|
||||
assert len(file_obj.tags) == 2 # Stav/Nové + Video/4K
|
||||
|
||||
def test_file_add_tag_string(self, test_file, tag_manager):
|
||||
"""Test přidání tagu jako string"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.add_tag("Audio/MP3")
|
||||
|
||||
tag_paths = {tag.full_path for tag in file_obj.tags}
|
||||
assert "Audio/MP3" in tag_paths
|
||||
|
||||
def test_file_add_tag_string_without_category(self, test_file, tag_manager):
|
||||
"""Test přidání tagu bez kategorie (použije 'default')"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.add_tag("SimpleTag")
|
||||
|
||||
tag_paths = {tag.full_path for tag in file_obj.tags}
|
||||
assert "default/SimpleTag" in tag_paths
|
||||
|
||||
def test_file_add_duplicate_tag(self, test_file, tag_manager):
|
||||
"""Test že duplicitní tag není přidán"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
tag = Tag("Video", "HD")
|
||||
file_obj.add_tag(tag)
|
||||
file_obj.add_tag(tag)
|
||||
|
||||
# Spočítáme kolikrát se tag vyskytuje
|
||||
count = sum(1 for t in file_obj.tags if t == tag)
|
||||
assert count == 1
|
||||
|
||||
def test_file_remove_tag_object(self, test_file, tag_manager):
|
||||
"""Test odstranění Tag objektu"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
tag = Tag("Video", "HD")
|
||||
file_obj.add_tag(tag)
|
||||
file_obj.remove_tag(tag)
|
||||
|
||||
assert tag not in file_obj.tags
|
||||
|
||||
def test_file_remove_tag_string(self, test_file, tag_manager):
|
||||
"""Test odstranění tagu jako string"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.add_tag("Video/HD")
|
||||
file_obj.remove_tag("Video/HD")
|
||||
|
||||
tag_paths = {tag.full_path for tag in file_obj.tags}
|
||||
assert "Video/HD" not in tag_paths
|
||||
|
||||
def test_file_remove_tag_string_without_category(self, test_file, tag_manager):
|
||||
"""Test odstranění tagu bez kategorie"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.add_tag("SimpleTag")
|
||||
file_obj.remove_tag("SimpleTag")
|
||||
|
||||
tag_paths = {tag.full_path for tag in file_obj.tags}
|
||||
assert "default/SimpleTag" not in tag_paths
|
||||
|
||||
def test_file_remove_nonexistent_tag(self, test_file, tag_manager):
|
||||
"""Test odstranění neexistujícího tagu (nemělo by vyhodit výjimku)"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
initial_count = len(file_obj.tags)
|
||||
file_obj.remove_tag("Nonexistent/Tag")
|
||||
assert len(file_obj.tags) == initial_count
|
||||
|
||||
def test_file_without_tagmanager(self, test_file):
|
||||
"""Test File bez TagManager"""
|
||||
file_obj = File(test_file, tagmanager=None)
|
||||
assert file_obj.tagmanager is None
|
||||
assert len(file_obj.tags) == 0 # Bez TagManager se nepřidá Stav/Nové
|
||||
|
||||
def test_file_metadata_persistence(self, test_file, tag_manager):
|
||||
"""Test že metadata přežijí reload"""
|
||||
# Vytvoření a úprava souboru
|
||||
file_obj1 = File(test_file, tag_manager)
|
||||
file_obj1.add_tag("Video/HD")
|
||||
file_obj1.add_tag("Audio/Stereo")
|
||||
file_obj1.set_date("2025-01-01")
|
||||
file_obj1.new = False
|
||||
file_obj1.ignored = True
|
||||
file_obj1.save_metadata()
|
||||
|
||||
# Načtení nového objektu
|
||||
file_obj2 = File(test_file, tag_manager)
|
||||
|
||||
# Kontrola
|
||||
assert file_obj2.new == False
|
||||
assert file_obj2.ignored == True
|
||||
assert file_obj2.date == "2025-01-01"
|
||||
|
||||
tag_paths = {tag.full_path for tag in file_obj2.tags}
|
||||
assert "Video/HD" in tag_paths
|
||||
assert "Audio/Stereo" in tag_paths
|
||||
|
||||
def test_file_metadata_json_format(self, test_file, tag_manager):
|
||||
"""Test formátu JSON metadat"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.add_tag("Test/Tag")
|
||||
file_obj.set_date("2025-06-15")
|
||||
|
||||
# Kontrola obsahu JSON
|
||||
with open(file_obj.metadata_filename, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert "new" in data
|
||||
assert "ignored" in data
|
||||
assert "tags" in data
|
||||
assert "date" in data
|
||||
assert isinstance(data["tags"], list)
|
||||
|
||||
def test_file_unicode_handling(self, temp_dir, tag_manager):
|
||||
"""Test správného zacházení s unicode znaky"""
|
||||
test_file = temp_dir / "český_soubor.txt"
|
||||
test_file.write_text("obsah")
|
||||
|
||||
file_obj = File(test_file, tag_manager)
|
||||
file_obj.add_tag("Kategorie/Český tag")
|
||||
file_obj.save_metadata()
|
||||
|
||||
# Reload a kontrola
|
||||
file_obj2 = File(test_file, tag_manager)
|
||||
tag_paths = {tag.full_path for tag in file_obj2.tags}
|
||||
assert "Kategorie/Český tag" in tag_paths
|
||||
|
||||
def test_file_complex_scenario(self, test_file, tag_manager):
|
||||
"""Test komplexního scénáře použití"""
|
||||
file_obj = File(test_file, tag_manager)
|
||||
|
||||
# Přidání více tagů
|
||||
file_obj.add_tag("Video/HD")
|
||||
file_obj.add_tag("Video/Stereo")
|
||||
file_obj.add_tag("Stav/Zkontrolováno")
|
||||
file_obj.set_date("2025-01-01")
|
||||
|
||||
# Odstranění tagu
|
||||
file_obj.remove_tag("Stav/Nové")
|
||||
|
||||
# Kontrola stavu
|
||||
tag_paths = {tag.full_path for tag in file_obj.tags}
|
||||
assert "Video/HD" in tag_paths
|
||||
assert "Video/Stereo" in tag_paths
|
||||
assert "Stav/Zkontrolováno" in tag_paths
|
||||
assert "Stav/Nové" not in tag_paths
|
||||
assert file_obj.date == "2025-01-01"
|
||||
|
||||
# Reload a kontrola persistence
|
||||
file_obj2 = File(test_file, tag_manager)
|
||||
tag_paths2 = {tag.full_path for tag in file_obj2.tags}
|
||||
assert tag_paths == tag_paths2
|
||||
assert file_obj2.date == "2025-01-01"
|
||||
@@ -0,0 +1,612 @@
|
||||
import pytest
|
||||
from src.core.file_manager import FileManager
|
||||
from src.core.tag_manager import TagManager
|
||||
from src.core.tag import Tag
|
||||
|
||||
|
||||
class TestFileManager:
|
||||
"""Testy pro třídu FileManager"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
"""Fixture pro TagManager"""
|
||||
return TagManager()
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, tag_manager, temp_global_config):
|
||||
"""Fixture pro FileManager"""
|
||||
return FileManager(tag_manager)
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self, tmp_path):
|
||||
"""Fixture pro dočasný adresář s testovacími soubory"""
|
||||
# Vytvoření struktury souborů
|
||||
(tmp_path / "file1.txt").write_text("content1")
|
||||
(tmp_path / "file2.txt").write_text("content2")
|
||||
(tmp_path / "file3.jpg").write_text("image")
|
||||
|
||||
# Podsložka
|
||||
subdir = tmp_path / "subdir"
|
||||
subdir.mkdir()
|
||||
(subdir / "file4.txt").write_text("content4")
|
||||
|
||||
return tmp_path
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
"""Fixture pro dočasný global config soubor"""
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
def test_file_manager_creation(self, file_manager, tag_manager):
|
||||
"""Test vytvoření FileManager"""
|
||||
assert file_manager.filelist == []
|
||||
assert file_manager.folders == []
|
||||
assert file_manager.tagmanager == tag_manager
|
||||
assert file_manager.global_config is not None
|
||||
assert file_manager.folder_configs == {}
|
||||
assert file_manager.current_folder is None
|
||||
|
||||
def test_file_manager_append_folder(self, file_manager, temp_dir):
|
||||
"""Test přidání složky"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
assert temp_dir in file_manager.folders
|
||||
assert len(file_manager.filelist) > 0
|
||||
assert file_manager.current_folder == temp_dir
|
||||
|
||||
def test_file_manager_append_folder_finds_all_files(self, file_manager, temp_dir):
|
||||
"""Test že append najde všechny soubory včetně podsložek"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
# Měli bychom najít file1.txt, file2.txt, file3.jpg, subdir/file4.txt
|
||||
# (ne .!tag soubory)
|
||||
filenames = {f.filename for f in file_manager.filelist}
|
||||
assert "file1.txt" in filenames
|
||||
assert "file2.txt" in filenames
|
||||
assert "file3.jpg" in filenames
|
||||
assert "file4.txt" in filenames
|
||||
|
||||
def test_file_manager_ignores_tag_files(self, file_manager, temp_dir):
|
||||
"""Test že .!tag soubory jsou ignorovány"""
|
||||
# Vytvoření .!tag souboru
|
||||
(temp_dir / ".file1.txt.!tag").write_text('{"tags": []}')
|
||||
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
filenames = {f.filename for f in file_manager.filelist}
|
||||
assert ".file1.txt.!tag" not in filenames
|
||||
|
||||
def test_file_manager_ignores_curator_config_files(self, file_manager, temp_dir):
|
||||
"""Test že Curator config soubory jsou ignorovány"""
|
||||
(temp_dir / ".Curator.!ftag").write_text('{}') # Folder config
|
||||
(temp_dir / ".Curator.!gtag").write_text('{}') # Global config
|
||||
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
filenames = {f.filename for f in file_manager.filelist}
|
||||
assert ".Curator.!ftag" not in filenames
|
||||
assert ".Curator.!gtag" not in filenames
|
||||
|
||||
|
||||
def test_file_manager_updates_last_folder(self, file_manager, temp_dir):
|
||||
"""Test aktualizace last_folder v global configu"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
assert file_manager.global_config["last_folder"] == str(temp_dir)
|
||||
|
||||
def test_file_manager_updates_recent_folders(self, file_manager, temp_dir):
|
||||
"""Test aktualizace recent_folders"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
assert str(temp_dir) in file_manager.global_config["recent_folders"]
|
||||
assert file_manager.global_config["recent_folders"][0] == str(temp_dir)
|
||||
|
||||
def test_file_manager_recent_folders_max_10(self, file_manager, tmp_path):
|
||||
"""Test že recent_folders má max 10 položek"""
|
||||
for i in range(15):
|
||||
folder = tmp_path / f"folder{i}"
|
||||
folder.mkdir()
|
||||
(folder / "file.txt").write_text("content")
|
||||
file_manager.append(folder)
|
||||
|
||||
assert len(file_manager.global_config["recent_folders"]) <= 10
|
||||
|
||||
def test_file_manager_loads_folder_config(self, file_manager, temp_dir):
|
||||
"""Test že se načte folder config při append"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
assert temp_dir in file_manager.folder_configs
|
||||
assert "ignore_patterns" in file_manager.folder_configs[temp_dir]
|
||||
|
||||
|
||||
class TestFileManagerIgnorePatterns:
|
||||
"""Testy pro ignore patterns"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
return TagManager()
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, tag_manager, temp_global_config):
|
||||
return FileManager(tag_manager)
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self, tmp_path):
|
||||
(tmp_path / "file1.txt").write_text("content1")
|
||||
(tmp_path / "file2.txt").write_text("content2")
|
||||
(tmp_path / "file3.jpg").write_text("image")
|
||||
subdir = tmp_path / "subdir"
|
||||
subdir.mkdir()
|
||||
(subdir / "file4.txt").write_text("content4")
|
||||
return tmp_path
|
||||
|
||||
def test_ignore_patterns_by_extension(self, file_manager, temp_dir):
|
||||
"""Test ignorování souborů podle přípony"""
|
||||
from src.core.config import save_folder_config
|
||||
save_folder_config(temp_dir, {"ignore_patterns": ["*.jpg"], "custom_tags": {}, "recursive": True})
|
||||
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
filenames = {f.filename for f in file_manager.filelist}
|
||||
assert "file3.jpg" not in filenames
|
||||
assert "file1.txt" in filenames
|
||||
|
||||
def test_ignore_patterns_path(self, file_manager, temp_dir):
|
||||
"""Test ignorování podle celé cesty"""
|
||||
from src.core.config import save_folder_config
|
||||
save_folder_config(temp_dir, {"ignore_patterns": ["*/subdir/*"], "custom_tags": {}, "recursive": True})
|
||||
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
filenames = {f.filename for f in file_manager.filelist}
|
||||
assert "file4.txt" not in filenames
|
||||
assert "file1.txt" in filenames
|
||||
|
||||
def test_multiple_ignore_patterns(self, file_manager, temp_dir):
|
||||
"""Test více ignore patternů najednou"""
|
||||
from src.core.config import save_folder_config
|
||||
save_folder_config(temp_dir, {"ignore_patterns": ["*.jpg", "*/subdir/*"], "custom_tags": {}, "recursive": True})
|
||||
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
filenames = {f.filename for f in file_manager.filelist}
|
||||
assert "file3.jpg" not in filenames
|
||||
assert "file4.txt" not in filenames
|
||||
assert "file1.txt" in filenames
|
||||
assert "file2.txt" in filenames
|
||||
|
||||
def test_set_ignore_patterns(self, file_manager, temp_dir):
|
||||
"""Test nastavení ignore patterns přes metodu"""
|
||||
file_manager.append(temp_dir)
|
||||
file_manager.set_ignore_patterns(["*.tmp", "*.log"])
|
||||
|
||||
patterns = file_manager.get_ignore_patterns()
|
||||
assert patterns == ["*.tmp", "*.log"]
|
||||
|
||||
def test_get_ignore_patterns_empty(self, file_manager, temp_dir):
|
||||
"""Test získání prázdných ignore patterns"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
patterns = file_manager.get_ignore_patterns()
|
||||
assert patterns == []
|
||||
|
||||
|
||||
class TestFileManagerFolderConfig:
|
||||
"""Testy pro folder config management"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
return TagManager()
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, tag_manager, temp_global_config):
|
||||
return FileManager(tag_manager)
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self, tmp_path):
|
||||
(tmp_path / "file1.txt").write_text("content")
|
||||
return tmp_path
|
||||
|
||||
def test_get_folder_config_current(self, file_manager, temp_dir):
|
||||
"""Test získání configu pro aktuální složku"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
config = file_manager.get_folder_config()
|
||||
assert "ignore_patterns" in config
|
||||
|
||||
def test_get_folder_config_specific(self, file_manager, temp_dir, tmp_path):
|
||||
"""Test získání configu pro specifickou složku"""
|
||||
folder2 = tmp_path / "folder2"
|
||||
folder2.mkdir()
|
||||
(folder2 / "file.txt").write_text("content")
|
||||
|
||||
file_manager.append(temp_dir)
|
||||
file_manager.append(folder2)
|
||||
|
||||
config = file_manager.get_folder_config(temp_dir)
|
||||
assert config is not None
|
||||
|
||||
def test_get_folder_config_no_current(self, file_manager):
|
||||
"""Test získání configu když není current folder"""
|
||||
config = file_manager.get_folder_config()
|
||||
assert config == {}
|
||||
|
||||
def test_save_folder_config(self, file_manager, temp_dir):
|
||||
"""Test uložení folder configu"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
new_config = {"ignore_patterns": ["*.test"], "custom_tags": {}, "recursive": False}
|
||||
file_manager.save_folder_config(config=new_config)
|
||||
|
||||
loaded = file_manager.get_folder_config()
|
||||
assert loaded["ignore_patterns"] == ["*.test"]
|
||||
assert loaded["recursive"] is False
|
||||
|
||||
|
||||
class TestFileManagerTagOperations:
|
||||
"""Testy pro operace s tagy"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
return TagManager()
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, tag_manager, temp_global_config):
|
||||
return FileManager(tag_manager)
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self, tmp_path):
|
||||
(tmp_path / "file1.txt").write_text("content1")
|
||||
(tmp_path / "file2.txt").write_text("content2")
|
||||
(tmp_path / "file3.txt").write_text("content3")
|
||||
return tmp_path
|
||||
|
||||
def test_assign_tag_to_file_objects_tag_object(self, file_manager, temp_dir):
|
||||
"""Test přiřazení Tag objektu k souborům"""
|
||||
file_manager.append(temp_dir)
|
||||
files = file_manager.filelist[:2]
|
||||
tag = Tag("Video", "HD")
|
||||
|
||||
file_manager.assign_tag_to_file_objects(files, tag)
|
||||
|
||||
for f in files:
|
||||
assert tag in f.tags
|
||||
|
||||
def test_assign_tag_string_with_category(self, file_manager, temp_dir):
|
||||
"""Test přiřazení tagu jako string s kategorií"""
|
||||
file_manager.append(temp_dir)
|
||||
files = file_manager.filelist[:1]
|
||||
|
||||
file_manager.assign_tag_to_file_objects(files, "Video/4K")
|
||||
|
||||
tag_paths = {tag.full_path for tag in files[0].tags}
|
||||
assert "Video/4K" in tag_paths
|
||||
|
||||
def test_assign_tag_string_without_category(self, file_manager, temp_dir):
|
||||
"""Test přiřazení tagu bez kategorie (default)"""
|
||||
file_manager.append(temp_dir)
|
||||
files = file_manager.filelist[:1]
|
||||
|
||||
file_manager.assign_tag_to_file_objects(files, "SimpleTag")
|
||||
|
||||
tag_paths = {tag.full_path for tag in files[0].tags}
|
||||
assert "default/SimpleTag" in tag_paths
|
||||
|
||||
def test_assign_tag_no_duplicate(self, file_manager, temp_dir):
|
||||
"""Test že tag není přidán dvakrát"""
|
||||
file_manager.append(temp_dir)
|
||||
files = file_manager.filelist[:1]
|
||||
tag = Tag("Video", "HD")
|
||||
|
||||
file_manager.assign_tag_to_file_objects(files, tag)
|
||||
file_manager.assign_tag_to_file_objects(files, tag)
|
||||
|
||||
count = sum(1 for t in files[0].tags if t == tag)
|
||||
assert count == 1
|
||||
|
||||
def test_remove_tag_from_file_objects(self, file_manager, temp_dir):
|
||||
"""Test odstranění tagu ze souborů"""
|
||||
file_manager.append(temp_dir)
|
||||
files = file_manager.filelist[:2]
|
||||
tag = Tag("Video", "HD")
|
||||
|
||||
file_manager.assign_tag_to_file_objects(files, tag)
|
||||
file_manager.remove_tag_from_file_objects(files, tag)
|
||||
|
||||
for f in files:
|
||||
assert tag not in f.tags
|
||||
|
||||
def test_remove_tag_string(self, file_manager, temp_dir):
|
||||
"""Test odstranění tagu jako string"""
|
||||
file_manager.append(temp_dir)
|
||||
files = file_manager.filelist[:1]
|
||||
|
||||
file_manager.assign_tag_to_file_objects(files, "Video/HD")
|
||||
file_manager.remove_tag_from_file_objects(files, "Video/HD")
|
||||
|
||||
tag_paths = {tag.full_path for tag in files[0].tags}
|
||||
assert "Video/HD" not in tag_paths
|
||||
|
||||
def test_callback_on_tag_change(self, file_manager, temp_dir):
|
||||
"""Test callback při změně tagů"""
|
||||
file_manager.append(temp_dir)
|
||||
callback_calls = []
|
||||
|
||||
def callback(filelist):
|
||||
callback_calls.append(len(filelist))
|
||||
|
||||
file_manager.on_files_changed = callback
|
||||
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], Tag("Test", "Tag"))
|
||||
|
||||
assert len(callback_calls) == 1
|
||||
|
||||
|
||||
class TestFileManagerFiltering:
|
||||
"""Testy pro filtrování souborů"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
return TagManager()
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, tag_manager, temp_global_config):
|
||||
return FileManager(tag_manager)
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self, tmp_path):
|
||||
(tmp_path / "file1.txt").write_text("content1")
|
||||
(tmp_path / "file2.txt").write_text("content2")
|
||||
(tmp_path / "file3.txt").write_text("content3")
|
||||
return tmp_path
|
||||
|
||||
def test_filter_empty_tags_returns_all(self, file_manager, temp_dir):
|
||||
"""Test filtrace bez tagů vrací všechny soubory"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
filtered = file_manager.filter_files_by_tags([])
|
||||
assert len(filtered) == len(file_manager.filelist)
|
||||
|
||||
def test_filter_none_returns_all(self, file_manager, temp_dir):
|
||||
"""Test filtrace s None vrací všechny soubory"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
filtered = file_manager.filter_files_by_tags(None)
|
||||
assert len(filtered) == len(file_manager.filelist)
|
||||
|
||||
def test_filter_by_single_tag(self, file_manager, temp_dir):
|
||||
"""Test filtrace podle jednoho tagu"""
|
||||
file_manager.append(temp_dir)
|
||||
tag = Tag("Video", "HD")
|
||||
files_to_tag = file_manager.filelist[:2]
|
||||
file_manager.assign_tag_to_file_objects(files_to_tag, tag)
|
||||
|
||||
filtered = file_manager.filter_files_by_tags([tag])
|
||||
assert len(filtered) == 2
|
||||
for f in filtered:
|
||||
assert tag in f.tags
|
||||
|
||||
def test_filter_by_multiple_tags_and_logic(self, file_manager, temp_dir):
|
||||
"""Test filtrace podle více tagů (AND logika)"""
|
||||
file_manager.append(temp_dir)
|
||||
tag1 = Tag("Video", "HD")
|
||||
tag2 = Tag("Audio", "Stereo")
|
||||
|
||||
# První soubor má oba tagy
|
||||
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], tag1)
|
||||
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], tag2)
|
||||
|
||||
# Druhý soubor má jen první tag
|
||||
file_manager.assign_tag_to_file_objects([file_manager.filelist[1]], tag1)
|
||||
|
||||
filtered = file_manager.filter_files_by_tags([tag1, tag2])
|
||||
assert len(filtered) == 1
|
||||
assert filtered[0] == file_manager.filelist[0]
|
||||
|
||||
def test_filter_by_tag_strings(self, file_manager, temp_dir):
|
||||
"""Test filtrace podle tagů jako stringy"""
|
||||
file_manager.append(temp_dir)
|
||||
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], "Video/HD")
|
||||
|
||||
filtered = file_manager.filter_files_by_tags(["Video/HD"])
|
||||
assert len(filtered) == 1
|
||||
|
||||
def test_filter_no_match(self, file_manager, temp_dir):
|
||||
"""Test filtrace když nic neodpovídá"""
|
||||
file_manager.append(temp_dir)
|
||||
|
||||
filtered = file_manager.filter_files_by_tags([Tag("NonExistent", "Tag")])
|
||||
assert len(filtered) == 0
|
||||
|
||||
|
||||
class TestFileManagerLegacy:
|
||||
"""Testy pro zpětnou kompatibilitu"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
return TagManager()
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, tag_manager, temp_global_config):
|
||||
return FileManager(tag_manager)
|
||||
|
||||
def test_config_property_returns_global(self, file_manager):
|
||||
"""Test že property config vrací global_config"""
|
||||
assert file_manager.config is file_manager.global_config
|
||||
|
||||
def test_config_property_modifiable(self, file_manager):
|
||||
"""Test že změny přes config property se projeví"""
|
||||
file_manager.config["test_key"] = "test_value"
|
||||
assert file_manager.global_config["test_key"] == "test_value"
|
||||
|
||||
|
||||
class TestFileManagerEdgeCases:
|
||||
"""Testy pro edge cases"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
return TagManager()
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, tag_manager, temp_global_config):
|
||||
return FileManager(tag_manager)
|
||||
|
||||
def test_empty_filelist_operations(self, file_manager):
|
||||
"""Test operací s prázdným filelistem"""
|
||||
filtered = file_manager.filter_files_by_tags([Tag("Video", "HD")])
|
||||
assert filtered == []
|
||||
|
||||
# Přiřazení tagů na prázdný seznam
|
||||
file_manager.assign_tag_to_file_objects([], Tag("Video", "HD"))
|
||||
assert len(file_manager.filelist) == 0
|
||||
|
||||
def test_assign_tag_to_empty_list(self, file_manager):
|
||||
"""Test přiřazení tagu prázdnému seznamu souborů"""
|
||||
file_manager.assign_tag_to_file_objects([], Tag("Test", "Tag"))
|
||||
# Nemělo by vyhodit výjimku
|
||||
|
||||
def test_remove_nonexistent_tag(self, file_manager, tmp_path):
|
||||
"""Test odstranění neexistujícího tagu"""
|
||||
(tmp_path / "file.txt").write_text("content")
|
||||
file_manager.append(tmp_path)
|
||||
|
||||
# Nemělo by vyhodit výjimku
|
||||
file_manager.remove_tag_from_file_objects(file_manager.filelist, Tag("NonExistent", "Tag"))
|
||||
|
||||
def test_multiple_folders(self, file_manager, tmp_path):
|
||||
"""Test práce s více složkami"""
|
||||
folder1 = tmp_path / "folder1"
|
||||
folder2 = tmp_path / "folder2"
|
||||
folder1.mkdir()
|
||||
folder2.mkdir()
|
||||
(folder1 / "file1.txt").write_text("content1")
|
||||
(folder2 / "file2.txt").write_text("content2")
|
||||
|
||||
file_manager.append(folder1)
|
||||
file_manager.append(folder2)
|
||||
|
||||
assert len(file_manager.folders) == 2
|
||||
filenames = {f.filename for f in file_manager.filelist}
|
||||
assert "file1.txt" in filenames
|
||||
assert "file2.txt" in filenames
|
||||
|
||||
def test_folder_with_special_characters(self, file_manager, tmp_path):
|
||||
"""Test složky se speciálními znaky v názvu"""
|
||||
special_folder = tmp_path / "složka s českou diakritikou"
|
||||
special_folder.mkdir()
|
||||
(special_folder / "soubor.txt").write_text("obsah")
|
||||
|
||||
file_manager.append(special_folder)
|
||||
|
||||
filenames = {f.filename for f in file_manager.filelist}
|
||||
assert "soubor.txt" in filenames
|
||||
|
||||
def test_file_with_special_characters(self, file_manager, tmp_path):
|
||||
"""Test souboru se speciálními znaky v názvu"""
|
||||
(tmp_path / "soubor s mezerami.txt").write_text("content")
|
||||
(tmp_path / "čeština.txt").write_text("obsah")
|
||||
|
||||
file_manager.append(tmp_path)
|
||||
|
||||
filenames = {f.filename for f in file_manager.filelist}
|
||||
assert "soubor s mezerami.txt" in filenames
|
||||
assert "čeština.txt" in filenames
|
||||
|
||||
|
||||
class TestPoolManagement:
|
||||
"""Testy pro pool a copy-as-is složky"""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_global_config(self, tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "test_config.json"
|
||||
import src.core.config as config_module
|
||||
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
|
||||
return config_path
|
||||
|
||||
@pytest.fixture
|
||||
def file_manager(self, temp_global_config):
|
||||
return FileManager(TagManager())
|
||||
|
||||
def test_set_pool_creates_top_level_folders(self, file_manager, tmp_path):
|
||||
pool = tmp_path / "pool"
|
||||
file_manager.set_pool_dir(pool)
|
||||
|
||||
assert (pool / "Filmy").is_dir()
|
||||
assert (pool / "Seriály").is_dir()
|
||||
assert file_manager.pool_dir == pool
|
||||
|
||||
def test_import_movie_copies_and_indexes(self, file_manager, tmp_path):
|
||||
file_manager.set_pool_dir(tmp_path / "pool")
|
||||
source = tmp_path / "raw.mkv"
|
||||
source.write_bytes(b"x" * 10)
|
||||
|
||||
movie = file_manager.import_movie(source, "Matrix", "https://csfd.cz/film/1")
|
||||
|
||||
assert movie.file_path == tmp_path / "pool" / "Filmy" / "Matrix.mkv"
|
||||
assert source.exists() # non-destructive copy
|
||||
assert movie.title == "Matrix"
|
||||
assert movie.csfd_link == "https://csfd.cz/film/1"
|
||||
assert file_manager.index.get(movie.file_path) is not None
|
||||
|
||||
def test_load_pool_movies_reads_from_index(self, file_manager, tmp_path):
|
||||
file_manager.set_pool_dir(tmp_path / "pool")
|
||||
source = tmp_path / "raw.mkv"
|
||||
source.write_bytes(b"x" * 10)
|
||||
file_manager.import_movie(source, "Matrix", "https://csfd.cz/film/1")
|
||||
|
||||
reloaded = FileManager(TagManager())
|
||||
reloaded.set_pool_dir(tmp_path / "pool")
|
||||
reloaded.load_pool_movies()
|
||||
|
||||
assert len(reloaded.filelist) == 1
|
||||
assert reloaded.filelist[0].title == "Matrix"
|
||||
|
||||
def test_copyasis_folders_default_and_set(self, file_manager):
|
||||
assert file_manager.copyasis_folders == ["Seriály"]
|
||||
|
||||
file_manager.set_copyasis_folders(["Seriály", " Dokumenty ", ""])
|
||||
assert file_manager.copyasis_folders == ["Seriály", "Dokumenty"]
|
||||
@@ -0,0 +1,628 @@
|
||||
import pytest
|
||||
import os
|
||||
from src.core.hardlink_manager import HardlinkManager, create_hardlink_structure
|
||||
from src.core.file import File
|
||||
from src.core.tag import Tag
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
|
||||
class TestHardlinkManager:
|
||||
"""Testy pro HardlinkManager"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
"""Fixture pro TagManager"""
|
||||
tm = TagManager()
|
||||
# Remove default tags for cleaner tests
|
||||
for cat in list(tm.tags_by_category.keys()):
|
||||
tm.remove_category(cat)
|
||||
return tm
|
||||
|
||||
@pytest.fixture
|
||||
def temp_source_dir(self, tmp_path):
|
||||
"""Fixture pro zdrojovou složku s testovacími soubory"""
|
||||
source_dir = tmp_path / "source"
|
||||
source_dir.mkdir()
|
||||
(source_dir / "file1.txt").write_text("content1")
|
||||
(source_dir / "file2.txt").write_text("content2")
|
||||
(source_dir / "file3.txt").write_text("content3")
|
||||
return source_dir
|
||||
|
||||
@pytest.fixture
|
||||
def temp_output_dir(self, tmp_path):
|
||||
"""Fixture pro výstupní složku"""
|
||||
output_dir = tmp_path / "output"
|
||||
output_dir.mkdir()
|
||||
return output_dir
|
||||
|
||||
@pytest.fixture
|
||||
def files_with_tags(self, temp_source_dir, tag_manager):
|
||||
"""Fixture pro soubory s tagy"""
|
||||
files = []
|
||||
|
||||
# File 1 with multiple tags
|
||||
f1 = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
f1.tags.clear() # Remove default "Stav/Nové" tag
|
||||
f1.add_tag(Tag("žánr", "Komedie"))
|
||||
f1.add_tag(Tag("žánr", "Akční"))
|
||||
f1.add_tag(Tag("rok", "1988"))
|
||||
files.append(f1)
|
||||
|
||||
# File 2 with one tag
|
||||
f2 = File(temp_source_dir / "file2.txt", tag_manager)
|
||||
f2.tags.clear() # Remove default "Stav/Nové" tag
|
||||
f2.add_tag(Tag("žánr", "Drama"))
|
||||
files.append(f2)
|
||||
|
||||
# File 3 with no tags
|
||||
f3 = File(temp_source_dir / "file3.txt", tag_manager)
|
||||
f3.tags.clear() # Remove default "Stav/Nové" tag
|
||||
files.append(f3)
|
||||
|
||||
return files
|
||||
|
||||
def test_hardlink_manager_creation(self, temp_output_dir):
|
||||
"""Test vytvoření HardlinkManager"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
assert manager.output_dir == temp_output_dir
|
||||
assert manager.created_links == []
|
||||
assert manager.errors == []
|
||||
|
||||
def test_create_structure_basic(self, files_with_tags, temp_output_dir):
|
||||
"""Test základního vytvoření struktury"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
success, fail = manager.create_structure_for_files(files_with_tags)
|
||||
|
||||
# File1 has 3 tags, File2 has 1 tag, File3 has 0 tags
|
||||
# Should create 4 hardlinks total
|
||||
assert success == 4
|
||||
assert fail == 0
|
||||
|
||||
# Check directory structure
|
||||
assert (temp_output_dir / "žánr" / "Komedie" / "file1.txt").exists()
|
||||
assert (temp_output_dir / "žánr" / "Akční" / "file1.txt").exists()
|
||||
assert (temp_output_dir / "rok" / "1988" / "file1.txt").exists()
|
||||
assert (temp_output_dir / "žánr" / "Drama" / "file2.txt").exists()
|
||||
|
||||
def test_hardlinks_are_same_inode(self, files_with_tags, temp_output_dir, temp_source_dir):
|
||||
"""Test že vytvořené soubory jsou opravdu hardlinky (stejný inode)"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
manager.create_structure_for_files(files_with_tags)
|
||||
|
||||
original = temp_source_dir / "file1.txt"
|
||||
hardlink = temp_output_dir / "žánr" / "Komedie" / "file1.txt"
|
||||
|
||||
# Same inode = hardlink
|
||||
assert original.stat().st_ino == hardlink.stat().st_ino
|
||||
|
||||
def test_create_structure_with_category_filter(self, files_with_tags, temp_output_dir):
|
||||
"""Test vytvoření struktury jen pro vybrané kategorie"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
success, fail = manager.create_structure_for_files(files_with_tags, categories=["žánr"])
|
||||
|
||||
# Only "žánr" tags should be processed (3 links)
|
||||
assert success == 3
|
||||
assert fail == 0
|
||||
|
||||
assert (temp_output_dir / "žánr" / "Komedie" / "file1.txt").exists()
|
||||
assert not (temp_output_dir / "rok").exists()
|
||||
|
||||
def test_dry_run(self, files_with_tags, temp_output_dir):
|
||||
"""Test dry run (bez skutečného vytváření)"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
success, fail = manager.create_structure_for_files(files_with_tags, dry_run=True)
|
||||
|
||||
assert success == 4
|
||||
assert fail == 0
|
||||
|
||||
# No actual files should be created
|
||||
assert not (temp_output_dir / "žánr").exists()
|
||||
|
||||
def test_get_preview(self, files_with_tags, temp_output_dir):
|
||||
"""Test náhledu co bude vytvořeno"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
preview = manager.get_preview(files_with_tags)
|
||||
|
||||
assert len(preview) == 4
|
||||
|
||||
# Check that preview contains expected paths
|
||||
targets = [p[1] for p in preview]
|
||||
assert temp_output_dir / "žánr" / "Komedie" / "file1.txt" in targets
|
||||
assert temp_output_dir / "žánr" / "Drama" / "file2.txt" in targets
|
||||
|
||||
def test_get_preview_with_category_filter(self, files_with_tags, temp_output_dir):
|
||||
"""Test náhledu s filtrem kategorií"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
preview = manager.get_preview(files_with_tags, categories=["rok"])
|
||||
|
||||
assert len(preview) == 1
|
||||
assert preview[0][1] == temp_output_dir / "rok" / "1988" / "file1.txt"
|
||||
|
||||
def test_remove_created_links(self, files_with_tags, temp_output_dir):
|
||||
"""Test odstranění vytvořených hardlinků"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
manager.create_structure_for_files(files_with_tags)
|
||||
|
||||
# Verify links exist
|
||||
assert (temp_output_dir / "žánr" / "Komedie" / "file1.txt").exists()
|
||||
|
||||
# Remove links
|
||||
removed = manager.remove_created_links()
|
||||
assert removed == 4
|
||||
|
||||
# Links should be gone
|
||||
assert not (temp_output_dir / "žánr" / "Komedie" / "file1.txt").exists()
|
||||
|
||||
# Empty directories should also be removed
|
||||
assert not (temp_output_dir / "žánr" / "Komedie").exists()
|
||||
|
||||
def test_empty_files_list(self, temp_output_dir):
|
||||
"""Test s prázdným seznamem souborů"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
success, fail = manager.create_structure_for_files([])
|
||||
|
||||
assert success == 0
|
||||
assert fail == 0
|
||||
|
||||
def test_files_without_tags(self, temp_source_dir, temp_output_dir, tag_manager):
|
||||
"""Test se soubory bez tagů"""
|
||||
f1 = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
f1.tags.clear() # Remove default tags
|
||||
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
success, fail = manager.create_structure_for_files([f1])
|
||||
|
||||
assert success == 0
|
||||
assert fail == 0
|
||||
|
||||
def test_duplicate_link_same_file(self, files_with_tags, temp_output_dir):
|
||||
"""Test že existující hardlink na stejný soubor je přeskočen"""
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
|
||||
# Create first time
|
||||
success1, _ = manager.create_structure_for_files(files_with_tags)
|
||||
|
||||
# Create second time - should skip existing
|
||||
manager2 = HardlinkManager(temp_output_dir)
|
||||
success2, fail2 = manager2.create_structure_for_files(files_with_tags)
|
||||
|
||||
# All should be skipped (same inode)
|
||||
assert success2 == 0
|
||||
assert fail2 == 0
|
||||
|
||||
def test_unique_name_on_conflict(self, temp_source_dir, temp_output_dir, tag_manager):
|
||||
"""Test že při konfliktu (jiný soubor) se použije unikátní jméno"""
|
||||
# Create first file
|
||||
f1 = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
f1.tags.clear()
|
||||
f1.add_tag(Tag("test", "tag"))
|
||||
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
manager.create_structure_for_files([f1])
|
||||
|
||||
# Create different file with same name in different location
|
||||
source2 = temp_source_dir / "subdir"
|
||||
source2.mkdir()
|
||||
(source2 / "file1.txt").write_text("different content")
|
||||
|
||||
f2 = File(source2 / "file1.txt", tag_manager)
|
||||
f2.tags.clear()
|
||||
f2.add_tag(Tag("test", "tag"))
|
||||
|
||||
# Should create file1_1.txt
|
||||
manager2 = HardlinkManager(temp_output_dir)
|
||||
success, fail = manager2.create_structure_for_files([f2])
|
||||
|
||||
assert success == 1
|
||||
assert (temp_output_dir / "test" / "tag" / "file1_1.txt").exists()
|
||||
|
||||
def test_czech_characters_in_tags(self, temp_source_dir, temp_output_dir, tag_manager):
|
||||
"""Test českých znaků v názvech tagů"""
|
||||
f1 = File(temp_source_dir / "file1.txt", tag_manager)
|
||||
f1.tags.clear()
|
||||
f1.add_tag(Tag("Žánr", "Česká komedie"))
|
||||
f1.add_tag(Tag("Štítky", "Příběh"))
|
||||
|
||||
manager = HardlinkManager(temp_output_dir)
|
||||
success, fail = manager.create_structure_for_files([f1])
|
||||
|
||||
assert success == 2
|
||||
assert fail == 0
|
||||
assert (temp_output_dir / "Žánr" / "Česká komedie" / "file1.txt").exists()
|
||||
assert (temp_output_dir / "Štítky" / "Příběh" / "file1.txt").exists()
|
||||
|
||||
|
||||
class TestConvenienceFunction:
|
||||
"""Testy pro convenience funkci create_hardlink_structure"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
tm = TagManager()
|
||||
for cat in list(tm.tags_by_category.keys()):
|
||||
tm.remove_category(cat)
|
||||
return tm
|
||||
|
||||
@pytest.fixture
|
||||
def temp_files(self, tmp_path, tag_manager):
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag"))
|
||||
return [f]
|
||||
|
||||
def test_create_hardlink_structure_function(self, temp_files, tmp_path):
|
||||
"""Test convenience funkce"""
|
||||
output = tmp_path / "output"
|
||||
output.mkdir()
|
||||
|
||||
success, fail, errors = create_hardlink_structure(temp_files, output)
|
||||
|
||||
assert success == 1
|
||||
assert fail == 0
|
||||
assert len(errors) == 0
|
||||
assert (output / "cat" / "tag" / "file.txt").exists()
|
||||
|
||||
def test_create_hardlink_structure_with_categories(self, tmp_path, tag_manager):
|
||||
"""Test convenience funkce s filtrem kategorií"""
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("include", "yes"))
|
||||
f.add_tag(Tag("exclude", "no"))
|
||||
|
||||
output = tmp_path / "output"
|
||||
output.mkdir()
|
||||
|
||||
success, fail, errors = create_hardlink_structure([f], output, categories=["include"])
|
||||
|
||||
assert success == 1
|
||||
assert (output / "include" / "yes" / "file.txt").exists()
|
||||
assert not (output / "exclude").exists()
|
||||
|
||||
|
||||
class TestSyncStructure:
|
||||
"""Testy pro synchronizaci hardlink struktury"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
tm = TagManager()
|
||||
for cat in list(tm.tags_by_category.keys()):
|
||||
tm.remove_category(cat)
|
||||
return tm
|
||||
|
||||
@pytest.fixture
|
||||
def setup_dirs(self, tmp_path):
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
output = tmp_path / "output"
|
||||
output.mkdir()
|
||||
return source, output
|
||||
|
||||
def test_find_obsolete_links_empty_output(self, setup_dirs, tag_manager):
|
||||
"""Test find_obsolete_links s prázdným výstupem"""
|
||||
source, output = setup_dirs
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag"))
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
obsolete = manager.find_obsolete_links([f])
|
||||
|
||||
assert obsolete == []
|
||||
|
||||
def test_find_obsolete_links_detects_removed_tag(self, setup_dirs, tag_manager):
|
||||
"""Test že find_obsolete_links najde hardlink pro odebraný tag"""
|
||||
source, output = setup_dirs
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag1"))
|
||||
f.add_tag(Tag("cat", "tag2"))
|
||||
|
||||
# Create structure with both tags
|
||||
manager = HardlinkManager(output)
|
||||
manager.create_structure_for_files([f])
|
||||
|
||||
assert (output / "cat" / "tag1" / "file.txt").exists()
|
||||
assert (output / "cat" / "tag2" / "file.txt").exists()
|
||||
|
||||
# Remove one tag from file
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag1")) # Only tag1 remains
|
||||
|
||||
# Find obsolete
|
||||
obsolete = manager.find_obsolete_links([f])
|
||||
|
||||
assert len(obsolete) == 1
|
||||
assert obsolete[0][0] == output / "cat" / "tag2" / "file.txt"
|
||||
|
||||
def test_remove_obsolete_links(self, setup_dirs, tag_manager):
|
||||
"""Test odstranění zastaralých hardlinků"""
|
||||
source, output = setup_dirs
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag1"))
|
||||
f.add_tag(Tag("cat", "tag2"))
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
manager.create_structure_for_files([f])
|
||||
|
||||
# Remove tag2
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag1"))
|
||||
|
||||
# Remove obsolete links
|
||||
removed, paths = manager.remove_obsolete_links([f])
|
||||
|
||||
assert removed == 1
|
||||
assert not (output / "cat" / "tag2" / "file.txt").exists()
|
||||
assert (output / "cat" / "tag1" / "file.txt").exists()
|
||||
|
||||
def test_remove_obsolete_links_dry_run(self, setup_dirs, tag_manager):
|
||||
"""Test dry run pro remove_obsolete_links"""
|
||||
source, output = setup_dirs
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag1"))
|
||||
f.add_tag(Tag("cat", "tag2"))
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
manager.create_structure_for_files([f])
|
||||
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag1"))
|
||||
|
||||
removed, paths = manager.remove_obsolete_links([f], dry_run=True)
|
||||
|
||||
assert removed == 1
|
||||
# File should still exist (dry run)
|
||||
assert (output / "cat" / "tag2" / "file.txt").exists()
|
||||
|
||||
def test_sync_structure_creates_and_removes(self, setup_dirs, tag_manager):
|
||||
"""Test sync_structure vytvoří nové a odstraní staré hardlinky"""
|
||||
source, output = setup_dirs
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "old_tag"))
|
||||
|
||||
# Create initial structure
|
||||
manager = HardlinkManager(output)
|
||||
manager.create_structure_for_files([f])
|
||||
|
||||
assert (output / "cat" / "old_tag" / "file.txt").exists()
|
||||
|
||||
# Change tags
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "new_tag"))
|
||||
|
||||
# Sync
|
||||
created, c_fail, removed, r_fail = manager.sync_structure([f])
|
||||
|
||||
assert created == 1
|
||||
assert removed == 1
|
||||
assert c_fail == 0
|
||||
assert r_fail == 0
|
||||
assert not (output / "cat" / "old_tag").exists()
|
||||
assert (output / "cat" / "new_tag" / "file.txt").exists()
|
||||
|
||||
def test_sync_structure_no_changes_needed(self, setup_dirs, tag_manager):
|
||||
"""Test sync_structure když není potřeba žádná změna"""
|
||||
source, output = setup_dirs
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag"))
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
manager.create_structure_for_files([f])
|
||||
|
||||
# Sync again without changes
|
||||
created, c_fail, removed, r_fail = manager.sync_structure([f])
|
||||
|
||||
# Nothing should change (existing links are skipped)
|
||||
assert removed == 0
|
||||
assert (output / "cat" / "tag" / "file.txt").exists()
|
||||
|
||||
def test_find_obsolete_with_category_filter(self, setup_dirs, tag_manager):
|
||||
"""Test find_obsolete_links s filtrem kategorií"""
|
||||
source, output = setup_dirs
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat1", "tag"))
|
||||
f.add_tag(Tag("cat2", "tag"))
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
manager.create_structure_for_files([f])
|
||||
|
||||
# Remove both tags
|
||||
f.tags.clear()
|
||||
|
||||
# Find obsolete only in cat1
|
||||
obsolete = manager.find_obsolete_links([f], categories=["cat1"])
|
||||
|
||||
assert len(obsolete) == 1
|
||||
assert obsolete[0][0] == output / "cat1" / "tag" / "file.txt"
|
||||
|
||||
def test_removes_empty_directories(self, setup_dirs, tag_manager):
|
||||
"""Test že prázdné adresáře jsou odstraněny po sync"""
|
||||
source, output = setup_dirs
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("category", "tag"))
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
manager.create_structure_for_files([f])
|
||||
|
||||
# Remove all tags
|
||||
f.tags.clear()
|
||||
|
||||
manager.remove_obsolete_links([f])
|
||||
|
||||
# Directory should be gone
|
||||
assert not (output / "category" / "tag").exists()
|
||||
assert not (output / "category").exists()
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Testy pro okrajové případy"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
tm = TagManager()
|
||||
for cat in list(tm.tags_by_category.keys()):
|
||||
tm.remove_category(cat)
|
||||
return tm
|
||||
|
||||
def test_nonexistent_output_dir_created(self, tmp_path, tag_manager):
|
||||
"""Test že výstupní složka je vytvořena pokud neexistuje"""
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag"))
|
||||
|
||||
output = tmp_path / "output" / "nested" / "deep"
|
||||
# output doesn't exist
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
success, fail = manager.create_structure_for_files([f])
|
||||
|
||||
assert success == 1
|
||||
assert (output / "cat" / "tag" / "file.txt").exists()
|
||||
|
||||
def test_special_characters_in_filename(self, tmp_path, tag_manager):
|
||||
"""Test souboru se speciálními znaky v názvu"""
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
(source / "file with spaces (2024).txt").write_text("content")
|
||||
|
||||
f = File(source / "file with spaces (2024).txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("test", "tag"))
|
||||
|
||||
output = tmp_path / "output"
|
||||
output.mkdir()
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
success, fail = manager.create_structure_for_files([f])
|
||||
|
||||
assert success == 1
|
||||
assert (output / "test" / "tag" / "file with spaces (2024).txt").exists()
|
||||
|
||||
def test_empty_category_filter(self, tmp_path, tag_manager):
|
||||
"""Test s prázdným seznamem kategorií"""
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
(source / "file.txt").write_text("content")
|
||||
|
||||
f = File(source / "file.txt", tag_manager)
|
||||
f.tags.clear()
|
||||
f.add_tag(Tag("cat", "tag"))
|
||||
|
||||
output = tmp_path / "output"
|
||||
output.mkdir()
|
||||
|
||||
manager = HardlinkManager(output)
|
||||
# Empty list = no categories = no links
|
||||
success, fail = manager.create_structure_for_files([f], categories=[])
|
||||
|
||||
assert success == 0
|
||||
|
||||
def test_is_same_file_method(self, tmp_path):
|
||||
"""Test metody _is_same_file"""
|
||||
file1 = tmp_path / "file1.txt"
|
||||
file1.write_text("content")
|
||||
|
||||
link = tmp_path / "link.txt"
|
||||
os.link(file1, link)
|
||||
|
||||
file2 = tmp_path / "file2.txt"
|
||||
file2.write_text("different")
|
||||
|
||||
manager = HardlinkManager(tmp_path)
|
||||
|
||||
# Same inode
|
||||
assert manager._is_same_file(file1, link) is True
|
||||
|
||||
# Different inode
|
||||
assert manager._is_same_file(file1, file2) is False
|
||||
|
||||
# Non-existent file
|
||||
assert manager._is_same_file(file1, tmp_path / "nonexistent") is False
|
||||
|
||||
def test_get_unique_name_method(self, tmp_path):
|
||||
"""Test metody _get_unique_name"""
|
||||
(tmp_path / "file.txt").write_text("1")
|
||||
(tmp_path / "file_1.txt").write_text("2")
|
||||
(tmp_path / "file_2.txt").write_text("3")
|
||||
|
||||
manager = HardlinkManager(tmp_path)
|
||||
unique = manager._get_unique_name(tmp_path / "file.txt")
|
||||
|
||||
assert unique == tmp_path / "file_3.txt"
|
||||
|
||||
|
||||
class TestMirrorAsIs:
|
||||
"""Testy pro copy-as-is zrcadlení (Seriály)"""
|
||||
|
||||
def test_mirror_clones_hierarchy_with_hardlinks(self, tmp_path):
|
||||
"""Adresářová struktura se zrcadlí 1:1 a soubory jsou hardlinky"""
|
||||
source = tmp_path / "Seriály"
|
||||
(source / "Show" / "S01").mkdir(parents=True)
|
||||
ep1 = source / "Show" / "S01" / "ep1.mkv"
|
||||
ep2 = source / "Show" / "S01" / "ep2.mkv"
|
||||
ep1.write_text("a")
|
||||
ep2.write_text("b")
|
||||
|
||||
output = tmp_path / "out"
|
||||
manager = HardlinkManager(output)
|
||||
created, failed = manager.mirror_as_is(source, "Seriály")
|
||||
|
||||
assert failed == 0
|
||||
assert created == 2
|
||||
linked = output / "Seriály" / "Show" / "S01" / "ep1.mkv"
|
||||
assert linked.exists()
|
||||
assert linked.stat().st_ino == ep1.stat().st_ino
|
||||
|
||||
def test_mirror_skips_curator_metadata(self, tmp_path):
|
||||
"""Metadata soubory (.!tag, .!index) se nezrcadlí"""
|
||||
source = tmp_path / "Seriály"
|
||||
source.mkdir()
|
||||
(source / "ep1.mkv").write_text("a")
|
||||
(source / ".ep1.mkv.!tag").write_text("{}")
|
||||
(source / ".Curator.!index").write_text("{}")
|
||||
|
||||
output = tmp_path / "out"
|
||||
manager = HardlinkManager(output)
|
||||
created, failed = manager.mirror_as_is(source, "Seriály")
|
||||
|
||||
assert created == 1
|
||||
assert failed == 0
|
||||
assert not (output / "Seriály" / ".ep1.mkv.!tag").exists()
|
||||
|
||||
def test_mirror_nonexistent_source_is_noop(self, tmp_path):
|
||||
"""Neexistující zdroj nic neudělá"""
|
||||
manager = HardlinkManager(tmp_path / "out")
|
||||
assert manager.mirror_as_is(tmp_path / "missing", "Seriály") == (0, 0)
|
||||
@@ -0,0 +1,75 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from src.core.media_utils import load_icon
|
||||
from PIL import Image, ImageTk
|
||||
import tkinter as tk
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def tk_root():
|
||||
"""Fixture pro inicializaci Tkinteru (nutné pro ImageTk)."""
|
||||
root = tk.Tk()
|
||||
yield root
|
||||
root.destroy()
|
||||
|
||||
|
||||
def test_load_icon_returns_photoimage(tk_root):
|
||||
"""Test že load_icon vrací PhotoImage"""
|
||||
# vytvoříme dočasný obrázek
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
# vytvoříme 100x100 červený obrázek
|
||||
img = Image.new("RGB", (100, 100), color="red")
|
||||
img.save(tmp_path)
|
||||
|
||||
icon = load_icon(tmp_path)
|
||||
|
||||
# musí být PhotoImage
|
||||
assert isinstance(icon, ImageTk.PhotoImage)
|
||||
|
||||
# ověříme velikost 16x16
|
||||
assert icon.width() == 16
|
||||
assert icon.height() == 16
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_load_icon_resizes_image(tk_root):
|
||||
"""Test že load_icon správně změní velikost obrázku"""
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
# vytvoříme velký obrázek 500x500
|
||||
img = Image.new("RGB", (500, 500), color="blue")
|
||||
img.save(tmp_path)
|
||||
|
||||
icon = load_icon(tmp_path)
|
||||
|
||||
# i velký obrázek by měl být zmenšen na 16x16
|
||||
assert icon.width() == 16
|
||||
assert icon.height() == 16
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_load_icon_different_formats(tk_root):
|
||||
"""Test načítání různých formátů obrázků"""
|
||||
formats = [".png", ".jpg", ".bmp"]
|
||||
|
||||
for fmt in formats:
|
||||
with tempfile.NamedTemporaryFile(suffix=fmt, delete=False) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
img = Image.new("RGB", (32, 32), color="green")
|
||||
img.save(tmp_path)
|
||||
|
||||
icon = load_icon(tmp_path)
|
||||
|
||||
assert isinstance(icon, ImageTk.PhotoImage)
|
||||
assert icon.width() == 16
|
||||
assert icon.height() == 16
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
@@ -0,0 +1,97 @@
|
||||
import json
|
||||
|
||||
from src.core.pool_index import PoolIndex, INDEX_FILENAME
|
||||
from src.core.file import File
|
||||
from src.core.tag_manager import TagManager
|
||||
|
||||
|
||||
class TestPoolIndex:
|
||||
"""Testy pro unifikovaný metadata index poolu"""
|
||||
|
||||
def test_empty_index_when_no_file(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
assert index.records == {}
|
||||
assert index.get(tmp_path / "Filmy" / "x.mkv") is None
|
||||
|
||||
def test_set_and_get_by_relative_key(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
path = tmp_path / "Filmy" / "Matrix.mkv"
|
||||
index.set(path, {"title": "Matrix"})
|
||||
|
||||
assert index.get(path) == {"title": "Matrix"}
|
||||
# key is the pool-relative POSIX path
|
||||
assert "Filmy/Matrix.mkv" in index.records
|
||||
|
||||
def test_set_persists_to_disk(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
index.set(tmp_path / "Filmy" / "A.mkv", {"title": "A"})
|
||||
|
||||
on_disk = json.loads((tmp_path / INDEX_FILENAME).read_text(encoding="utf-8"))
|
||||
assert on_disk["movies"]["Filmy/A.mkv"]["title"] == "A"
|
||||
|
||||
def test_reload_reads_existing_index(self, tmp_path):
|
||||
PoolIndex(tmp_path).set(tmp_path / "Filmy" / "A.mkv", {"title": "A"})
|
||||
|
||||
reloaded = PoolIndex(tmp_path)
|
||||
assert reloaded.get(tmp_path / "Filmy" / "A.mkv") == {"title": "A"}
|
||||
|
||||
def test_delete_removes_record(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
path = tmp_path / "Filmy" / "A.mkv"
|
||||
index.set(path, {"title": "A"})
|
||||
index.delete(path)
|
||||
|
||||
assert index.get(path) is None
|
||||
assert PoolIndex(tmp_path).get(path) is None
|
||||
|
||||
def test_corrupt_index_loads_empty(self, tmp_path):
|
||||
(tmp_path / INDEX_FILENAME).write_text("{ not json", encoding="utf-8")
|
||||
index = PoolIndex(tmp_path)
|
||||
assert index.records == {}
|
||||
|
||||
|
||||
class TestFileWithIndex:
|
||||
"""File používá index místo sidecar souboru, je-li injektován"""
|
||||
|
||||
def test_index_backed_file_writes_no_sidecar(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
movie = tmp_path / "Filmy" / "Matrix.mkv"
|
||||
movie.parent.mkdir(parents=True)
|
||||
movie.write_bytes(b"x")
|
||||
|
||||
f = File(movie, TagManager(), index=index)
|
||||
|
||||
assert not f.metadata_filename.exists() # no sidecar
|
||||
assert index.get(movie) is not None # record created in index
|
||||
assert f.tags[0].full_path == "Stav/Nové"
|
||||
|
||||
def test_index_backed_metadata_persists_across_reload(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
movie = tmp_path / "Filmy" / "Matrix.mkv"
|
||||
movie.parent.mkdir(parents=True)
|
||||
movie.write_bytes(b"x")
|
||||
|
||||
tm = TagManager()
|
||||
f = File(movie, tm, index=index)
|
||||
f.title = "Matrix"
|
||||
f.csfd_link = "https://csfd.cz/film/1"
|
||||
f.add_tag("Žánr/Akční")
|
||||
f.set_date("1999-03-31")
|
||||
|
||||
# Fresh index + File read from disk
|
||||
index2 = PoolIndex(tmp_path)
|
||||
f2 = File(movie, TagManager(), index=index2)
|
||||
assert f2.title == "Matrix"
|
||||
assert f2.csfd_link == "https://csfd.cz/film/1"
|
||||
assert f2.date == "1999-03-31"
|
||||
assert "Žánr/Akční" in {t.full_path for t in f2.tags}
|
||||
|
||||
def test_delete_metadata_removes_index_record(self, tmp_path):
|
||||
index = PoolIndex(tmp_path)
|
||||
movie = tmp_path / "Filmy" / "Matrix.mkv"
|
||||
movie.parent.mkdir(parents=True)
|
||||
movie.write_bytes(b"x")
|
||||
|
||||
f = File(movie, TagManager(), index=index)
|
||||
f.delete_metadata()
|
||||
assert index.get(movie) is None
|
||||
@@ -0,0 +1,106 @@
|
||||
import pytest
|
||||
from src.core.tag import Tag
|
||||
|
||||
|
||||
class TestTag:
|
||||
"""Testy pro třídu Tag"""
|
||||
|
||||
def test_tag_creation(self):
|
||||
"""Test vytvoření tagu"""
|
||||
tag = Tag("Kategorie", "Název")
|
||||
assert tag.category == "Kategorie"
|
||||
assert tag.name == "Název"
|
||||
|
||||
def test_tag_full_path(self):
|
||||
"""Test full_path property"""
|
||||
tag = Tag("Video", "HD")
|
||||
assert tag.full_path == "Video/HD"
|
||||
|
||||
def test_tag_str_representation(self):
|
||||
"""Test string reprezentace"""
|
||||
tag = Tag("Foto", "Dovolená")
|
||||
assert str(tag) == "Foto/Dovolená"
|
||||
|
||||
def test_tag_repr(self):
|
||||
"""Test repr reprezentace"""
|
||||
tag = Tag("Audio", "Hudba")
|
||||
assert repr(tag) == "Tag(Audio/Hudba)"
|
||||
|
||||
def test_tag_equality_same_tags(self):
|
||||
"""Test rovnosti stejných tagů"""
|
||||
tag1 = Tag("Kategorie", "Název")
|
||||
tag2 = Tag("Kategorie", "Název")
|
||||
assert tag1 == tag2
|
||||
|
||||
def test_tag_equality_different_tags(self):
|
||||
"""Test nerovnosti různých tagů"""
|
||||
tag1 = Tag("Kategorie1", "Název")
|
||||
tag2 = Tag("Kategorie2", "Název")
|
||||
assert tag1 != tag2
|
||||
|
||||
tag3 = Tag("Kategorie", "Název1")
|
||||
tag4 = Tag("Kategorie", "Název2")
|
||||
assert tag3 != tag4
|
||||
|
||||
def test_tag_equality_with_non_tag(self):
|
||||
"""Test porovnání s ne-Tag objektem"""
|
||||
tag = Tag("Kategorie", "Název")
|
||||
assert tag != "Kategorie/Název"
|
||||
assert tag != 123
|
||||
assert tag != None
|
||||
|
||||
def test_tag_hash(self):
|
||||
"""Test hashování - důležité pro použití v set/dict"""
|
||||
tag1 = Tag("Kategorie", "Název")
|
||||
tag2 = Tag("Kategorie", "Název")
|
||||
tag3 = Tag("Jiná", "Název")
|
||||
|
||||
# Stejné tagy mají stejný hash
|
||||
assert hash(tag1) == hash(tag2)
|
||||
# Různé tagy mají různý hash (většinou)
|
||||
assert hash(tag1) != hash(tag3)
|
||||
|
||||
def test_tag_in_set(self):
|
||||
"""Test použití tagů v set"""
|
||||
tag1 = Tag("Kategorie", "Název")
|
||||
tag2 = Tag("Kategorie", "Název")
|
||||
tag3 = Tag("Jiná", "Název")
|
||||
|
||||
tag_set = {tag1, tag2, tag3}
|
||||
# tag1 a tag2 jsou stejné, takže set obsahuje pouze 2 prvky
|
||||
assert len(tag_set) == 2
|
||||
assert tag1 in tag_set
|
||||
assert tag3 in tag_set
|
||||
|
||||
def test_tag_in_dict(self):
|
||||
"""Test použití tagů jako klíčů v dict"""
|
||||
tag1 = Tag("Kategorie", "Název")
|
||||
tag2 = Tag("Kategorie", "Název")
|
||||
|
||||
tag_dict = {tag1: "hodnota1"}
|
||||
tag_dict[tag2] = "hodnota2"
|
||||
|
||||
# tag1 a tag2 jsou stejné, takže dict má 1 klíč
|
||||
assert len(tag_dict) == 1
|
||||
assert tag_dict[tag1] == "hodnota2"
|
||||
|
||||
def test_tag_with_special_characters(self):
|
||||
"""Test tagů se speciálními znaky"""
|
||||
tag = Tag("Kategorie/Složitá", "Název s mezerami")
|
||||
assert tag.category == "Kategorie/Složitá"
|
||||
assert tag.name == "Název s mezerami"
|
||||
assert tag.full_path == "Kategorie/Složitá/Název s mezerami"
|
||||
|
||||
def test_tag_with_empty_strings(self):
|
||||
"""Test tagů s prázdnými řetězci"""
|
||||
tag = Tag("", "")
|
||||
assert tag.category == ""
|
||||
assert tag.name == ""
|
||||
assert tag.full_path == "/"
|
||||
|
||||
def test_tag_unicode(self):
|
||||
"""Test tagů s unicode znaky"""
|
||||
tag = Tag("Kategorie", "Čeština")
|
||||
assert tag.category == "Kategorie"
|
||||
assert tag.name == "Čeština"
|
||||
assert tag.full_path == "Kategorie/Čeština"
|
||||
@@ -0,0 +1,327 @@
|
||||
import pytest
|
||||
from src.core.tag_manager import TagManager, DEFAULT_TAGS
|
||||
from src.core.tag import Tag
|
||||
|
||||
|
||||
class TestTagManager:
|
||||
"""Testy pro třídu TagManager"""
|
||||
|
||||
@pytest.fixture
|
||||
def tag_manager(self):
|
||||
"""Fixture pro vytvoření TagManager instance"""
|
||||
return TagManager()
|
||||
|
||||
@pytest.fixture
|
||||
def empty_tag_manager(self):
|
||||
"""Fixture pro prázdný TagManager (bez default tagů)"""
|
||||
tm = TagManager()
|
||||
# Odstranit default tagy pro testy které potřebují prázdný manager
|
||||
for category in list(tm.tags_by_category.keys()):
|
||||
tm.remove_category(category)
|
||||
return tm
|
||||
|
||||
def test_tag_manager_creation_has_defaults(self, tag_manager):
|
||||
"""Test vytvoření TagManager obsahuje default tagy"""
|
||||
assert "Hodnocení" in tag_manager.tags_by_category
|
||||
assert "Barva" in tag_manager.tags_by_category
|
||||
|
||||
def test_tag_manager_default_tags_count(self, tag_manager):
|
||||
"""Test počtu default tagů"""
|
||||
# Hodnocení má 5 hvězdiček
|
||||
assert len(tag_manager.tags_by_category["Hodnocení"]) == 5
|
||||
# Barva má 6 barev
|
||||
assert len(tag_manager.tags_by_category["Barva"]) == 6
|
||||
|
||||
def test_add_category(self, tag_manager):
|
||||
"""Test přidání kategorie"""
|
||||
tag_manager.add_category("Video")
|
||||
assert "Video" in tag_manager.tags_by_category
|
||||
assert tag_manager.tags_by_category["Video"] == set()
|
||||
|
||||
def test_add_category_duplicate(self, empty_tag_manager):
|
||||
"""Test přidání duplicitní kategorie"""
|
||||
empty_tag_manager.add_category("Video")
|
||||
empty_tag_manager.add_category("Video")
|
||||
assert len(empty_tag_manager.tags_by_category) == 1
|
||||
|
||||
def test_remove_category(self, tag_manager):
|
||||
"""Test odstranění kategorie"""
|
||||
tag_manager.add_category("Video")
|
||||
tag_manager.remove_category("Video")
|
||||
assert "Video" not in tag_manager.tags_by_category
|
||||
|
||||
def test_remove_nonexistent_category(self, tag_manager):
|
||||
"""Test odstranění neexistující kategorie"""
|
||||
# Nemělo by vyhodit výjimku
|
||||
tag_manager.remove_category("Neexistující")
|
||||
assert "Neexistující" not in tag_manager.tags_by_category
|
||||
|
||||
def test_add_tag(self, tag_manager):
|
||||
"""Test přidání tagu"""
|
||||
tag = tag_manager.add_tag("Video", "HD")
|
||||
assert isinstance(tag, Tag)
|
||||
assert tag.category == "Video"
|
||||
assert tag.name == "HD"
|
||||
assert "Video" in tag_manager.tags_by_category
|
||||
assert tag in tag_manager.tags_by_category["Video"]
|
||||
|
||||
def test_add_tag_creates_category(self, tag_manager):
|
||||
"""Test že add_tag vytvoří kategorii pokud neexistuje"""
|
||||
tag = tag_manager.add_tag("NovaKategorie", "Tag")
|
||||
assert "NovaKategorie" in tag_manager.tags_by_category
|
||||
|
||||
def test_add_multiple_tags_same_category(self, tag_manager):
|
||||
"""Test přidání více tagů do stejné kategorie"""
|
||||
tag1 = tag_manager.add_tag("Video", "HD")
|
||||
tag2 = tag_manager.add_tag("Video", "4K")
|
||||
tag3 = tag_manager.add_tag("Video", "SD")
|
||||
|
||||
assert len(tag_manager.tags_by_category["Video"]) == 3
|
||||
assert tag1 in tag_manager.tags_by_category["Video"]
|
||||
assert tag2 in tag_manager.tags_by_category["Video"]
|
||||
assert tag3 in tag_manager.tags_by_category["Video"]
|
||||
|
||||
def test_add_duplicate_tag(self, tag_manager):
|
||||
"""Test přidání duplicitního tagu (set zabrání duplicitám)"""
|
||||
tag1 = tag_manager.add_tag("Video", "HD")
|
||||
tag2 = tag_manager.add_tag("Video", "HD")
|
||||
|
||||
assert len(tag_manager.tags_by_category["Video"]) == 1
|
||||
assert tag1 == tag2
|
||||
|
||||
def test_remove_tag(self, tag_manager):
|
||||
"""Test odstranění tagu - když je poslední, kategorie se smaže"""
|
||||
tag_manager.add_tag("Video", "HD")
|
||||
tag_manager.remove_tag("Video", "HD")
|
||||
|
||||
# Kategorie by měla být smazána (podle implementace v tag_manager.py)
|
||||
assert "Video" not in tag_manager.tags_by_category
|
||||
|
||||
def test_remove_tag_removes_empty_category(self, tag_manager):
|
||||
"""Test že odstranění posledního tagu odstraní i kategorii"""
|
||||
tag_manager.add_tag("Video", "HD")
|
||||
tag_manager.remove_tag("Video", "HD")
|
||||
|
||||
assert "Video" not in tag_manager.tags_by_category
|
||||
|
||||
def test_remove_tag_keeps_category_with_other_tags(self, tag_manager):
|
||||
"""Test že odstranění tagu neodstraní kategorii s dalšími tagy"""
|
||||
tag_manager.add_tag("Video", "HD")
|
||||
tag_manager.add_tag("Video", "4K")
|
||||
tag_manager.remove_tag("Video", "HD")
|
||||
|
||||
assert "Video" in tag_manager.tags_by_category
|
||||
assert len(tag_manager.tags_by_category["Video"]) == 1
|
||||
|
||||
def test_remove_nonexistent_tag(self, tag_manager):
|
||||
"""Test odstranění neexistujícího tagu"""
|
||||
tag_manager.add_category("Video")
|
||||
# Nemělo by vyhodit výjimku
|
||||
tag_manager.remove_tag("Video", "Neexistující")
|
||||
|
||||
def test_remove_tag_from_nonexistent_category(self, tag_manager):
|
||||
"""Test odstranění tagu z neexistující kategorie"""
|
||||
# Nemělo by vyhodit výjimku
|
||||
tag_manager.remove_tag("Neexistující", "Tag")
|
||||
|
||||
def test_get_all_tags_empty(self, empty_tag_manager):
|
||||
"""Test získání všech tagů (prázdný manager)"""
|
||||
tags = empty_tag_manager.get_all_tags()
|
||||
assert tags == []
|
||||
|
||||
def test_get_all_tags(self, empty_tag_manager):
|
||||
"""Test získání všech tagů"""
|
||||
empty_tag_manager.add_tag("Video", "HD")
|
||||
empty_tag_manager.add_tag("Video", "4K")
|
||||
empty_tag_manager.add_tag("Audio", "MP3")
|
||||
|
||||
tags = empty_tag_manager.get_all_tags()
|
||||
assert len(tags) == 3
|
||||
assert "Video/HD" in tags
|
||||
assert "Video/4K" in tags
|
||||
assert "Audio/MP3" in tags
|
||||
|
||||
def test_get_all_tags_includes_defaults(self, tag_manager):
|
||||
"""Test že get_all_tags obsahuje default tagy"""
|
||||
tags = tag_manager.get_all_tags()
|
||||
# Minimálně 11 default tagů (5 hodnocení + 6 barev)
|
||||
assert len(tags) >= 11
|
||||
|
||||
def test_get_categories_empty(self, empty_tag_manager):
|
||||
"""Test získání kategorií (prázdný manager)"""
|
||||
categories = empty_tag_manager.get_categories()
|
||||
assert categories == []
|
||||
|
||||
def test_get_categories(self, empty_tag_manager):
|
||||
"""Test získání kategorií"""
|
||||
empty_tag_manager.add_tag("Video", "HD")
|
||||
empty_tag_manager.add_tag("Audio", "MP3")
|
||||
empty_tag_manager.add_tag("Foto", "RAW")
|
||||
|
||||
categories = empty_tag_manager.get_categories()
|
||||
assert len(categories) == 3
|
||||
assert "Video" in categories
|
||||
assert "Audio" in categories
|
||||
assert "Foto" in categories
|
||||
|
||||
def test_get_categories_includes_defaults(self, tag_manager):
|
||||
"""Test že get_categories obsahuje default kategorie"""
|
||||
categories = tag_manager.get_categories()
|
||||
assert "Hodnocení" in categories
|
||||
assert "Barva" in categories
|
||||
|
||||
def test_get_tags_in_category_empty(self, tag_manager):
|
||||
"""Test získání tagů z prázdné kategorie"""
|
||||
tag_manager.add_category("Video")
|
||||
tags = tag_manager.get_tags_in_category("Video")
|
||||
assert tags == []
|
||||
|
||||
def test_get_tags_in_category(self, tag_manager):
|
||||
"""Test získání tagů z kategorie"""
|
||||
tag_manager.add_tag("Video", "HD")
|
||||
tag_manager.add_tag("Video", "4K")
|
||||
tag_manager.add_tag("Audio", "MP3")
|
||||
|
||||
video_tags = tag_manager.get_tags_in_category("Video")
|
||||
assert len(video_tags) == 2
|
||||
|
||||
# Kontrola že obsahují správné tagy (pořadí není garantováno)
|
||||
tag_names = {tag.name for tag in video_tags}
|
||||
assert "HD" in tag_names
|
||||
assert "4K" in tag_names
|
||||
|
||||
def test_get_tags_in_nonexistent_category(self, tag_manager):
|
||||
"""Test získání tagů z neexistující kategorie"""
|
||||
tags = tag_manager.get_tags_in_category("Neexistující")
|
||||
assert tags == []
|
||||
|
||||
def test_complex_scenario(self, empty_tag_manager):
|
||||
"""Test komplexního scénáře použití"""
|
||||
tm = empty_tag_manager
|
||||
|
||||
# Přidání několika kategorií a tagů
|
||||
tm.add_tag("Video", "HD")
|
||||
tm.add_tag("Video", "4K")
|
||||
tm.add_tag("Audio", "MP3")
|
||||
tm.add_tag("Audio", "FLAC")
|
||||
tm.add_tag("Foto", "RAW")
|
||||
|
||||
# Kontrola stavu
|
||||
assert len(tm.get_categories()) == 3
|
||||
assert len(tm.get_all_tags()) == 5
|
||||
|
||||
# Odstranění některých tagů
|
||||
tm.remove_tag("Video", "HD")
|
||||
assert len(tm.get_tags_in_category("Video")) == 1
|
||||
|
||||
# Odstranění celé kategorie
|
||||
tm.remove_category("Foto")
|
||||
assert "Foto" not in tm.get_categories()
|
||||
assert len(tm.get_all_tags()) == 3
|
||||
|
||||
def test_tag_uniqueness_in_set(self, tag_manager):
|
||||
"""Test že tagy jsou správně ukládány jako set (bez duplicit)"""
|
||||
tag_manager.add_tag("Video", "HD")
|
||||
tag_manager.add_tag("Video", "HD")
|
||||
tag_manager.add_tag("Video", "HD")
|
||||
|
||||
# I když přidáme 3x, v setu je jen 1
|
||||
assert len(tag_manager.tags_by_category["Video"]) == 1
|
||||
|
||||
|
||||
class TestDefaultTags:
|
||||
"""Testy pro defaultní tagy"""
|
||||
|
||||
def test_default_tags_constant_exists(self):
|
||||
"""Test že DEFAULT_TAGS konstanta existuje"""
|
||||
assert DEFAULT_TAGS is not None
|
||||
assert isinstance(DEFAULT_TAGS, dict)
|
||||
|
||||
def test_default_tags_has_hodnoceni(self):
|
||||
"""Test že DEFAULT_TAGS obsahuje Hodnocení"""
|
||||
assert "Hodnocení" in DEFAULT_TAGS
|
||||
assert len(DEFAULT_TAGS["Hodnocení"]) == 5
|
||||
|
||||
def test_default_tags_has_barva(self):
|
||||
"""Test že DEFAULT_TAGS obsahuje Barva"""
|
||||
assert "Barva" in DEFAULT_TAGS
|
||||
assert len(DEFAULT_TAGS["Barva"]) == 6
|
||||
|
||||
def test_hodnoceni_stars_content(self):
|
||||
"""Test obsahu hvězdiček v Hodnocení"""
|
||||
stars = DEFAULT_TAGS["Hodnocení"]
|
||||
assert "⭐" in stars
|
||||
assert "⭐⭐⭐⭐⭐" in stars
|
||||
|
||||
def test_barva_colors_content(self):
|
||||
"""Test obsahu barev v Barva"""
|
||||
colors = DEFAULT_TAGS["Barva"]
|
||||
# Kontrolujeme že obsahuje některé barvy
|
||||
color_names = " ".join(colors)
|
||||
assert "Červená" in color_names
|
||||
assert "Zelená" in color_names
|
||||
assert "Modrá" in color_names
|
||||
|
||||
def test_tag_manager_loads_all_default_tags(self):
|
||||
"""Test že TagManager načte všechny default tagy"""
|
||||
tm = TagManager()
|
||||
|
||||
for category, tag_names in DEFAULT_TAGS.items():
|
||||
assert category in tm.tags_by_category
|
||||
tags_in_category = tm.get_tags_in_category(category)
|
||||
assert len(tags_in_category) == len(tag_names)
|
||||
|
||||
def test_can_add_custom_tags_alongside_defaults(self):
|
||||
"""Test že lze přidat vlastní tagy vedle defaultních"""
|
||||
tm = TagManager()
|
||||
initial_count = len(tm.get_all_tags())
|
||||
|
||||
tm.add_tag("Custom", "MyTag")
|
||||
|
||||
assert len(tm.get_all_tags()) == initial_count + 1
|
||||
assert "Custom" in tm.get_categories()
|
||||
|
||||
def test_can_remove_default_category(self):
|
||||
"""Test že lze odstranit default kategorii"""
|
||||
tm = TagManager()
|
||||
tm.remove_category("Hodnocení")
|
||||
|
||||
assert "Hodnocení" not in tm.tags_by_category
|
||||
|
||||
def test_hodnoceni_tags_are_sorted_by_stars(self):
|
||||
"""Test že tagy v Hodnocení jsou seřazeny od 1 do 5 hvězd"""
|
||||
tm = TagManager()
|
||||
tags = tm.get_tags_in_category("Hodnocení")
|
||||
|
||||
tag_names = [t.name for t in tags]
|
||||
assert tag_names == ["⭐", "⭐⭐", "⭐⭐⭐", "⭐⭐⭐⭐", "⭐⭐⭐⭐⭐"]
|
||||
|
||||
def test_barva_tags_are_sorted_in_predefined_order(self):
|
||||
"""Test že tagy v Barva jsou seřazeny v předdefinovaném pořadí"""
|
||||
tm = TagManager()
|
||||
tags = tm.get_tags_in_category("Barva")
|
||||
|
||||
tag_names = [t.name for t in tags]
|
||||
expected = ["🔴 Červená", "🟠 Oranžová", "🟡 Žlutá", "🟢 Zelená", "🔵 Modrá", "🟣 Fialová"]
|
||||
assert tag_names == expected
|
||||
|
||||
def test_custom_category_tags_sorted_alphabetically(self):
|
||||
"""Test že tagy v custom kategorii jsou seřazeny abecedně"""
|
||||
tm = TagManager()
|
||||
tm.add_tag("Video", "HD")
|
||||
tm.add_tag("Video", "4K")
|
||||
tm.add_tag("Video", "SD")
|
||||
|
||||
tags = tm.get_tags_in_category("Video")
|
||||
tag_names = [t.name for t in tags]
|
||||
|
||||
assert tag_names == ["4K", "HD", "SD"]
|
||||
|
||||
def test_can_add_tag_to_default_category(self):
|
||||
"""Test že lze přidat tag do default kategorie"""
|
||||
tm = TagManager()
|
||||
initial_count = len(tm.get_tags_in_category("Hodnocení"))
|
||||
|
||||
tm.add_tag("Hodnocení", "Custom Rating")
|
||||
|
||||
assert len(tm.get_tags_in_category("Hodnocení")) == initial_count + 1
|
||||
@@ -0,0 +1,178 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from src.core.utils import list_files
|
||||
|
||||
|
||||
class TestUtils:
|
||||
"""Testy pro utils funkce"""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self, tmp_path):
|
||||
"""Fixture pro dočasný adresář s testovací strukturou"""
|
||||
# Vytvoření souborů v root
|
||||
(tmp_path / "file1.txt").write_text("content1")
|
||||
(tmp_path / "file2.jpg").write_text("image")
|
||||
|
||||
# Podsložka
|
||||
subdir1 = tmp_path / "subdir1"
|
||||
subdir1.mkdir()
|
||||
(subdir1 / "file3.txt").write_text("content3")
|
||||
(subdir1 / "file4.png").write_text("image2")
|
||||
|
||||
# Vnořená podsložka
|
||||
subdir2 = subdir1 / "subdir2"
|
||||
subdir2.mkdir()
|
||||
(subdir2 / "file5.txt").write_text("content5")
|
||||
|
||||
# Prázdná složka
|
||||
empty_dir = tmp_path / "empty"
|
||||
empty_dir.mkdir()
|
||||
|
||||
return tmp_path
|
||||
|
||||
def test_list_files_basic(self, temp_dir):
|
||||
"""Test základního listování souborů"""
|
||||
files = list_files(temp_dir)
|
||||
assert isinstance(files, list)
|
||||
assert len(files) > 0
|
||||
assert all(isinstance(f, Path) for f in files)
|
||||
|
||||
def test_list_files_finds_all_files(self, temp_dir):
|
||||
"""Test že najde všechny soubory včetně vnořených"""
|
||||
files = list_files(temp_dir)
|
||||
filenames = {f.name for f in files}
|
||||
|
||||
assert "file1.txt" in filenames
|
||||
assert "file2.jpg" in filenames
|
||||
assert "file3.txt" in filenames
|
||||
assert "file4.png" in filenames
|
||||
assert "file5.txt" in filenames
|
||||
assert len(filenames) == 5
|
||||
|
||||
def test_list_files_recursive(self, temp_dir):
|
||||
"""Test rekurzivního procházení složek"""
|
||||
files = list_files(temp_dir)
|
||||
|
||||
# Kontrola cest - měly by obsahovat subdir1 a subdir2
|
||||
file_paths = [str(f) for f in files]
|
||||
assert any("subdir1" in path for path in file_paths)
|
||||
assert any("subdir2" in path for path in file_paths)
|
||||
|
||||
def test_list_files_only_files_no_directories(self, temp_dir):
|
||||
"""Test že vrací pouze soubory, ne složky"""
|
||||
files = list_files(temp_dir)
|
||||
|
||||
# Všechny výsledky by měly být soubory
|
||||
assert all(f.is_file() for f in files)
|
||||
|
||||
# Složky by neměly být ve výsledcích
|
||||
filenames = {f.name for f in files}
|
||||
assert "subdir1" not in filenames
|
||||
assert "subdir2" not in filenames
|
||||
assert "empty" not in filenames
|
||||
|
||||
def test_list_files_with_string_path(self, temp_dir):
|
||||
"""Test s cestou jako string"""
|
||||
files = list_files(str(temp_dir))
|
||||
assert len(files) == 5
|
||||
|
||||
def test_list_files_with_path_object(self, temp_dir):
|
||||
"""Test s cestou jako Path objekt"""
|
||||
files = list_files(temp_dir)
|
||||
assert len(files) == 5
|
||||
|
||||
def test_list_files_empty_directory(self, temp_dir):
|
||||
"""Test prázdné složky"""
|
||||
empty_dir = temp_dir / "empty"
|
||||
files = list_files(empty_dir)
|
||||
assert files == []
|
||||
|
||||
def test_list_files_nonexistent_directory(self):
|
||||
"""Test neexistující složky"""
|
||||
with pytest.raises(NotADirectoryError) as exc_info:
|
||||
list_files("/nonexistent/path")
|
||||
assert "není platná složka" in str(exc_info.value)
|
||||
|
||||
def test_list_files_file_not_directory(self, temp_dir):
|
||||
"""Test když je zadán soubor místo složky"""
|
||||
file_path = temp_dir / "file1.txt"
|
||||
with pytest.raises(NotADirectoryError) as exc_info:
|
||||
list_files(file_path)
|
||||
assert "není platná složka" in str(exc_info.value)
|
||||
|
||||
def test_list_files_returns_absolute_paths(self, temp_dir):
|
||||
"""Test že vrací absolutní cesty"""
|
||||
files = list_files(temp_dir)
|
||||
assert all(f.is_absolute() for f in files)
|
||||
|
||||
def test_list_files_different_extensions(self, temp_dir):
|
||||
"""Test s různými příponami"""
|
||||
files = list_files(temp_dir)
|
||||
extensions = {f.suffix for f in files}
|
||||
|
||||
assert ".txt" in extensions
|
||||
assert ".jpg" in extensions
|
||||
assert ".png" in extensions
|
||||
|
||||
def test_list_files_hidden_files(self, temp_dir):
|
||||
"""Test se skrytými soubory (začínající tečkou)"""
|
||||
# Vytvoření skrytého souboru
|
||||
(temp_dir / ".hidden").write_text("hidden content")
|
||||
|
||||
files = list_files(temp_dir)
|
||||
filenames = {f.name for f in files}
|
||||
|
||||
# Skryté soubory by měly být také nalezeny
|
||||
assert ".hidden" in filenames
|
||||
|
||||
def test_list_files_special_characters_in_names(self, temp_dir):
|
||||
"""Test se speciálními znaky v názvech"""
|
||||
# Vytvoření souborů se spec. znaky
|
||||
(temp_dir / "soubor s mezerami.txt").write_text("content")
|
||||
(temp_dir / "český_název.txt").write_text("content")
|
||||
|
||||
files = list_files(temp_dir)
|
||||
filenames = {f.name for f in files}
|
||||
|
||||
assert "soubor s mezerami.txt" in filenames
|
||||
assert "český_název.txt" in filenames
|
||||
|
||||
def test_list_files_symlinks(self, temp_dir):
|
||||
"""Test se symbolickými linky (pokud OS podporuje)"""
|
||||
try:
|
||||
# Vytvoření symlinku
|
||||
target = temp_dir / "file1.txt"
|
||||
link = temp_dir / "link_to_file1.txt"
|
||||
link.symlink_to(target)
|
||||
|
||||
files = list_files(temp_dir)
|
||||
# Symlink by měl být také nalezen a považován za soubor
|
||||
filenames = {f.name for f in files}
|
||||
assert "link_to_file1.txt" in filenames or "file1.txt" in filenames
|
||||
except OSError:
|
||||
# Pokud OS nepodporuje symlinky, přeskočíme
|
||||
pytest.skip("OS does not support symlinks")
|
||||
|
||||
def test_list_files_large_directory_structure(self, tmp_path):
|
||||
"""Test s větší strukturou složek"""
|
||||
# Vytvoření více vnořených úrovní
|
||||
for i in range(3):
|
||||
level_dir = tmp_path / f"level{i}"
|
||||
level_dir.mkdir()
|
||||
for j in range(5):
|
||||
(level_dir / f"file_{i}_{j}.txt").write_text(f"content {i} {j}")
|
||||
|
||||
files = list_files(tmp_path)
|
||||
# Měli bychom najít 3 * 5 = 15 souborů
|
||||
assert len(files) == 15
|
||||
|
||||
def test_list_files_preserves_path_structure(self, temp_dir):
|
||||
"""Test že zachovává strukturu cest"""
|
||||
files = list_files(temp_dir)
|
||||
|
||||
# Najdeme soubor v subdir2
|
||||
file5 = [f for f in files if f.name == "file5.txt"][0]
|
||||
|
||||
# Cesta by měla obsahovat obě složky
|
||||
assert "subdir1" in str(file5)
|
||||
assert "subdir2" in str(file5)
|
||||
Reference in New Issue
Block a user