136 lines
5.0 KiB
Python
136 lines
5.0 KiB
Python
"""
|
||
tests/test_quick_pick.py
|
||
------------------------
|
||
Tests for the quick_pick strategy and the exclude parameter
|
||
added to all predictor strategies in Phase 17.
|
||
"""
|
||
|
||
import pytest
|
||
from db.models import get_game_by_name, add_game, insert_draw
|
||
from core.predictor import (
|
||
quick_pick, hot_numbers, due_numbers,
|
||
weighted_random, monte_carlo, positional_pick,
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def pb(tmp_db):
|
||
game = get_game_by_name("Powerball")
|
||
insert_draw(game["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7)
|
||
insert_draw(game["id"], "2024-01-03", [1, 7, 13, 45, 61], bonus=15)
|
||
insert_draw(game["id"], "2024-01-05", [2, 13, 22, 36, 55], bonus=3)
|
||
return game
|
||
|
||
|
||
# ── quick_pick — basic validity ───────────────────────────────────────────────
|
||
|
||
def test_quick_pick_count(pb):
|
||
assert len(quick_pick(pb["id"])["numbers"]) == pb["main_count"]
|
||
|
||
def test_quick_pick_range(pb):
|
||
result = quick_pick(pb["id"])
|
||
assert all(1 <= n <= pb["main_max"] for n in result["numbers"])
|
||
|
||
def test_quick_pick_no_duplicates(pb):
|
||
nums = quick_pick(pb["id"])["numbers"]
|
||
assert len(nums) == len(set(nums))
|
||
|
||
def test_quick_pick_sorted(pb):
|
||
nums = quick_pick(pb["id"])["numbers"]
|
||
assert nums == sorted(nums)
|
||
|
||
def test_quick_pick_bonus_in_range(pb):
|
||
bonus = quick_pick(pb["id"])["bonus"]
|
||
assert bonus is not None
|
||
assert 1 <= bonus <= pb["bonus_max"]
|
||
|
||
def test_quick_pick_empty_db(tmp_db):
|
||
game = get_game_by_name("Powerball")
|
||
result = quick_pick(game["id"])
|
||
assert len(result["numbers"]) == 5
|
||
assert all(1 <= n <= 69 for n in result["numbers"])
|
||
|
||
def test_quick_pick_no_bonus_game(tmp_db):
|
||
gid = add_game("NoBonusLotto", 6, 49, bonus_count=0, bonus_max=0)
|
||
result = quick_pick(gid)
|
||
assert result["bonus"] is None
|
||
assert len(result["numbers"]) == 6
|
||
assert all(1 <= n <= 49 for n in result["numbers"])
|
||
|
||
|
||
# ── quick_pick — exclude parameter ───────────────────────────────────────────
|
||
|
||
def test_quick_pick_exclude_numbers(pb):
|
||
excl = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||
result = quick_pick(pb["id"], exclude=excl)
|
||
assert not any(n in excl for n in result["numbers"])
|
||
|
||
def test_quick_pick_exclude_tight(pb):
|
||
# Exclude all but exactly main_count numbers — should still pick valid ticket
|
||
excl = set(range(1, 65)) # leaves 65–69 (exactly 5 for Powerball)
|
||
result = quick_pick(pb["id"], exclude=excl)
|
||
assert len(result["numbers"]) == 5
|
||
assert all(n >= 65 for n in result["numbers"])
|
||
|
||
def test_quick_pick_exclude_too_many_falls_back(pb):
|
||
# Exclude so many that not enough remain — silently falls back
|
||
excl = set(range(1, 69)) # only [69] left, need 5
|
||
result = quick_pick(pb["id"], exclude=excl)
|
||
assert len(result["numbers"]) == 5 # must always return a full ticket
|
||
|
||
def test_quick_pick_exclude_empty_set(pb):
|
||
result = quick_pick(pb["id"], exclude=set())
|
||
assert len(result["numbers"]) == 5
|
||
|
||
def test_quick_pick_exclude_none(pb):
|
||
result = quick_pick(pb["id"], exclude=None)
|
||
assert len(result["numbers"]) == 5
|
||
|
||
|
||
# ── exclude parameter — all existing strategies ───────────────────────────────
|
||
|
||
def test_hot_numbers_exclude(pb):
|
||
excl = {1, 13, 61} # the most frequent numbers in our fixture
|
||
result = hot_numbers(pb["id"], exclude=excl)
|
||
assert not any(n in excl for n in result["numbers"])
|
||
assert len(result["numbers"]) == 5
|
||
|
||
def test_due_numbers_exclude(pb):
|
||
excl = {36, 69}
|
||
result = due_numbers(pb["id"], exclude=excl)
|
||
assert not any(n in excl for n in result["numbers"])
|
||
assert len(result["numbers"]) == 5
|
||
|
||
def test_weighted_random_exclude(pb):
|
||
excl = {1, 7, 13, 22, 36, 45, 55, 61, 69}
|
||
result = weighted_random(pb["id"], exclude=excl)
|
||
assert not any(n in excl for n in result["numbers"])
|
||
assert len(result["numbers"]) == 5
|
||
|
||
def test_monte_carlo_exclude(pb):
|
||
excl = {1, 13}
|
||
result = monte_carlo(pb["id"], simulations=200, exclude=excl)
|
||
assert not any(n in excl for n in result["numbers"])
|
||
assert len(result["numbers"]) == 5
|
||
|
||
def test_positional_pick_exclude(pb):
|
||
excl = {1, 2, 3, 4, 5}
|
||
result = positional_pick(pb["id"], exclude=excl)
|
||
assert not any(n in excl for n in result["numbers"])
|
||
assert len(result["numbers"]) == 5
|
||
|
||
def test_exclude_none_unchanged(pb):
|
||
# All strategies accept exclude=None without breaking
|
||
for fn in (hot_numbers, due_numbers, weighted_random, positional_pick):
|
||
result = fn(pb["id"], exclude=None)
|
||
assert len(result["numbers"]) == 5
|
||
|
||
def test_exclude_empty_set_unchanged(pb):
|
||
for fn in (hot_numbers, due_numbers, weighted_random, positional_pick):
|
||
result = fn(pb["id"], exclude=set())
|
||
assert len(result["numbers"]) == 5
|
||
|
||
def test_monte_carlo_exclude_none(pb):
|
||
result = monte_carlo(pb["id"], simulations=100, exclude=None)
|
||
assert len(result["numbers"]) == 5
|