Hardlink generation added

This commit is contained in:
2025-12-28 16:05:34 +01:00
parent 5cdf98bdfe
commit 7e02e57397
21 changed files with 3392 additions and 2343 deletions

View File

@@ -1,252 +1,411 @@
import pytest
import json
from pathlib import Path
from src.core.config import load_config, save_config, default_config
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 TestConfig:
"""Testy pro config modul"""
class TestGlobalConfig:
"""Testy pro globální config"""
@pytest.fixture
def temp_config_file(self, tmp_path, monkeypatch):
"""Fixture pro dočasný config soubor"""
config_path = tmp_path / "test_config.json"
# Změníme CONFIG_FILE v modulu config
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, 'CONFIG_FILE', config_path)
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
return config_path
def test_default_config_structure(self):
"""Test struktury defaultní konfigurace"""
assert "ignore_patterns" in default_config
assert "last_folder" in default_config
assert isinstance(default_config["ignore_patterns"], list)
assert default_config["last_folder"] is None
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_config_nonexistent_file(self, temp_config_file):
"""Test načtení konfigurace když soubor neexistuje"""
config = load_config()
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
assert config == default_config
assert config["ignore_patterns"] == []
assert config["last_folder"] is None
def test_save_config(self, temp_config_file):
"""Test uložení konfigurace"""
def test_save_global_config(self, temp_global_config):
"""Test uložení globální konfigurace"""
test_config = {
"ignore_patterns": ["*.tmp", "*.log"],
"last_folder": "/home/user/documents"
"window_geometry": "800x600",
"window_maximized": True,
"last_folder": "/home/user/documents",
"sidebar_width": 300,
"recent_folders": ["/path1", "/path2"],
}
save_config(test_config)
save_global_config(test_config)
# Kontrola že soubor existuje
assert temp_config_file.exists()
# Kontrola obsahu
with open(temp_config_file, "r", encoding="utf-8") as f:
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_config_existing_file(self, temp_config_file):
"""Test načtení existující konfigurace"""
def test_load_global_config_existing_file(self, temp_global_config):
"""Test načtení existující globální konfigurace"""
test_config = {
"ignore_patterns": ["*.tmp"],
"last_folder": "/test/path"
"window_geometry": "1920x1080",
"window_maximized": False,
"last_folder": "/test/path",
"sidebar_width": 250,
"recent_folders": [],
}
# Uložení
save_config(test_config)
# Načtení
loaded_config = load_config()
save_global_config(test_config)
loaded_config = load_global_config()
assert loaded_config == test_config
assert loaded_config["ignore_patterns"] == ["*.tmp"]
assert loaded_config["last_folder"] == "/test/path"
def test_save_and_load_config_cycle(self, temp_config_file):
"""Test cyklu uložení a načtení"""
original_config = {
"ignore_patterns": ["*.jpg", "*.png", "*.gif"],
"last_folder": "/home/user/pictures"
}
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"}
save_config(original_config)
loaded_config = load_config()
with open(temp_global_config, "w", encoding="utf-8") as f:
json.dump(partial_config, f)
assert loaded_config == original_config
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_config_json_format(self, temp_config_file):
"""Test že config je uložen ve správném JSON formátu"""
test_config = {
"ignore_patterns": ["*.tmp"],
"last_folder": "/test"
}
save_config(test_config)
# Kontrola formátování
with open(temp_config_file, "r", encoding="utf-8") as f:
content = f.read()
# Mělo by být naformátováno s indentací
assert " " in content # 2 mezery jako indent
def test_config_utf8_encoding(self, temp_config_file):
"""Test UTF-8 encoding s českými znaky"""
test_config = {
"ignore_patterns": ["*.čeština"],
"last_folder": "/cesta/s/čestnými/znaky"
}
save_config(test_config)
loaded_config = load_config()
assert loaded_config == test_config
assert loaded_config["last_folder"] == "/cesta/s/čestnými/znaky"
def test_config_empty_ignore_patterns(self, temp_config_file):
"""Test s prázdným seznamem ignore_patterns"""
test_config = {
"ignore_patterns": [],
"last_folder": "/test"
}
save_config(test_config)
loaded_config = load_config()
assert loaded_config["ignore_patterns"] == []
def test_config_null_last_folder(self, temp_config_file):
"""Test s None hodnotou pro last_folder"""
test_config = {
"ignore_patterns": ["*.tmp"],
"last_folder": None
}
save_config(test_config)
loaded_config = load_config()
assert loaded_config["last_folder"] is None
def test_config_multiple_ignore_patterns(self, temp_config_file):
"""Test s více ignore patterny"""
patterns = ["*.tmp", "*.log", "*.cache", "*/node_modules/*", "*.pyc"]
test_config = {
"ignore_patterns": patterns,
"last_folder": "/test"
}
save_config(test_config)
loaded_config = load_config()
assert loaded_config["ignore_patterns"] == patterns
assert len(loaded_config["ignore_patterns"]) == 5
def test_config_special_characters_in_patterns(self, temp_config_file):
"""Test se speciálními znaky v patterns"""
test_config = {
"ignore_patterns": ["*.tmp", "file[0-9].txt", "test?.log"],
"last_folder": "/test"
}
save_config(test_config)
loaded_config = load_config()
assert loaded_config["ignore_patterns"] == test_config["ignore_patterns"]
def test_load_config_corrupted_file(self, temp_config_file):
"""Test načtení poškozeného config souboru"""
# Vytvoření poškozeného JSON
with open(temp_config_file, "w") as f:
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 }")
# Mělo by vrátit default config
config = load_config()
assert config == default_config
config = load_global_config()
assert config == DEFAULT_GLOBAL_CONFIG
def test_load_config_returns_new_dict(self, temp_config_file):
"""Test že load_config vrací nový dictionary (ne stejnou referenci)"""
config1 = load_config()
config2 = load_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()
# Měly by to být různé objekty (ne stejná reference)
assert config1 is not config2
# Ale hodnoty by měly být stejné
assert config1 == config2
def test_config_overwrite(self, temp_config_file):
"""Test přepsání existující konfigurace"""
config1 = {
"ignore_patterns": ["*.tmp"],
"last_folder": "/path1"
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 == ".tagger.json"
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,
}
config2 = {
"ignore_patterns": ["*.log"],
"last_folder": "/path2"
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_config(config1)
save_config(config2)
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 == config2
def test_config_path_with_spaces(self, temp_config_file):
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 = {
"ignore_patterns": [],
**DEFAULT_GLOBAL_CONFIG,
"last_folder": "/path/with spaces/in name"
}
save_config(test_config)
loaded_config = load_config()
save_global_config(test_config)
loaded = load_global_config()
assert loaded_config["last_folder"] == "/path/with spaces/in name"
assert loaded["last_folder"] == "/path/with spaces/in name"
def test_config_long_path(self, temp_config_file):
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 = {
"ignore_patterns": [],
"last_folder": long_path
**DEFAULT_FOLDER_CONFIG,
"ignore_patterns": ["*.tmp", "file[0-9].txt", "test?.log"]
}
save_config(test_config)
loaded_config = load_config()
save_folder_config(tmp_path, test_config)
loaded = load_folder_config(tmp_path)
assert loaded_config["last_folder"] == long_path
assert loaded["ignore_patterns"] == test_config["ignore_patterns"]
def test_config_many_patterns(self, temp_config_file):
"""Test s velkým počtem patterns"""
patterns = [f"*.ext{i}" for i in range(100)]
test_config = {
"ignore_patterns": patterns,
"last_folder": "/test"
}
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_config(test_config)
loaded_config = load_config()
save_global_config(test_config)
assert len(loaded_config["ignore_patterns"]) == 100
assert loaded_config["ignore_patterns"] == patterns
with open(temp_global_config, "r", encoding="utf-8") as f:
content = f.read()
def test_config_ensure_ascii_false(self, temp_config_file):
# 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 = {
"ignore_patterns": ["čeština", "русский", "中文"],
**DEFAULT_GLOBAL_CONFIG,
"last_folder": "/cesta/čeština"
}
save_config(test_config)
save_global_config(test_config)
# Kontrola že znaky nejsou escapovány
with open(temp_config_file, "r", encoding="utf-8") as f:
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

