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)