224 lines
7.7 KiB
Python
224 lines
7.7 KiB
Python
"""
|
||
tests/test_filters.py
|
||
---------------------
|
||
Tests for core/filters.py and the filter integration in core/predictor.py.
|
||
"""
|
||
|
||
import pytest
|
||
from db.models import get_game_by_name, insert_draw
|
||
from core.filters import _has_consecutive_run, sum_range_percentiles, passes_filters
|
||
from core.predictor import (
|
||
hot_numbers, due_numbers, weighted_random,
|
||
monte_carlo, positional_pick, quick_pick,
|
||
_swap_filter, _retry_filter,
|
||
)
|
||
|
||
|
||
# ── _has_consecutive_run ──────────────────────────────────────────────────────
|
||
|
||
def test_no_consecutive():
|
||
assert _has_consecutive_run([1, 5, 10, 20, 30]) is False
|
||
|
||
|
||
def test_run_of_three_not_flagged():
|
||
assert _has_consecutive_run([1, 2, 3, 10, 20]) is False # run=3, threshold=4
|
||
|
||
|
||
def test_run_of_four_flagged():
|
||
assert _has_consecutive_run([1, 2, 3, 4, 20]) is True
|
||
|
||
|
||
def test_run_of_five_flagged():
|
||
assert _has_consecutive_run([10, 11, 12, 13, 14]) is True
|
||
|
||
|
||
def test_run_at_end():
|
||
assert _has_consecutive_run([5, 20, 30, 31, 32, 33]) is True
|
||
|
||
|
||
def test_run_not_contiguous():
|
||
assert _has_consecutive_run([1, 3, 5, 7, 9]) is False
|
||
|
||
|
||
# ── passes_filters — odd/even ─────────────────────────────────────────────────
|
||
|
||
def test_all_even_rejected():
|
||
assert passes_filters([2, 14, 28, 42, 60]) is False
|
||
|
||
|
||
def test_all_odd_rejected():
|
||
assert passes_filters([1, 7, 13, 29, 69]) is False
|
||
|
||
|
||
def test_mixed_odd_even_accepted():
|
||
assert passes_filters([1, 14, 28, 42, 60]) is True
|
||
|
||
|
||
def test_all_even_under_4_accepted():
|
||
# Odd/even filter only kicks in for 4+ numbers
|
||
assert passes_filters([2, 4, 6]) is True
|
||
|
||
|
||
# ── passes_filters — consecutive ─────────────────────────────────────────────
|
||
|
||
def test_four_consecutive_rejected():
|
||
assert passes_filters([5, 6, 7, 8, 20]) is False
|
||
|
||
|
||
def test_three_consecutive_accepted():
|
||
assert passes_filters([5, 6, 7, 20, 35]) is True
|
||
|
||
|
||
# ── passes_filters — sum range ────────────────────────────────────────────────
|
||
|
||
def test_sum_in_range_accepted():
|
||
assert passes_filters([1, 14, 28, 42, 60], sum_range=(100, 200)) is True
|
||
|
||
|
||
def test_sum_below_range_rejected():
|
||
assert passes_filters([1, 2, 3, 4, 10], sum_range=(100, 200)) is False
|
||
|
||
|
||
def test_sum_above_range_rejected():
|
||
assert passes_filters([60, 62, 64, 66, 69], sum_range=(100, 200)) is False
|
||
|
||
|
||
def test_sum_range_none_skips_check():
|
||
# All-even but sum would be out of range if range were set — only even check applies
|
||
assert passes_filters([2, 4, 6, 8, 10], sum_range=None) is False # all-even fails
|
||
|
||
|
||
def test_sum_at_boundary_accepted():
|
||
assert passes_filters([1, 14, 28, 42, 15], sum_range=(100, 100)) is True # sum=100
|
||
|
||
|
||
# ── sum_range_percentiles ─────────────────────────────────────────────────────
|
||
|
||
def test_sum_range_none_with_few_draws(tmp_db):
|
||
pb = get_game_by_name("Powerball")
|
||
for i in range(5):
|
||
insert_draw(pb["id"], f"2024-01-{i+1:02d}", [1, 2, 3, 4, i+5], bonus=1)
|
||
result = sum_range_percentiles(pb["id"])
|
||
assert result is None # < 10 draws
|
||
|
||
|
||
def test_sum_range_returns_tuple_with_enough_draws(tmp_db):
|
||
pb = get_game_by_name("Powerball")
|
||
for i in range(20):
|
||
insert_draw(pb["id"], f"2024-02-{i+1:02d}", [1+i, 2+i, 3+i, 4+i, 5+i], bonus=1)
|
||
result = sum_range_percentiles(pb["id"])
|
||
assert result is not None
|
||
lo, hi = result
|
||
assert lo < hi
|
||
assert isinstance(lo, int)
|
||
assert isinstance(hi, int)
|
||
|
||
|
||
def test_sum_range_bounds_reasonable(tmp_db):
|
||
pb = get_game_by_name("Powerball")
|
||
# Insert draws with sums ranging 15–25
|
||
for i in range(15, 26):
|
||
insert_draw(pb["id"], f"2024-03-{i-14:02d}", [1, 2, 3, 4, i-10], bonus=1)
|
||
lo, hi = sum_range_percentiles(pb["id"])
|
||
assert lo >= 10 # 10th pct of sums around 15
|
||
assert hi <= 25 # 90th pct of sums around 25
|
||
|
||
|
||
# ── _swap_filter ──────────────────────────────────────────────────────────────
|
||
|
||
def test_swap_filter_passes_already():
|
||
numbers = [1, 14, 28, 42, 60]
|
||
assert _swap_filter(numbers, [2, 3, 5], None) == numbers
|
||
|
||
|
||
def test_swap_filter_fixes_all_even():
|
||
# all-even → swap last (weakest-ranked) for an odd
|
||
numbers = [2, 14, 28, 42, 60]
|
||
pool = [1, 3, 5, 7, 9] # odd alternates
|
||
result = _swap_filter(numbers, pool, None)
|
||
assert passes_filters(result), f"Expected filtered result, got {result}"
|
||
assert len(result) == 5
|
||
assert len(set(result)) == 5
|
||
|
||
|
||
def test_swap_filter_fixes_four_consecutive():
|
||
numbers = [10, 11, 12, 13, 25]
|
||
pool = [1, 5, 8, 20, 30, 35]
|
||
result = _swap_filter(numbers, pool, None)
|
||
assert passes_filters(result), f"Expected filtered result, got {result}"
|
||
|
||
|
||
def test_swap_filter_graceful_fallback():
|
||
# If no swap fixes the ticket, return original
|
||
numbers = [2, 4, 6, 8, 10] # all-even
|
||
pool = [12, 14, 16] # all-even alternates — can't fix
|
||
result = _swap_filter(numbers, pool, None)
|
||
assert result == numbers # graceful fallback
|
||
|
||
|
||
# ── Filter integration in strategies ─────────────────────────────────────────
|
||
|
||
def _large_draw_set(game_id):
|
||
"""Insert 30 varied Powerball draws so sum_range_percentiles returns a range."""
|
||
import random as rng
|
||
rng.seed(42)
|
||
from db.models import get_game_by_id
|
||
game = get_game_by_id(game_id)
|
||
for i in range(30):
|
||
nums = sorted(rng.sample(range(1, game["main_max"] + 1), game["main_count"]))
|
||
bonus = rng.randint(1, game["bonus_max"]) if game["bonus_count"] > 0 else None
|
||
insert_draw(game_id, f"2020-{(i//30)+1:02d}-{(i%28)+1:02d}", nums, bonus=bonus)
|
||
|
||
|
||
def _assert_filtered(result, game):
|
||
nums = result["numbers"]
|
||
assert len(nums) == game["main_count"]
|
||
assert nums == sorted(nums)
|
||
assert len(set(nums)) == len(nums)
|
||
assert all(1 <= n <= game["main_max"] for n in nums)
|
||
assert not (len(nums) >= 4 and all(n % 2 == 0 for n in nums)), "all-even"
|
||
assert not (len(nums) >= 4 and all(n % 2 != 0 for n in nums)), "all-odd"
|
||
assert not _has_consecutive_run(nums, 4), "4+ consecutive"
|
||
|
||
|
||
def test_hot_numbers_filter_integration(tmp_db):
|
||
pb = get_game_by_name("Powerball")
|
||
_large_draw_set(pb["id"])
|
||
for _ in range(10):
|
||
_assert_filtered(hot_numbers(pb["id"]), pb)
|
||
|
||
|
||
def test_due_numbers_filter_integration(tmp_db):
|
||
pb = get_game_by_name("Powerball")
|
||
_large_draw_set(pb["id"])
|
||
for _ in range(10):
|
||
_assert_filtered(due_numbers(pb["id"]), pb)
|
||
|
||
|
||
def test_weighted_random_filter_integration(tmp_db):
|
||
pb = get_game_by_name("Powerball")
|
||
_large_draw_set(pb["id"])
|
||
for _ in range(20):
|
||
_assert_filtered(weighted_random(pb["id"]), pb)
|
||
|
||
|
||
def test_monte_carlo_filter_integration(tmp_db):
|
||
pb = get_game_by_name("Powerball")
|
||
_large_draw_set(pb["id"])
|
||
for _ in range(5):
|
||
_assert_filtered(monte_carlo(pb["id"], simulations=500), pb)
|
||
|
||
|
||
def test_quick_pick_filter_integration(tmp_db):
|
||
pb = get_game_by_name("Powerball")
|
||
_large_draw_set(pb["id"])
|
||
for _ in range(20):
|
||
_assert_filtered(quick_pick(pb["id"]), pb)
|
||
|
||
|
||
def test_positional_pick_filter_integration(tmp_db):
|
||
pb = get_game_by_name("Powerball")
|
||
_large_draw_set(pb["id"])
|
||
for _ in range(10):
|
||
_assert_filtered(positional_pick(pb["id"]), pb)
|