View File

@@ -15,7 +15,7 @@ class TestFileManager:
return TagManager()
@pytest.fixture
def file_manager(self, tag_manager):
def file_manager(self, tag_manager, temp_global_config):
"""Fixture pro FileManager"""
return FileManager(tag_manager)
@@ -35,12 +35,11 @@ class TestFileManager:
return tmp_path
@pytest.fixture
def temp_config_file(self, tmp_path, monkeypatch):
"""Fixture pro dočasný config soubor"""
def temp_global_config(self, tmp_path, monkeypatch):
"""Fixture pro dočasný global config soubor"""
config_path = tmp_path / "test_config.json"
# Změníme CONFIG_FILE v modulu config
import src.core.config as config_module
monkeypatch.setattr(config_module, 'CONFIG_FILE', config_path)
monkeypatch.setattr(config_module, 'GLOBAL_CONFIG_FILE', config_path)
return config_path
def test_file_manager_creation(self, file_manager, tag_manager):
@@ -48,15 +47,19 @@ class TestFileManager:
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, temp_config_file):
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, temp_config_file):
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)
@@ -68,7 +71,7 @@ class TestFileManager:
assert "file3.jpg" in filenames
assert "file4.txt" in filenames
def test_file_manager_ignores_tag_files(self, file_manager, temp_dir, temp_config_file):
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": []}')
@@ -78,29 +81,212 @@ class TestFileManager:
filenames = {f.filename for f in file_manager.filelist}
assert ".file1.txt.!tag" not in filenames
def test_file_manager_ignore_patterns(self, file_manager, temp_dir, temp_config_file):
"""Test ignorování souborů podle patternů"""
file_manager.config["ignore_patterns"] = ["*.jpg"]
def test_file_manager_ignores_tagger_json(self, file_manager, temp_dir):
"""Test že .tagger.json je ignorován"""
(temp_dir / ".tagger.json").write_text('{}')
file_manager.append(temp_dir)
filenames = {f.filename for f in file_manager.filelist}
assert ".tagger.json" 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_file_manager_ignore_patterns_path(self, file_manager, temp_dir, temp_config_file):
def test_ignore_patterns_path(self, file_manager, temp_dir):
"""Test ignorování podle celé cesty"""
file_manager.config["ignore_patterns"] = ["*/subdir/*"]
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_file_manager_assign_tag_to_file_objects(self, file_manager, temp_dir, temp_config_file):
"""Test přiřazení tagu k souborům"""
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)
# Vybereme první dva soubory
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")
@@ -109,84 +295,129 @@ class TestFileManager:
for f in files:
assert tag in f.tags
def test_file_manager_assign_tag_string(self, file_manager, temp_dir, temp_config_file):
"""Test přiřazení tagu jako string"""
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_file_manager_assign_tag_without_category(self, file_manager, temp_dir, temp_config_file):
"""Test přiřazení tagu bez kategorie"""
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_file_manager_remove_tag_from_file_objects(self, file_manager, temp_dir, temp_config_file):
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")
# Přidání a pak odstranění
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_file_manager_remove_tag_string(self, file_manager, temp_dir, temp_config_file):
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_file_manager_filter_files_by_tags_empty(self, file_manager, temp_dir, temp_config_file):
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_file_manager_filter_files_by_tags_none(self, file_manager, temp_dir, temp_config_file):
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_file_manager_filter_files_by_single_tag(self, file_manager, temp_dir, temp_config_file):
def test_filter_by_single_tag(self, file_manager, temp_dir):
"""Test filtrace podle jednoho tagu"""
file_manager.append(temp_dir)
# Přiřadíme tag některým souborům
tag = Tag("Video", "HD")
files_to_tag = file_manager.filelist[:2]
file_manager.assign_tag_to_file_objects(files_to_tag, tag)
# Filtrujeme
filtered = file_manager.filter_files_by_tags([tag])
assert len(filtered) == 2
for f in filtered:
assert tag in f.tags
def test_file_manager_filter_files_by_multiple_tags(self, file_manager, temp_dir, temp_config_file):
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")
@@ -197,87 +428,129 @@ class TestFileManager:
# Druhý soubor má jen první tag
file_manager.assign_tag_to_file_objects([file_manager.filelist[1]], tag1)
# Filtrujeme podle obou tagů
filtered = file_manager.filter_files_by_tags([tag1, tag2])
assert len(filtered) == 1
assert filtered[0] == file_manager.filelist[0]
def test_file_manager_filter_files_by_tag_strings(self, file_manager, temp_dir, temp_config_file):
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_file_manager_on_files_changed_callback(self, file_manager, temp_dir, temp_config_file):
"""Test callback při změně souborů"""
callback_called = []
def callback(filelist):
callback_called.append(filelist)
file_manager.on_files_changed = callback
def test_filter_no_match(self, file_manager, temp_dir):
"""Test filtrace když nic neodpovídá"""
file_manager.append(temp_dir)
# Přiřazení tagu by mělo zavolat callback
tag = Tag("Video", "HD")
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], tag)
filtered = file_manager.filter_files_by_tags([Tag("NonExistent", "Tag")])
assert len(filtered) == 0
assert len(callback_called) == 1
def test_file_manager_complex_scenario(self, file_manager, temp_dir, temp_config_file):
"""Test komplexního scénáře"""
# Přidání složky
file_manager.append(temp_dir)
initial_count = len(file_manager.filelist)
assert initial_count > 0
class TestFileManagerLegacy:
"""Testy pro zpětnou kompatibilitu"""
# Přiřazení různých tagů různým souborům
tag_hd = Tag("Video", "HD")
tag_4k = Tag("Video", "4K")
tag_stereo = Tag("Audio", "Stereo")
@pytest.fixture
def tag_manager(self):
return TagManager()
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], tag_hd)
file_manager.assign_tag_to_file_objects([file_manager.filelist[0]], tag_stereo)
file_manager.assign_tag_to_file_objects([file_manager.filelist[1]], tag_4k)
@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
# Filtrace podle HD
filtered_hd = file_manager.filter_files_by_tags([tag_hd])
assert len(filtered_hd) == 1
@pytest.fixture
def file_manager(self, tag_manager, temp_global_config):
return FileManager(tag_manager)
# Filtrace podle HD + Stereo
filtered_both = file_manager.filter_files_by_tags([tag_hd, tag_stereo])
assert len(filtered_both) == 1
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
# Filtrace podle 4K
filtered_4k = file_manager.filter_files_by_tags([tag_4k])
assert len(filtered_4k) == 1
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"
def test_file_manager_config_last_folder(self, file_manager, temp_dir, temp_config_file):
"""Test uložení poslední složky do konfigurace"""
file_manager.append(temp_dir)
assert file_manager.config["last_folder"] == str(temp_dir)
class TestFileManagerEdgeCases:
"""Testy pro edge cases"""
def test_file_manager_empty_filelist(self, file_manager):
"""Test práce s prázdným filelistem"""
# Test filtrace na prázdném seznamu
@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 == []
# Test přiřazení tagů na prázdný seznam
# 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_file_manager_multiple_ignore_patterns(self, file_manager, temp_dir, temp_config_file):
"""Test více ignore patternů najednou"""
file_manager.config["ignore_patterns"] = ["*.jpg", "*.png", "*/subdir/*"]
file_manager.append(temp_dir)
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 "file3.jpg" not in filenames
assert "file4.txt" not in filenames
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

