backend/tests/test_foreign_tracks_enumeration_resume.py
Rama master · réplica de lectura del repositorio original.
"""A foreign-track walk that is stopped must still get somewhere (Forgejo #68).
Every run started a new generation and walked from the root; the prune and
the probe phase only follow a completed walk. A walk longer than the job's
ceiling — a Raspberry Pi on an SD card, a slow network mount, a very large
library — was stopped every time and never finished.
"""
import threading
from services.foreign_tracks import sweep as sw
from services.foreign_tracks.state import load_state
from services.scheduler import cancellation
from tests.test_foreign_tracks_sweep_tick import CFG, FakeClock
from tests.test_foreign_tracks_sweep_tick import app as app # noqa: F401 — fixture
from tests.test_foreign_tracks_sweep_tick import repo as repo # noqa: F401 — fixture
FILES = [(f"/media/{n}.mkv", 10, 1.0) for n in "abcd"]
def _slice(tmp_path, repo, monkeypatch, *, stop_after=None):
"""One slice over FILES; asked to stop once ``stop_after`` files were yielded."""
event = threading.Event()
upserted = []
real_upsert = repo.upsert_seen
def upsert(path, *a, **k):
upserted.append(path)
return real_upsert(path, *a, **k)
def walk(*a, **k):
for n, item in enumerate(FILES, 1):
yield item
if stop_after is not None and n >= stop_after:
event.set()
monkeypatch.setattr(repo, "upsert_seen", upsert)
monkeypatch.setattr(sw, "iter_video_files", walk)
monkeypatch.setattr(sw, "_probe_file", lambda path: {"streams": []})
token = cancellation.activate(event)
try:
sw.run_slice(str(tmp_path), CFG, budget_s=60, now_fn=FakeClock(), repo=repo)
finally:
cancellation.deactivate(token)
return upserted
def test_a_stopped_walk_keeps_its_generation(app, repo, tmp_path, monkeypatch):
_slice(tmp_path, repo, monkeypatch, stop_after=2)
generation = load_state().generation
_slice(tmp_path, repo, monkeypatch)
assert load_state().generation == generation
assert load_state().enumeration_complete
def test_the_next_walk_only_records_what_the_last_one_did_not(app, repo, tmp_path, monkeypatch):
_slice(tmp_path, repo, monkeypatch, stop_after=2)
upserted = _slice(tmp_path, repo, monkeypatch)
assert upserted == ["/media/c.mkv", "/media/d.mkv"]
def test_the_finished_walk_prunes_only_files_no_part_saw(app, repo, tmp_path, monkeypatch):
repo.upsert_seen("/media/gone.mkv", 10, 1.0, generation=0)
_slice(tmp_path, repo, monkeypatch, stop_after=2)
_slice(tmp_path, repo, monkeypatch)
from db.models.foreign_tracks import ForeignTrackScan
paths = {r.path for r in repo.session.query(ForeignTrackScan).all()}
assert paths == {f[0] for f in FILES}
def test_rows_under_an_unreadable_folder_survive_the_prune(app, repo, tmp_path, monkeypatch):
"""os.walk skips a folder it cannot list; its files are not deleted ones."""
repo.upsert_seen("/media/flaky/x.mkv", 10, 1.0, generation=0)
repo.upsert_seen("/media/gone.mkv", 10, 1.0, generation=0)
def walk(*a, unreadable=None, **k):
unreadable.append("/media/flaky")
yield from FILES
monkeypatch.setattr(sw, "iter_video_files", walk)
monkeypatch.setattr(sw, "_probe_file", lambda path: {"streams": []})
sw.run_slice(str(tmp_path), CFG, budget_s=60, now_fn=FakeClock(), repo=repo)
from db.models.foreign_tracks import ForeignTrackScan
paths = {r.path for r in repo.session.query(ForeignTrackScan).all()}
assert "/media/flaky/x.mkv" in paths
assert "/media/gone.mkv" not in paths
def test_safe_walk_reports_folders_it_cannot_read(tmp_path):
from services.cleanup_executors import _safe_walk
locked = tmp_path / "locked"
locked.mkdir()
(locked / "a.mkv").write_bytes(b"x")
locked.chmod(0)
unreadable: list[str] = []
try:
list(_safe_walk(str(tmp_path), unreadable))
finally:
locked.chmod(0o755)
assert unreadable == [str(locked)]