mtg_python_deckbuilder/code/tests/test_exclude_cards_performance.py

144 lines
5.1 KiB
Python

"""
Exclude Cards Performance Tests
Ensures that exclude filtering doesn't create significant performance
regressions and meets the specified benchmarks for parsing, filtering,
and validation operations.
"""
import time
import pytest
from deck_builder.include_exclude_utils import parse_card_list_input
def test_card_parsing_speed():
"""Test that exclude card parsing is fast."""
# Create a list of 15 cards (max excludes)
exclude_cards_text = "\n".join([
"Sol Ring", "Rhystic Study", "Smothering Tithe", "Lightning Bolt",
"Counterspell", "Swords to Plowshares", "Path to Exile",
"Mystical Tutor", "Demonic Tutor", "Vampiric Tutor",
"Mana Crypt", "Chrome Mox", "Mox Diamond", "Mox Opal", "Lotus Petal"
])
# Time the parsing operation
start_time = time.time()
for _ in range(100): # Run 100 times to get a meaningful measurement
result = parse_card_list_input(exclude_cards_text)
end_time = time.time()
# Should complete 100 parses in well under 1 second
total_time = end_time - start_time
avg_time_per_parse = total_time / 100
assert len(result) == 15
assert avg_time_per_parse < 0.01 # Less than 10ms per parse (very generous)
print(f"Average parse time: {avg_time_per_parse*1000:.2f}ms")
def test_large_cardpool_filtering_speed():
"""Simulate exclude filtering performance on a large card pool."""
# Create a mock dataframe-like structure to simulate filtering
mock_card_pool_size = 20000 # Typical large card pool
exclude_list = [
"Sol Ring", "Rhystic Study", "Smothering Tithe", "Lightning Bolt",
"Counterspell", "Swords to Plowshares", "Path to Exile",
"Mystical Tutor", "Demonic Tutor", "Vampiric Tutor",
"Mana Crypt", "Chrome Mox", "Mox Diamond", "Mox Opal", "Lotus Petal"
]
# Simulate the filtering operation (set-based lookup)
exclude_set = set(exclude_list)
# Create mock card names
mock_cards = [f"Card {i}" for i in range(mock_card_pool_size)]
# Add a few cards that will be excluded
mock_cards.extend(exclude_list)
# Time the filtering operation
start_time = time.time()
filtered_cards = [card for card in mock_cards if card not in exclude_set]
end_time = time.time()
filter_time = end_time - start_time
# Should complete filtering in well under 50ms (our target)
assert filter_time < 0.050 # 50ms
print(f"Filtering {len(mock_cards)} cards took {filter_time*1000:.2f}ms")
# Verify filtering worked
for excluded_card in exclude_list:
assert excluded_card not in filtered_cards
def test_validation_api_response_time():
"""Test validation endpoint response time."""
import importlib
import os
import sys
from starlette.testclient import TestClient
# Ensure project root is in sys.path for reliable imports
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
if project_root not in sys.path:
sys.path.insert(0, project_root)
# Enable feature flag
original_value = os.environ.get('ALLOW_MUST_HAVES')
os.environ['ALLOW_MUST_HAVES'] = '1'
try:
# Fresh import
try:
del importlib.sys.modules['code.web.app']
except KeyError:
pass
app_module = importlib.import_module('code.web.app')
client = TestClient(app_module.app)
# Test data
exclude_text = "\n".join([
"Sol Ring", "Rhystic Study", "Smothering Tithe", "Lightning Bolt",
"Counterspell", "Swords to Plowshares", "Path to Exile",
"Mystical Tutor", "Demonic Tutor", "Vampiric Tutor"
])
# Time the validation request
start_time = time.time()
response = client.post('/build/validate/exclude_cards',
data={'exclude_cards': exclude_text})
end_time = time.time()
response_time = end_time - start_time
# Should respond in under 100ms (our target)
assert response_time < 0.100 # 100ms
assert response.status_code == 200
print(f"Validation endpoint response time: {response_time*1000:.2f}ms")
finally:
# Restore environment
if original_value is not None:
os.environ['ALLOW_MUST_HAVES'] = original_value
else:
os.environ.pop('ALLOW_MUST_HAVES', None)
@pytest.mark.parametrize("exclude_count", [0, 5, 10, 15])
def test_parsing_scales_with_list_size(exclude_count):
"""Test that performance scales reasonably with number of excludes."""
exclude_cards = [f"Exclude Card {i}" for i in range(exclude_count)]
exclude_text = "\n".join(exclude_cards)
start_time = time.time()
result = parse_card_list_input(exclude_text)
end_time = time.time()
parse_time = end_time - start_time
# Even with maximum excludes, should be very fast
assert parse_time < 0.005 # 5ms
assert len(result) == exclude_count
print(f"Parse time for {exclude_count} excludes: {parse_time*1000:.2f}ms")