mirror of https://github.com/Nezreka/SoulSync.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
138 lines
4.3 KiB
138 lines
4.3 KiB
"""Atomic tag saves: an interrupted/OOM-killed save must never destroy the
|
|
user's file (#819 — CubeComming's hi-res FLACs imported to an empty shell).
|
|
|
|
save_audio_file writes the modified tags into a temp COPY, verifies it's still
|
|
valid audio, then os.replace()s it in — the original is untouched until that
|
|
atomic swap.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from core.metadata.common import save_audio_file
|
|
|
|
|
|
def _symbols(file_length):
|
|
"""Symbols whose File() reports the given decoded length (None → invalid)."""
|
|
return SimpleNamespace(
|
|
ID3=type("ID3", (), {}), FLAC=type("FLAC", (), {}),
|
|
File=lambda p: (None if file_length is None
|
|
else SimpleNamespace(info=SimpleNamespace(length=file_length))),
|
|
)
|
|
|
|
|
|
def test_atomic_replace_on_success(tmp_path):
|
|
f = tmp_path / "song.flac"
|
|
f.write_bytes(b"ORIGINAL-AUDIO")
|
|
saved_to = []
|
|
|
|
class Audio:
|
|
filename = str(f)
|
|
tags = None
|
|
|
|
def save(self, target=None, **k):
|
|
saved_to.append(target)
|
|
# mimic mutagen writing modified tags into the temp copy
|
|
with open(target, "ab") as h:
|
|
h.write(b"+TAGS")
|
|
|
|
save_audio_file(Audio(), _symbols(180.0))
|
|
|
|
assert f.read_bytes() == b"ORIGINAL-AUDIO+TAGS" # replaced with the tagged copy
|
|
assert saved_to == [str(f) + ".sstmp"] # wrote the temp, NOT in place
|
|
assert not (tmp_path / "song.flac.sstmp").exists() # temp cleaned up
|
|
|
|
|
|
def test_original_survives_save_failure(tmp_path):
|
|
# The #819 scenario: the save blows up mid-write. The original must be intact.
|
|
f = tmp_path / "song.flac"
|
|
f.write_bytes(b"ORIGINAL")
|
|
inplace = []
|
|
|
|
class Audio:
|
|
filename = str(f)
|
|
tags = None
|
|
|
|
def save(self, target=None, **k):
|
|
if target is not None:
|
|
raise OSError("simulated interrupted/OOM save")
|
|
inplace.append(True) # fallback in-place save (writes nothing here)
|
|
|
|
save_audio_file(Audio(), _symbols(180.0))
|
|
|
|
assert f.read_bytes() == b"ORIGINAL" # never destroyed
|
|
assert not (tmp_path / "song.flac.sstmp").exists() # temp removed
|
|
assert inplace == [True] # fell back to in-place
|
|
|
|
|
|
def test_corrupt_temp_rejected(tmp_path):
|
|
# save-to-temp "succeeds" but produces a file with no audio → must NOT
|
|
# replace the original; fall back instead.
|
|
f = tmp_path / "song.flac"
|
|
f.write_bytes(b"ORIGINAL")
|
|
inplace = []
|
|
|
|
class Audio:
|
|
filename = str(f)
|
|
tags = None
|
|
|
|
def save(self, target=None, **k):
|
|
if target is None:
|
|
inplace.append(True)
|
|
|
|
save_audio_file(Audio(), _symbols(0)) # File().info.length == 0 → invalid
|
|
|
|
assert f.read_bytes() == b"ORIGINAL"
|
|
assert not (tmp_path / "song.flac.sstmp").exists()
|
|
assert inplace == [True]
|
|
|
|
|
|
def test_no_filename_plain_save():
|
|
saves = []
|
|
|
|
class Audio:
|
|
filename = None
|
|
tags = None
|
|
|
|
def save(self, target=None, **k):
|
|
saves.append(target)
|
|
|
|
save_audio_file(Audio(), _symbols(180.0))
|
|
assert saves == [None] # nothing to be atomic about → plain in-place
|
|
|
|
|
|
# ── real mutagen round-trip (only if ffmpeg can make a FLAC) ──
|
|
|
|
def _make_flac(path):
|
|
ff = shutil.which("ffmpeg")
|
|
if not ff:
|
|
return False
|
|
r = subprocess.run(
|
|
[ff, "-f", "lavfi", "-i", "sine=frequency=440:duration=1", "-y", str(path)],
|
|
capture_output=True)
|
|
return r.returncode == 0 and os.path.getsize(path) > 0
|
|
|
|
|
|
def test_real_flac_atomic_save_preserves_audio(tmp_path):
|
|
from mutagen.flac import FLAC
|
|
f = tmp_path / "real.flac"
|
|
if not _make_flac(f):
|
|
pytest.skip("ffmpeg unavailable — cannot build a real FLAC")
|
|
orig_len = FLAC(str(f)).info.length
|
|
|
|
audio = FLAC(str(f))
|
|
audio["title"] = "Atomic Test"
|
|
save_audio_file(audio, SimpleNamespace(ID3=type("ID3", (), {}), FLAC=FLAC,
|
|
File=__import__("mutagen").File))
|
|
|
|
reread = FLAC(str(f))
|
|
assert reread.info.length == pytest.approx(orig_len, abs=0.05) # audio intact
|
|
assert reread["title"] == ["Atomic Test"] # tag written
|
|
assert not (tmp_path / "real.flac.sstmp").exists()
|