Add backup-first duplicate repair
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from serato_doctor.repair import (
|
||||
BACKUP_FOLDER,
|
||||
apply_duplicate_repair,
|
||||
plan_duplicate_repair,
|
||||
restore_backup,
|
||||
rotate_backups,
|
||||
)
|
||||
|
||||
|
||||
def library(tmp_path):
|
||||
serato = tmp_path / "_Serato_"
|
||||
crate = serato / "Subcrates" / "House.crate"
|
||||
crate.parent.mkdir(parents=True)
|
||||
crate.write_bytes(b"crate-data")
|
||||
(serato / "database V2").write_bytes(b"database-data")
|
||||
music = tmp_path / "Music"
|
||||
keeper = music / "Main" / "Track.mp3"
|
||||
duplicate = music / "Old" / "Track.mp3"
|
||||
keeper.parent.mkdir(parents=True)
|
||||
duplicate.parent.mkdir(parents=True)
|
||||
keeper.write_bytes(b"keeper")
|
||||
duplicate.write_bytes(b"duplicate")
|
||||
return serato, keeper, duplicate
|
||||
|
||||
|
||||
def test_duplicate_repair_backs_up_then_preserves_old_path_as_link(tmp_path):
|
||||
serato, keeper, duplicate = library(tmp_path)
|
||||
plan = plan_duplicate_repair(keeper, (keeper, duplicate), serato)
|
||||
|
||||
receipt = apply_duplicate_repair(plan, serato, backup_limit=3)
|
||||
|
||||
assert duplicate.is_symlink()
|
||||
assert duplicate.resolve() == keeper.resolve()
|
||||
assert (receipt.backup / "manifest.json").is_file()
|
||||
assert any((receipt.backup / "serato-metadata").rglob("House.crate"))
|
||||
assert any((receipt.backup / "serato-metadata").rglob("database V2"))
|
||||
|
||||
restored = restore_backup(receipt.backup)
|
||||
assert restored == (duplicate.resolve(),)
|
||||
assert not duplicate.is_symlink()
|
||||
assert duplicate.read_bytes() == b"duplicate"
|
||||
|
||||
|
||||
def test_plan_rejects_keeper_outside_duplicate_group(tmp_path):
|
||||
serato, keeper, duplicate = library(tmp_path)
|
||||
outsider = tmp_path / "outsider.mp3"
|
||||
outsider.write_bytes(b"other")
|
||||
|
||||
with pytest.raises(ValueError, match="must belong"):
|
||||
plan_duplicate_repair(outsider, (keeper, duplicate), serato)
|
||||
|
||||
|
||||
def test_backup_rotation_can_be_limited_or_unlimited(tmp_path):
|
||||
root = tmp_path / BACKUP_FOLDER
|
||||
for name in ("001", "002", "003"):
|
||||
backup = root / name
|
||||
backup.mkdir(parents=True)
|
||||
(backup / "manifest.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
rotate_backups(root, None)
|
||||
assert len(list(root.iterdir())) == 3
|
||||
rotate_backups(root, 2)
|
||||
assert {path.name for path in root.iterdir()} == {"002", "003"}
|
||||
+22
-1
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from serato_doctor.web import STATIC_FILES, analyze_paths
|
||||
from serato_doctor.web import STATIC_FILES, analyze_paths, duplicate_repair
|
||||
|
||||
|
||||
def test_web_analysis_uses_production_health_pipeline(tmp_path):
|
||||
@@ -49,3 +49,24 @@ def test_web_static_assets_are_declared_and_packaged():
|
||||
assert 'data-detail="database_missing_tracks"' in html
|
||||
assert 'data-detail="old_crate_references"' in html
|
||||
assert html.count('class="info-button"') >= 10
|
||||
|
||||
|
||||
def test_duplicate_repair_preview_does_not_change_files(tmp_path):
|
||||
serato = tmp_path / "_Serato_"
|
||||
serato.mkdir()
|
||||
music = tmp_path / "Music"
|
||||
first = music / "A" / "Track.mp3"
|
||||
second = music / "B" / "Track.mp3"
|
||||
first.parent.mkdir(parents=True)
|
||||
second.parent.mkdir(parents=True)
|
||||
first.write_bytes(b"first")
|
||||
second.write_bytes(b"second")
|
||||
|
||||
result = duplicate_repair(
|
||||
serato, music, first, (first, second), backup_limit=10
|
||||
)
|
||||
|
||||
assert result["applied"] is False
|
||||
assert result["database_v2_modified"] is False
|
||||
assert second.read_bytes() == b"second"
|
||||
assert not second.is_symlink()
|
||||
|
||||
Reference in New Issue
Block a user