mtg_python_deckbuilder/code/tests/test_include_exclude_utils.py

283 lines
11 KiB
Python

"""
Unit tests for include/exclude utilities.
Tests the fuzzy matching, normalization, and validation functions
that support the must-include/must-exclude feature.
"""
import pytest
from typing import Set
from deck_builder.include_exclude_utils import (
normalize_card_name,
normalize_punctuation,
fuzzy_match_card_name,
validate_list_sizes,
collapse_duplicates,
parse_card_list_input,
get_baseline_performance_metrics,
FuzzyMatchResult,
FUZZY_CONFIDENCE_THRESHOLD,
MAX_INCLUDES,
MAX_EXCLUDES
)
class TestNormalization:
"""Test card name normalization functions."""
def test_normalize_card_name_basic(self):
"""Test basic name normalization."""
assert normalize_card_name("Lightning Bolt") == "lightning bolt"
assert normalize_card_name(" Sol Ring ") == "sol ring"
assert normalize_card_name("") == ""
def test_normalize_card_name_unicode(self):
"""Test unicode character normalization."""
# Curly apostrophe to straight
assert normalize_card_name("Thassa's Oracle") == "thassa's oracle"
# Test case from combo tag applier
assert normalize_card_name("Thassa\u2019s Oracle") == "thassa's oracle"
def test_normalize_card_name_arena_prefix(self):
"""Test Arena/Alchemy prefix removal."""
assert normalize_card_name("A-Lightning Bolt") == "lightning bolt"
assert normalize_card_name("A-") == "a-" # Edge case: too short
def test_normalize_punctuation_commas(self):
"""Test punctuation normalization for commas."""
assert normalize_punctuation("Krenko, Mob Boss") == "krenko mob boss"
assert normalize_punctuation("Krenko Mob Boss") == "krenko mob boss"
# Should be equivalent for fuzzy matching
assert (normalize_punctuation("Krenko, Mob Boss") ==
normalize_punctuation("Krenko Mob Boss"))
class TestFuzzyMatching:
"""Test fuzzy card name matching."""
@pytest.fixture
def sample_card_names(self) -> Set[str]:
"""Sample card names for testing."""
return {
"Lightning Bolt",
"Lightning Strike",
"Lightning Helix",
"Krenko, Mob Boss",
"Sol Ring",
"Thassa's Oracle",
"Demonic Consultation"
}
def test_exact_match(self, sample_card_names):
"""Test exact name matching."""
result = fuzzy_match_card_name("Lightning Bolt", sample_card_names)
assert result.matched_name == "Lightning Bolt"
assert result.confidence == 1.0
assert result.auto_accepted is True
assert len(result.suggestions) == 0
def test_exact_match_after_normalization(self, sample_card_names):
"""Test exact match after punctuation normalization."""
result = fuzzy_match_card_name("Krenko Mob Boss", sample_card_names)
assert result.matched_name == "Krenko, Mob Boss"
assert result.confidence == 1.0
assert result.auto_accepted is True
def test_typo_suggestion(self, sample_card_names):
"""Test typo suggestions."""
result = fuzzy_match_card_name("Lightnig Bolt", sample_card_names)
assert "Lightning Bolt" in result.suggestions
# Should have high confidence but maybe not auto-accepted depending on threshold
assert result.confidence > 0.8
def test_ambiguous_match(self, sample_card_names):
"""Test ambiguous input requiring confirmation."""
result = fuzzy_match_card_name("Lightning", sample_card_names)
# Should return multiple lightning-related suggestions
lightning_suggestions = [s for s in result.suggestions if "Lightning" in s]
assert len(lightning_suggestions) >= 2
def test_no_match(self, sample_card_names):
"""Test input with no reasonable matches."""
result = fuzzy_match_card_name("Completely Invalid Card", sample_card_names)
assert result.matched_name is None
assert result.confidence == 0.0
assert result.auto_accepted is False
def test_empty_input(self, sample_card_names):
"""Test empty input handling."""
result = fuzzy_match_card_name("", sample_card_names)
assert result.matched_name is None
assert result.confidence == 0.0
assert result.auto_accepted is False
class TestValidation:
"""Test validation functions."""
def test_validate_list_sizes_valid(self):
"""Test validation with acceptable list sizes."""
includes = ["Card A", "Card B"] # Well under limit
excludes = ["Card X", "Card Y", "Card Z"] # Well under limit
result = validate_list_sizes(includes, excludes)
assert result['valid'] is True
assert len(result['errors']) == 0
assert result['counts']['includes'] == 2
assert result['counts']['excludes'] == 3
def test_validate_list_sizes_warnings(self):
"""Test warning thresholds."""
includes = ["Card"] * 8 # 80% of 10 = 8, should trigger warning
excludes = ["Card"] * 12 # 80% of 15 = 12, should trigger warning
result = validate_list_sizes(includes, excludes)
assert result['valid'] is True
assert 'includes_approaching_limit' in result['warnings']
assert 'excludes_approaching_limit' in result['warnings']
def test_validate_list_sizes_errors(self):
"""Test size limit errors."""
includes = ["Card"] * 15 # Over limit of 10
excludes = ["Card"] * 20 # Over limit of 15
result = validate_list_sizes(includes, excludes)
assert result['valid'] is False
assert len(result['errors']) == 2
assert "Too many include cards" in result['errors'][0]
assert "Too many exclude cards" in result['errors'][1]
class TestDuplicateCollapse:
"""Test duplicate handling."""
def test_collapse_duplicates_basic(self):
"""Test basic duplicate removal."""
names = ["Lightning Bolt", "Sol Ring", "Lightning Bolt"]
unique, duplicates = collapse_duplicates(names)
assert len(unique) == 2
assert "Lightning Bolt" in unique
assert "Sol Ring" in unique
assert duplicates["Lightning Bolt"] == 2
def test_collapse_duplicates_case_insensitive(self):
"""Test case-insensitive duplicate detection."""
names = ["Lightning Bolt", "LIGHTNING BOLT", "lightning bolt"]
unique, duplicates = collapse_duplicates(names)
assert len(unique) == 1
assert duplicates[unique[0]] == 3
def test_collapse_duplicates_empty(self):
"""Test empty input."""
unique, duplicates = collapse_duplicates([])
assert unique == []
assert duplicates == {}
def test_collapse_duplicates_whitespace(self):
"""Test whitespace handling."""
names = ["Lightning Bolt", " Lightning Bolt ", "", " "]
unique, duplicates = collapse_duplicates(names)
assert len(unique) == 1
assert duplicates[unique[0]] == 2
class TestInputParsing:
"""Test input parsing functions."""
def test_parse_card_list_newlines(self):
"""Test newline-separated input."""
input_text = "Lightning Bolt\nSol Ring\nKrenko, Mob Boss"
result = parse_card_list_input(input_text)
assert len(result) == 3
assert "Lightning Bolt" in result
assert "Sol Ring" in result
assert "Krenko, Mob Boss" in result
def test_parse_card_list_commas(self):
"""Test comma-separated input (no newlines)."""
input_text = "Lightning Bolt, Sol Ring, Thassa's Oracle"
result = parse_card_list_input(input_text)
assert len(result) == 3
assert "Lightning Bolt" in result
assert "Sol Ring" in result
assert "Thassa's Oracle" in result
def test_parse_card_list_commas_in_names(self):
"""Test that commas in card names are preserved when using newlines."""
input_text = "Krenko, Mob Boss\nFinneas, Ace Archer"
result = parse_card_list_input(input_text)
assert len(result) == 2
assert "Krenko, Mob Boss" in result
assert "Finneas, Ace Archer" in result
def test_parse_card_list_mixed(self):
"""Test that newlines take precedence over commas."""
# When both separators present, newlines take precedence
input_text = "Lightning Bolt\nKrenko, Mob Boss\nThassa's Oracle"
result = parse_card_list_input(input_text)
assert len(result) == 3
assert "Lightning Bolt" in result
assert "Krenko, Mob Boss" in result # Comma preserved in name
assert "Thassa's Oracle" in result
def test_parse_card_list_empty(self):
"""Test empty input."""
assert parse_card_list_input("") == []
assert parse_card_list_input(" ") == []
assert parse_card_list_input("\n\n\n") == []
assert parse_card_list_input(" , , ") == []
class TestPerformance:
"""Test performance measurement functions."""
def test_baseline_performance_metrics(self):
"""Test baseline performance measurement."""
metrics = get_baseline_performance_metrics()
assert 'normalization_time_ms' in metrics
assert 'operations_count' in metrics
assert 'timestamp' in metrics
# Should be reasonably fast
assert metrics['normalization_time_ms'] < 1000 # Less than 1 second
assert metrics['operations_count'] > 0
class TestFeatureFlagIntegration:
"""Test feature flag integration."""
def test_constants_defined(self):
"""Test that required constants are properly defined."""
assert isinstance(FUZZY_CONFIDENCE_THRESHOLD, float)
assert 0.0 <= FUZZY_CONFIDENCE_THRESHOLD <= 1.0
assert isinstance(MAX_INCLUDES, int)
assert MAX_INCLUDES > 0
assert isinstance(MAX_EXCLUDES, int)
assert MAX_EXCLUDES > 0
def test_fuzzy_match_result_structure(self):
"""Test FuzzyMatchResult dataclass structure."""
result = FuzzyMatchResult(
input_name="test",
matched_name="Test Card",
confidence=0.95,
suggestions=["Test Card", "Other Card"],
auto_accepted=True
)
assert result.input_name == "test"
assert result.matched_name == "Test Card"
assert result.confidence == 0.95
assert len(result.suggestions) == 2
assert result.auto_accepted is True