View File

@@ -0,0 +1,585 @@
import pytest
import os
from pathlib import Path
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"

View File

@@ -1,5 +1,5 @@
import pytest
from src.core.tag_manager import TagManager
from src.core.tag_manager import TagManager, DEFAULT_TAGS
from src.core.tag import Tag
@@ -11,9 +11,26 @@ class TestTagManager:
"""Fixture pro vytvoření TagManager instance"""
return TagManager()
def test_tag_manager_creation(self, tag_manager):
"""Test vytvoření TagManager"""
assert tag_manager.tags_by_category == {}
@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"""
@@ -21,11 +38,11 @@ class TestTagManager:
assert "Video" in tag_manager.tags_by_category
assert tag_manager.tags_by_category["Video"] == set()
def test_add_category_duplicate(self, tag_manager):
def test_add_category_duplicate(self, empty_tag_manager):
"""Test přidání duplicitní kategorie"""
tag_manager.add_category("Video")
tag_manager.add_category("Video")
assert len(tag_manager.tags_by_category) == 1
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"""
@@ -107,40 +124,52 @@ class TestTagManager:
# Nemělo by vyhodit výjimku
tag_manager.remove_tag("Neexistující", "Tag")
def test_get_all_tags_empty(self, tag_manager):
def test_get_all_tags_empty(self, empty_tag_manager):
"""Test získání všech tagů (prázdný manager)"""
tags = tag_manager.get_all_tags()
tags = empty_tag_manager.get_all_tags()
assert tags == []
def test_get_all_tags(self, tag_manager):
def test_get_all_tags(self, empty_tag_manager):
"""Test získání všech tagů"""
tag_manager.add_tag("Video", "HD")
tag_manager.add_tag("Video", "4K")
tag_manager.add_tag("Audio", "MP3")
empty_tag_manager.add_tag("Video", "HD")
empty_tag_manager.add_tag("Video", "4K")
empty_tag_manager.add_tag("Audio", "MP3")
tags = tag_manager.get_all_tags()
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_categories_empty(self, tag_manager):
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 = tag_manager.get_categories()
categories = empty_tag_manager.get_categories()
assert categories == []
def test_get_categories(self, tag_manager):
def test_get_categories(self, empty_tag_manager):
"""Test získání kategorií"""
tag_manager.add_tag("Video", "HD")
tag_manager.add_tag("Audio", "MP3")
tag_manager.add_tag("Foto", "RAW")
empty_tag_manager.add_tag("Video", "HD")
empty_tag_manager.add_tag("Audio", "MP3")
empty_tag_manager.add_tag("Foto", "RAW")
categories = tag_manager.get_categories()
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")
@@ -166,27 +195,29 @@ class TestTagManager:
tags = tag_manager.get_tags_in_category("Neexistující")
assert tags == []
def test_complex_scenario(self, tag_manager):
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ů
tag_manager.add_tag("Video", "HD")
tag_manager.add_tag("Video", "4K")
tag_manager.add_tag("Audio", "MP3")
tag_manager.add_tag("Audio", "FLAC")
tag_manager.add_tag("Foto", "RAW")
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(tag_manager.get_categories()) == 3
assert len(tag_manager.get_all_tags()) == 5
assert len(tm.get_categories()) == 3
assert len(tm.get_all_tags()) == 5
# Odstranění některých tagů
tag_manager.remove_tag("Video", "HD")
assert len(tag_manager.get_tags_in_category("Video")) == 1
tm.remove_tag("Video", "HD")
assert len(tm.get_tags_in_category("Video")) == 1
# Odstranění celé kategorie
tag_manager.remove_category("Foto")
assert "Foto" not in tag_manager.get_categories()
assert len(tag_manager.get_all_tags()) == 3
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)"""
@@ -196,3 +227,73 @@ class TestTagManager:
# 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
assert "Barva" in tm.tags_by_category # Druhá zůstává
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