aboutsummaryrefslogtreecommitdiff
path: root/lib/project/tests/test_config.py
blob: 8144fc8ea7180bf2252fea6420adf43a80248c1d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
from dataclasses import asdict, fields

import pytest

from voiceforge.cli import main, parser, settings_for
from voiceforge.config import PROFILES, RANGES, Settings, resolve, toml
from voiceforge.pipeline import filters


@pytest.mark.parametrize("profile", PROFILES)
def test_profiles_and_toml_round_trip(profile, tmp_path):
    settings = resolve(profile=profile)
    assert asdict(settings) == asdict(Settings()) | PROFILES[profile] | {"profile": profile}
    path = tmp_path / "settings.toml"
    path.write_text(toml(settings))
    assert resolve(path) == settings


def test_precedence_defaults_profile_file_flags_then_set(tmp_path):
    path = tmp_path / "settings.toml"
    path.write_text('profile = "narrator"\ncompressor_ratio = 2.2\nwarmth_db = 3\n')
    args = parser().parse_args([
        "config", "--config", str(path), "--profile", "radio",
        "--warmth-db", "4", "--set", "warmth_db=5", "--set", "warmth_db=6",
    ])
    settings = settings_for(args)
    assert settings.profile == "radio"
    assert settings.compressor_attack_ms == 8  # Selected profile beats file profile.
    assert settings.compressor_ratio == 2.2  # File values beat profile defaults.
    assert settings.warmth_db == 6  # Last --set beats named flags.
    assert settings.sample_rate == 48000


@pytest.mark.parametrize("field", [f.name for f in fields(Settings) if type(getattr(Settings(), f.name)) is bool])
def test_boolean_flags_preserve_unspecified_and_accept_both_forms(field):
    assert getattr(parser().parse_args(["config"]), field) is None
    for enabled in (True, False):
        flag = "--" + ("" if enabled else "no-") + field.replace("_", "-")
        extra = ["--limiter"] if field == "normalize" and enabled else []
        args = parser().parse_args(["config", "--profile", "cleanup-only", flag, *extra])
        assert getattr(settings_for(args), field) is enabled


def test_false_cli_toggle_overrides_true_file_value(tmp_path):
    path = tmp_path / "settings.toml"
    path.write_text("eq = true\n")
    args = parser().parse_args(["config", "--config", str(path), "--no-eq"])
    assert settings_for(args).eq is False


def test_no_denoise_and_unquoted_set_strings():
    assert settings_for(parser().parse_args(["config", "--no-denoise"])).denoiser == "none"
    args = parser().parse_args(["config", "--no-denoise", "--set", "denoiser=fft"])
    assert settings_for(args).denoiser == "fft"


@pytest.mark.parametrize("key", RANGES)
def test_numeric_ranges_are_inclusive_and_reject_outside(key):
    low, high = RANGES[key]
    for value in (low, high):
        assert getattr(resolve(overrides={key: value}), key) == value
    for value in (low - 0.01, high + 0.01):
        with pytest.raises(ValueError, match=key):
            resolve(overrides={key: value})


@pytest.mark.parametrize("overrides,match", [
    ({"typo": True}, "Unknown setting"),
    ({"highpass": 1}, "highpass must be bool"),
    ({"sample_rate": 48000.0}, "sample_rate must be int"),
    ({"target_lufs": True}, "finite number"),
    ({"target_lufs": float("nan")}, "finite number"),
    ({"target_lufs": float("inf")}, "finite number"),
    ({"denoiser": "unknown"}, "denoiser must be one of"),
    ({"device": "gpu"}, "device must be one of"),
    ({"channel": "stereo"}, "channel must be one of"),
    ({"hum_hz": 55}, "hum_hz must be one of"),
    ({"sample_rate": 96000}, "sample_rate must be one of"),
    ({"mp3_bitrate": 64}, "mp3_bitrate must be one of"),
    ({"limiter": False}, "Normalization includes true-peak limiting"),
])
def test_invalid_configuration(overrides, match):
    with pytest.raises(ValueError, match=match):
        resolve(overrides=overrides)


def test_unknown_profile_and_malformed_toml(tmp_path):
    with pytest.raises(ValueError, match="Unknown profile"):
        resolve(profile="missing")
    path = tmp_path / "bad.toml"
    path.write_text("normalize = [")
    with pytest.raises(ValueError):
        resolve(path)


def test_invalid_cli_configuration_fails_before_processing(monkeypatch, capsys):
    def unexpected(*args, **kwargs):
        pytest.fail("Invalid configuration must not reach processing or tool setup")

    monkeypatch.setattr("voiceforge.cli.process", unexpected)
    monkeypatch.setattr("voiceforge.cli.ensure_ffmpeg", unexpected)
    monkeypatch.setattr("sys.argv", ["voiceforge", "process", "missing.wav", "--target-lufs", "0"])
    with pytest.raises(SystemExit) as error:
        main()
    assert error.value.code == 1
    assert "target_lufs must be between" in capsys.readouterr().err


def test_set_requires_assignment():
    with pytest.raises(ValueError, match="KEY=VALUE"):
        settings_for(parser().parse_args(["config", "--set", "normalize"]))


@pytest.mark.parametrize("profile", ["radio", "not-a-profile"])
def test_set_profile_uses_normal_profile_resolution(profile):
    args = parser().parse_args(["config", "--set", f"profile={profile}"])
    if profile not in PROFILES:
        with pytest.raises(ValueError, match="Unknown profile"):
            settings_for(args)
    else:
        assert settings_for(args) == resolve(profile=profile)


@pytest.mark.parametrize("toggle,prefix,count", [
    ("expansion", "agate=", 1), ("eq", "equalizer=", 3),
    ("compression", "acompressor=", 1), ("deess", "deesser=", 1),
    ("lowpass", "lowpass=", 1),
])
def test_shaping_filter_toggles(bypass, toggle, prefix, count):
    assert filters(bypass) == ["anull"]
    setattr(bypass, toggle, True)
    chain = filters(bypass)
    assert len(chain) == count
    assert all(item.startswith(prefix) for item in chain)