diff options
Diffstat (limited to 'lib/project/tests/test_pipeline.py')
| -rw-r--r-- | lib/project/tests/test_pipeline.py | 88 |
1 files changed, 73 insertions, 15 deletions
diff --git a/lib/project/tests/test_pipeline.py b/lib/project/tests/test_pipeline.py index 0bc2cef..fc40a82 100644 --- a/lib/project/tests/test_pipeline.py +++ b/lib/project/tests/test_pipeline.py @@ -1,4 +1,7 @@ +import errno import json +import os +from pathlib import Path import numpy as np import pytest @@ -51,14 +54,18 @@ def test_existing_output_or_sidecar_rejected_before_processing(wav, tmp_path, by assert existing.read_bytes() == b"existing output" -def test_existing_report_log_is_replaced_without_overwrite(wav, tmp_path, bypass, progress, ffmpeg): +def test_existing_report_log_is_kept_and_new_report_published(wav, tmp_path, bypass, progress, ffmpeg, logs): source = wav() target = tmp_path / "master.wav" - existing = pipeline.report_for(target) - existing.parent.mkdir(parents=True, exist_ok=True) - existing.write_bytes(b"stale report") + logs.mkdir(parents=True, exist_ok=True) + stale = logs / "master.20240101-000000.report.json" + stale.write_bytes(b"stale report") report = pipeline.process(source, target, bypass, progress) - assert json.loads(existing.read_text()) == report + # Timestamped reports never replace earlier ones. + assert stale.read_bytes() == b"stale report" + published = Path(report["report"]) + assert published.parent == logs and published != stale + assert json.loads(published.read_text()) == report @pytest.mark.parametrize("kind,message", [ @@ -139,7 +146,7 @@ def test_measure_reports_channels_peaks_clipping_and_progress(wav): @pytest.mark.parametrize("rate", [44100, 48000]) -def test_normalization_peak_ceiling_and_report(wav, tmp_path, progress, ffmpeg, rate): +def test_normalization_peak_ceiling_and_report(wav, tmp_path, progress, ffmpeg, rate, report_of): source = wav() original = source.read_bytes() target = tmp_path / "nested" / "master.wav" @@ -155,7 +162,7 @@ def test_normalization_peak_ceiling_and_report(wav, tmp_path, progress, ffmpeg, assert verified["input_tp"] <= settings.true_peak_db + 0.1 assert report["master"]["near_full_scale_samples"] == 0 assert report["master"]["sample_peak_dbfs"] <= settings.true_peak_db + 0.1 - assert json.loads(pipeline.report_for(target).read_text()) == report + assert json.loads(report_of(target).read_text()) == report assert source.read_bytes() == original assert not list(target.parent.glob(".producer-*")) @@ -216,32 +223,37 @@ def test_zero_denoise_strength_skips_ai(wav, tmp_path, bypass, progress, ffmpeg, assert report["cleaned"]["frames"] == report["input"]["frames"] -def test_fft_and_mp3_export_can_replace_generated_outputs(wav, tmp_path, bypass, progress, ffmpeg): +def test_fft_and_mp3_export_can_replace_generated_outputs(wav, tmp_path, bypass, progress, ffmpeg, logs): source = wav() original = source.read_bytes() bypass.denoiser = "fft" bypass.mp3 = True target = tmp_path / "master.wav" - for path in (target, pipeline.report_for(target), target.with_suffix(".mp3")): - path.parent.mkdir(parents=True, exist_ok=True) + logs.mkdir(parents=True, exist_ok=True) + stale_report = logs / "master.20240101-000000.report.json" + for path in (target, target.with_suffix(".mp3"), stale_report): path.write_bytes(b"old output") events = [] report = pipeline.process(source, target, bypass, lambda *event: events.append(event), overwrite=True) assert any(event[0] == "FFT noise reduction" for event in events) assert target.with_suffix(".mp3").stat().st_size > 1000 assert report["mp3_loudness"]["input_i"] is not None - assert json.loads(pipeline.report_for(target).read_text()) == report + # The old report stays; the new one is timestamped and returned. + assert stale_report.read_bytes() == b"old output" + assert json.loads(Path(report["report"]).read_text()) == report + assert Path(report["report"]).parent == logs assert sf.info(target).channels == 1 assert source.read_bytes() == original -def test_render_failure_preserves_existing_outputs_and_removes_temporary_files(wav, tmp_path, bypass, progress, ffmpeg, monkeypatch): +def test_render_failure_preserves_existing_outputs_and_removes_temporary_files(wav, tmp_path, bypass, progress, ffmpeg, monkeypatch, logs): source = wav() target = tmp_path / "master.wav" bypass.mp3 = True - outputs = [target, pipeline.report_for(target), target.with_suffix(".mp3")] - for path in outputs: - path.parent.mkdir(parents=True, exist_ok=True) + outputs = [target, target.with_suffix(".mp3")] + logs.mkdir(parents=True, exist_ok=True) + stale_report = logs / "master.20240101-000000.report.json" + for path in outputs + [stale_report]: path.write_bytes(b"previous publication") real_run = pipeline.ffmpeg_run @@ -254,6 +266,7 @@ def test_render_failure_preserves_existing_outputs_and_removes_temporary_files(w with pytest.raises(RuntimeError, match="injected encoder failure"): pipeline.process(source, target, bypass, progress, overwrite=True) assert all(path.read_bytes() == b"previous publication" for path in outputs) + assert stale_report.read_bytes() == b"previous publication" assert not list(tmp_path.glob(".producer-*")) @@ -283,6 +296,51 @@ def test_publish_new_output_moves_staged_file(tmp_path): assert not staged.exists() +def test_publish_across_filesystems_falls_back_to_copy(tmp_path, monkeypatch): + """Audio outputs and the shared logs directory can be on different mounts: + publication then copies atomically into the target directory instead of + failing with EXDEV.""" + real_link, real_replace = os.link, os.replace + + def refuse_cross_dir(src, dst=None): + src, dst = Path(src), Path(dst) + # Simulate two mounts: links and renames only work within one parent. + if dst.parent != src.parent: + raise OSError(errno.EXDEV, os.strerror(errno.EXDEV)) + return real_link(src, dst) + + def refuse_cross_rename(src, dst): + src, dst = Path(src), Path(dst) + if dst.parent != src.parent: + raise OSError(errno.EXDEV, os.strerror(errno.EXDEV)) + return real_replace(src, dst) + + monkeypatch.setattr(os, "link", refuse_cross_dir) + monkeypatch.setattr(os, "replace", refuse_cross_rename) + outgoing = tmp_path / "out" + incoming = tmp_path / "in" + outgoing.mkdir() + incoming.mkdir() + # Non-overwrite: copy path must still refuse an existing destination. + staged = outgoing / "report.json" + staged.write_bytes(b"new") + (incoming / "report.json").write_bytes(b"old") + with pytest.raises(FileExistsError): + pipeline.publish(staged, incoming / "report.json", overwrite=False) + assert (incoming / "report.json").read_bytes() == b"old" + # Successful non-overwrite publication moves the staged file. + fresh = outgoing / "fresh.json" + fresh.write_bytes(b"new") + pipeline.publish(fresh, incoming / "fresh.json", overwrite=False) + assert (incoming / "fresh.json").read_bytes() == b"new" + assert not list(incoming.glob(".fresh.json*")) and not fresh.exists() + # Overwrite replaces the existing destination. + staged.write_bytes(b"newer") + pipeline.publish(staged, incoming / "report.json", overwrite=True) + assert (incoming / "report.json").read_bytes() == b"newer" + assert not list(incoming.glob(".report.json*")) and not staged.exists() + + def test_trim_gaps_shortens_silence_and_keeps_speech_exact(wav, tmp_path, progress): from producer.audio import trim_gaps sr = 48000 |
