Files

224 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 1525
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)