diff --git a/lotto/tests/test_weight_evolver.py b/lotto/tests/test_weight_evolver.py new file mode 100644 index 0000000..c7d9d0d --- /dev/null +++ b/lotto/tests/test_weight_evolver.py @@ -0,0 +1,122 @@ +# lotto/tests/test_weight_evolver.py +import json +import math +import pytest + +from app import weight_evolver as we + + +def test_clamp_and_normalize_min_floor(): + """모든 값이 0.05 이상이 되도록 보장 + 합=1.0.""" + W = we.clamp_and_normalize([0.01, 0.6, 0.2, 0.1, 0.09]) + assert all(w >= 0.05 - 1e-9 for w in W) + assert abs(sum(W) - 1.0) < 1e-9 + + +def test_clamp_and_normalize_negative_becomes_floor(): + W = we.clamp_and_normalize([-0.1, 0.5, 0.3, 0.2, 0.1]) + assert W[0] >= 0.05 - 1e-9 + assert abs(sum(W) - 1.0) < 1e-9 + + +def test_perturbation_changes_around_base(): + """σ=0.05 정규분포 perturbation 후 정규화 — 각 값이 합리적 범위 안.""" + base = [0.2, 0.2, 0.2, 0.2, 0.2] + W = we.perturb_weights(base, sigma=0.05, seed=42) + assert abs(sum(W) - 1.0) < 1e-9 + assert all(w >= 0.05 - 1e-9 for w in W) + + +def test_dirichlet_random_distribution(): + """Dirichlet α=2 — 5종 비음수 합=1.""" + W = we.dirichlet_weights(alpha=2.0, seed=42) + assert abs(sum(W) - 1.0) < 1e-9 + assert all(0.05 - 1e-9 <= w <= 1.0 for w in W) + + +def test_generate_weekly_candidates_count(): + """6개 후보 생성 — 4 perturb + 2 dirichlet.""" + base = [0.2, 0.2, 0.2, 0.2, 0.2] + trials = we.generate_weekly_candidates(base, seed=42) + assert len(trials) == 6 + sources = [t["source"] for t in trials] + assert sources.count("perturb") == 4 + assert sources.count("dirichlet") == 2 + days = sorted(t["day_of_week"] for t in trials) + assert days == [0, 1, 2, 3, 4, 5] + + +def test_calc_pick_score_six_match(): + """6개 모두 일치 → 1등 → base=1.0 + bonus 1.0 = 2.0.""" + score = we.calc_pick_score([1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6]) + assert score == pytest.approx(2.0) + + +def test_calc_pick_score_four_match(): + """4개 일치 → 4등 → base=4/6 + bonus 0.3.""" + score = we.calc_pick_score([1, 2, 3, 4, 7, 8], [1, 2, 3, 4, 5, 6]) + assert score == pytest.approx(4/6 + 0.3) + + +def test_calc_pick_score_three_match(): + """3개 일치 → 5등 → base=3/6 + bonus 0.1.""" + score = we.calc_pick_score([1, 2, 3, 7, 8, 9], [1, 2, 3, 4, 5, 6]) + assert score == pytest.approx(3/6 + 0.1) + + +def test_calc_pick_score_two_match_no_bonus(): + """2개 일치 → 미당첨 → base=2/6 + bonus 0.""" + score = we.calc_pick_score([1, 2, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6]) + assert score == pytest.approx(2/6) + + +def test_decide_base_update_winner_4plus_replaces(): + """winner_max_correct ≥ 4 → 교체.""" + current = [0.2, 0.2, 0.2, 0.2, 0.2] + winner_W = [0.1, 0.3, 0.2, 0.3, 0.1] + new_base, reason = we.decide_base_update( + winner_max_correct=4, + winner_W=winner_W, + current_base=current, + ) + assert new_base == winner_W + assert reason == "winner_4plus" + + +def test_decide_base_update_winner_3_ema_blend(): + """winner_max_correct = 3 → 0.3*winner + 0.7*current.""" + current = [0.2, 0.2, 0.2, 0.2, 0.2] + winner_W = [0.1, 0.3, 0.2, 0.3, 0.1] + new_base, reason = we.decide_base_update( + winner_max_correct=3, + winner_W=winner_W, + current_base=current, + ) + expected = [0.3 * w + 0.7 * c for w, c in zip(winner_W, current)] + assert all(abs(a - b) < 1e-9 for a, b in zip(new_base, expected)) + assert reason == "ema_blend" + + +def test_decide_base_update_winner_lt3_unchanged(): + """winner_max_correct ≤ 2 → 직전 base 유지.""" + current = [0.2, 0.2, 0.2, 0.2, 0.2] + winner_W = [0.1, 0.3, 0.2, 0.3, 0.1] + new_base, reason = we.decide_base_update( + winner_max_correct=2, + winner_W=winner_W, + current_base=current, + ) + assert new_base == current + assert reason == "unchanged" + + +def test_decide_base_update_cold_start_returns_default(): + """current_base=None (첫 회) → 균등 default 반환.""" + winner_W = [0.1, 0.3, 0.2, 0.3, 0.1] + new_base, reason = we.decide_base_update( + winner_max_correct=4, + winner_W=winner_W, + current_base=None, + ) + assert new_base == winner_W + assert reason == "winner_4plus"