diff options
| author | historia <historiavg@proton.me> | 2026-09-07 06:47:47 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-09-07 06:47:47 -0400 |
| commit | 84dd2d068317998f6fb59400c534ef5be6b51b53 (patch) | |
| tree | 025293e9d9229e02960374771ae522d9de2628ce /lib/project/tests/test_cli.py | |
| parent | 39b0f2bbed74f6487a41b82501ae3c6799e4b5c4 (diff) | |
| download | producer-main.tar.gz | |
Diffstat (limited to 'lib/project/tests/test_cli.py')
| -rw-r--r-- | lib/project/tests/test_cli.py | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/lib/project/tests/test_cli.py b/lib/project/tests/test_cli.py new file mode 100644 index 0000000..a5437b7 --- /dev/null +++ b/lib/project/tests/test_cli.py @@ -0,0 +1,287 @@ +import json +from pathlib import Path +import sys + +import numpy as np +import pytest +import soundfile as sf + +from voiceforge import cli, pipeline + + +@pytest.fixture +def invoke(monkeypatch): + def run(*args): + monkeypatch.setattr(sys, "argv", ["voiceforge", *map(str, args)]) + return cli.main() + + return run + + +def test_preview_real_audio_preserves_original_and_reports_provenance( + wav, tmp_path, ffmpeg, invoke, capsys, monkeypatch +): + time = np.arange(48000 * 7) / 48000 + source = wav(0.15 * np.sin(2 * np.pi * 440 * time)) + original = source.read_bytes() + output_dir = tmp_path / "previews" + modes = [] + real_run = pipeline.ffmpeg_run + + def record_render(*args, **kwargs): + log = real_run(*args, **kwargs) + if args[2] == "Mastering (pass 2)": + modes.append(json.loads(log[log.rfind("{"):log.rfind("}") + 1])["normalization_type"]) + return log + + monkeypatch.setattr(pipeline, "ffmpeg_run", record_render) + invoke("preview", source, "--no-denoise", "--start", "1", "--duration", "5", + "--profiles", "natural", "radio", "--no-normalize", "--no-limiter", + "--output-dir", output_dir) + + captured = capsys.readouterr() + assert captured.out == "" + assert "Preview:" in captured.err + assert source.read_bytes() == original + assert len(modes) == 2 + assert {path.name for path in output_dir.iterdir()} == { + f"input.{profile}.preview{suffix}" + for profile in ("natural", "radio") for suffix in (".wav", ".report.json") + } + for profile, mode in zip(("natural", "radio"), modes): + target = output_dir / f"input.{profile}.preview.wav" + report = json.loads(target.with_suffix(".report.json").read_text()) + info = sf.info(target) + assert info.duration == pytest.approx(5, abs=0.02) + assert info.channels == 1 + assert info.subtype == "PCM_24" + assert report["output"] == str(target) + assert report["provenance"] == { + "original": str(source.resolve()), "preview_start_seconds": 1, + "requested_duration_seconds": 5, "normalization_forced": True, + } + assert not Path(report["source"]).exists() + assert report["settings"]["profile"] == profile + assert report["settings"]["denoiser"] == "none" + assert report["settings"]["normalize"] is True + assert report["settings"]["limiter"] is True + assert report["settings"]["compressor_ratio"] == {"natural": 2.0, "radio": 3.5}[profile] + assert report["normalization_mode"] == mode + assert report["master_loudness"]["input_i"] == pytest.approx(-19, abs=0.5) + assert report["master_loudness"]["input_tp"] <= -1.4 + + +def test_config_stdout_json_and_set_precedence(tmp_path, invoke, capsys): + config = tmp_path / "settings.toml" + config.write_text('profile = "cleanup-only"\ntarget_lufs = -24.0\n') + invoke("config", "--json", "--config", config, "--profile", "narrator", + "--target-lufs", "-21", "--set", "target_lufs=-18", + "--set", "profile=radio", "--no-denoise") + captured = capsys.readouterr() + settings = json.loads(captured.out) + assert captured.err == "" + assert settings["profile"] == "radio" + assert settings["compressor_ratio"] == 3.5 + assert settings["compressor_attack_ms"] == 8 + assert settings["normalize"] is True + assert settings["target_lufs"] == -18 + assert settings["denoiser"] == "none" + + +def test_analyze_stdout_is_json(wav, ffmpeg, invoke, capsys): + source = wav() + original = source.read_bytes() + invoke("analyze", source, "--no-denoise") + stats = json.loads(capsys.readouterr().out) + assert stats["duration_seconds"] == pytest.approx(3) + assert stats["channels"] == 1 + assert stats["frames"] == 144000 + assert np.isfinite(stats["loudness"]["input_i"]) + assert source.read_bytes() == original + assert list(source.parent.iterdir()) == [source] + + +@pytest.mark.parametrize("start", ["nan", "inf", "-inf"]) +def test_preview_rejects_nonfinite_start_before_tool_setup( + tmp_path, invoke, capsys, monkeypatch, start +): + monkeypatch.setattr(cli, "ensure_ffmpeg", lambda: pytest.fail("Invalid start reached FFmpeg")) + output_dir = tmp_path / "previews" + with pytest.raises(SystemExit) as error: + invoke("preview", tmp_path / "missing.wav", f"--start={start}", + "--output-dir", output_dir) + assert error.value.code == 1 + captured = capsys.readouterr() + assert captured.out == "" + assert "Preview start" in captured.err + assert not output_dir.exists() + + +def test_unknown_setting_is_a_cli_error(invoke, capsys): + with pytest.raises(SystemExit) as error: + invoke("config", "--json", "--set", "unknown_setting=1") + assert error.value.code == 1 + captured = capsys.readouterr() + assert captured.out == "" + assert "Unknown setting(s): unknown_setting" in captured.err + + +def test_missing_input_fails_before_tool_setup(tmp_path, invoke, capsys, monkeypatch): + monkeypatch.setattr(pipeline, "ensure_ffmpeg", lambda: pytest.fail("Missing input reached FFmpeg")) + target = tmp_path / "master.wav" + with pytest.raises(SystemExit) as error: + invoke("process", tmp_path / "missing.wav", "--no-denoise", "-o", target) + assert error.value.code == 1 + assert "existing WAV file" in capsys.readouterr().err + assert not target.exists() + + +def test_existing_output_is_not_overwritten(wav, tmp_path, invoke, capsys, monkeypatch): + source = wav() + original = source.read_bytes() + target = tmp_path / "master.wav" + target.write_bytes(b"previous master") + monkeypatch.setattr(pipeline, "ensure_ffmpeg", lambda: pytest.fail("Existing output reached FFmpeg")) + with pytest.raises(SystemExit) as error: + invoke("process", source, "--no-denoise", "-o", target) + assert error.value.code == 1 + assert "Output exists" in capsys.readouterr().err + assert target.read_bytes() == b"previous master" + assert source.read_bytes() == original + assert not target.with_suffix(".report.json").exists() + + +def test_overwrite_cannot_replace_input(wav, invoke, capsys, monkeypatch): + source = wav() + original = source.read_bytes() + monkeypatch.setattr(cli, "process", lambda *a, **kw: pytest.fail("Input collision reached processing")) + with pytest.raises(SystemExit) as error: + invoke("process", source, "-o", source, "--overwrite") + assert error.value.code == 1 + assert "overwrite an input recording" in capsys.readouterr().err + assert source.read_bytes() == original + + +def test_duplicate_batch_names_fail_before_processing(wav, tmp_path, invoke, capsys, monkeypatch): + (tmp_path / "other").mkdir() + sources = [wav(), wav(name="other/input.wav")] + originals = [source.read_bytes() for source in sources] + output_dir = tmp_path / "masters" + monkeypatch.setattr(cli, "process", lambda *a, **kw: pytest.fail("Duplicate names reached processing")) + with pytest.raises(SystemExit) as error: + invoke("process", *sources, "--output-dir", output_dir, "--overwrite") + assert error.value.code == 1 + assert "duplicate output names" in capsys.readouterr().err + assert not output_dir.exists() + assert [source.read_bytes() for source in sources] == originals + + +def test_default_outputs_are_written_beside_each_input(wav, tmp_path, ffmpeg, invoke, capsys): + (tmp_path / "a").mkdir() + (tmp_path / "b").mkdir() + sources = [wav(name="a/take.wav"), wav(name="b/take.wav")] + invoke("process", *sources, "--no-denoise") + for source in sources: + assert {path.name for path in source.parent.iterdir()} == { + "take.wav", "take.natural.wav", "take.natural.report.json"} + captured = capsys.readouterr() + assert captured.out == "" + assert captured.err.count("Written:") == 2 + + +def test_output_and_output_dir_cannot_be_combined(wav, tmp_path, invoke, capsys): + with pytest.raises(SystemExit) as error: + invoke("process", wav(), "-o", tmp_path / "x.wav", "--output-dir", tmp_path / "outs") + assert error.value.code == 1 + assert "--output-dir cannot be combined" in capsys.readouterr().err + + +def test_missing_later_input_fails_before_any_processing(wav, tmp_path, invoke, capsys, monkeypatch): + source = wav() + monkeypatch.setattr(cli, "process", lambda *a, **kw: pytest.fail("Preflight must reject the batch")) + with pytest.raises(SystemExit) as error: + invoke("process", source, tmp_path / "missing.wav", "--no-denoise") + assert error.value.code == 1 + assert "existing WAV file" in capsys.readouterr().err + assert not (tmp_path / "input.natural.wav").exists() + + +def test_existing_output_in_batch_fails_before_any_processing(wav, tmp_path, invoke, capsys, monkeypatch): + (tmp_path / "a").mkdir() + (tmp_path / "b").mkdir() + sources = [wav(name="a/take.wav"), wav(name="b/take.wav")] + (tmp_path / "b" / "take.natural.wav").write_bytes(b"previous result") + monkeypatch.setattr(cli, "process", lambda *a, **kw: pytest.fail("Preflight must reject the batch")) + with pytest.raises(SystemExit) as error: + invoke("process", *sources, "--no-denoise") + assert error.value.code == 1 + assert "Output exists" in capsys.readouterr().err + assert not (tmp_path / "a" / "take.natural.wav").exists() + + +def test_preview_rejects_duplicate_profiles_and_profile_override(wav, invoke, capsys, monkeypatch): + source = wav() + monkeypatch.setattr(cli, "ensure_ffmpeg", lambda: pytest.fail("Invalid preview plan reached FFmpeg")) + with pytest.raises(SystemExit) as error: + invoke("preview", source, "--no-denoise", "--profiles", "natural", "natural") + assert error.value.code == 1 + assert "unique" in capsys.readouterr().err + with pytest.raises(SystemExit) as error: + invoke("preview", source, "--no-denoise", "--profiles", "natural", + "--set", "profile=radio") + assert error.value.code == 1 + assert "remove the profile override" in capsys.readouterr().err + assert list(source.parent.iterdir()) == [source] + + +def test_preview_default_output_is_written_beside_input(wav, tmp_path, ffmpeg, invoke, capsys): + source = wav() + original = source.read_bytes() + invoke("preview", source, "--no-denoise", "--start", "1", "--duration", "2", + "--profiles", "natural") + assert {path.name for path in tmp_path.iterdir()} == { + "input.wav", "input.natural.preview.wav", "input.natural.preview.report.json"} + report = json.loads((tmp_path / "input.natural.preview.report.json").read_text()) + assert report["settings"]["profile"] == "natural" + assert source.read_bytes() == original + + +def test_preview_cancellation_cleans_excerpt(wav, tmp_path, ffmpeg, invoke, capsys, monkeypatch): + source = wav() + original = source.read_bytes() + output_dir = tmp_path / "previews" + excerpts = [] + + def cancel(excerpt, *args, **kwargs): + assert excerpt.is_file() + excerpts.append(excerpt) + raise KeyboardInterrupt + + monkeypatch.setattr(cli, "process", cancel) + with pytest.raises(SystemExit) as error: + invoke("preview", source, "--no-denoise", "--output-dir", output_dir) + assert error.value.code == 130 + assert "Cancelled" in capsys.readouterr().err + assert len(excerpts) == 1 + assert not excerpts[0].exists() + assert list(output_dir.iterdir()) == [] + assert source.read_bytes() == original + + +@pytest.mark.parametrize("suffix", [".wav", ".report.json", ".mp3"]) +def test_nonfile_output_with_overwrite_fails_before_tool_setup( + wav, tmp_path, bypass, progress, monkeypatch, suffix +): + source = wav() + original = source.read_bytes() + target = tmp_path / "master.wav" + nonfile = target.with_suffix(suffix) + nonfile.mkdir() + bypass.mp3 = True + monkeypatch.setattr(pipeline, "ensure_ffmpeg", lambda: pytest.fail("Non-file output reached FFmpeg")) + with pytest.raises(ValueError, match="Output is not a regular file"): + pipeline.process(source, target, bypass, progress, overwrite=True) + assert nonfile.is_dir() + assert list(nonfile.iterdir()) == [] + assert set(tmp_path.iterdir()) == {source, nonfile} + assert source.read_bytes() == original |
