mtg_python_deckbuilder/code/tests/test_include_exclude_validation.py

270 lines
10 KiB
Python

"""
Unit tests for include/exclude card validation and processing functionality.
Tests schema integration, validation utilities, fuzzy matching, strict enforcement,
and JSON export behavior for the include/exclude card system.
"""
import pytest
import json
import tempfile
from deck_builder.builder import DeckBuilder
from deck_builder.include_exclude_utils import (
IncludeExcludeDiagnostics,
validate_list_sizes,
collapse_duplicates,
parse_card_list_input
)
class TestIncludeExcludeSchema:
"""Test that DeckBuilder properly supports include/exclude configuration."""
def test_default_values(self):
"""Test that DeckBuilder has correct default values for include/exclude fields."""
builder = DeckBuilder()
assert builder.include_cards == []
assert builder.exclude_cards == []
assert builder.enforcement_mode == "warn"
assert builder.allow_illegal is False
assert builder.fuzzy_matching is True
assert builder.include_exclude_diagnostics is None
def test_field_assignment(self):
"""Test that include/exclude fields can be assigned."""
builder = DeckBuilder()
builder.include_cards = ["Sol Ring", "Lightning Bolt"]
builder.exclude_cards = ["Chaos Orb", "Shaharazad"]
builder.enforcement_mode = "strict"
builder.allow_illegal = True
builder.fuzzy_matching = False
assert builder.include_cards == ["Sol Ring", "Lightning Bolt"]
assert builder.exclude_cards == ["Chaos Orb", "Shaharazad"]
assert builder.enforcement_mode == "strict"
assert builder.allow_illegal is True
assert builder.fuzzy_matching is False
class TestProcessIncludesExcludes:
"""Test the _process_includes_excludes method."""
def test_basic_processing(self):
"""Test basic include/exclude processing."""
builder = DeckBuilder()
builder.include_cards = ["Sol Ring", "Lightning Bolt"]
builder.exclude_cards = ["Chaos Orb"]
# Mock output function to capture messages
output_messages = []
builder.output_func = lambda msg: output_messages.append(msg)
diagnostics = builder._process_includes_excludes()
assert isinstance(diagnostics, IncludeExcludeDiagnostics)
assert builder.include_exclude_diagnostics is not None
def test_duplicate_collapse(self):
"""Test that duplicates are properly collapsed."""
builder = DeckBuilder()
builder.include_cards = ["Sol Ring", "Sol Ring", "Lightning Bolt"]
builder.exclude_cards = ["Chaos Orb", "Chaos Orb", "Chaos Orb"]
output_messages = []
builder.output_func = lambda msg: output_messages.append(msg)
diagnostics = builder._process_includes_excludes()
# After processing, duplicates should be removed
assert builder.include_cards == ["Sol Ring", "Lightning Bolt"]
assert builder.exclude_cards == ["Chaos Orb"]
# Duplicates should be tracked in diagnostics
assert diagnostics.duplicates_collapsed["Sol Ring"] == 2
assert diagnostics.duplicates_collapsed["Chaos Orb"] == 3
def test_exclude_overrides_include(self):
"""Test that exclude takes precedence over include."""
builder = DeckBuilder()
builder.include_cards = ["Sol Ring", "Lightning Bolt"]
builder.exclude_cards = ["Sol Ring"] # Sol Ring appears in both lists
output_messages = []
builder.output_func = lambda msg: output_messages.append(msg)
diagnostics = builder._process_includes_excludes()
# Sol Ring should be removed from includes due to exclude precedence
assert "Sol Ring" not in builder.include_cards
assert "Lightning Bolt" in builder.include_cards
assert "Sol Ring" in diagnostics.excluded_removed
class TestValidationUtilities:
"""Test the validation utility functions."""
def test_list_size_validation_valid(self):
"""Test list size validation with valid sizes."""
includes = ["Card A", "Card B"]
excludes = ["Card X", "Card Y", "Card Z"]
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_list_size_validation_approaching_limit(self):
"""Test list size validation warnings when approaching limits."""
includes = ["Card"] * 8 # 80% of 10 = 8
excludes = ["Card"] * 12 # 80% of 15 = 12
result = validate_list_sizes(includes, excludes)
assert result['valid'] is True # Still valid, just warnings
assert 'includes_approaching_limit' in result['warnings']
assert 'excludes_approaching_limit' in result['warnings']
def test_list_size_validation_over_limit(self):
"""Test list size validation errors when over limits."""
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]
def test_collapse_duplicates(self):
"""Test duplicate collapse functionality."""
card_names = ["Sol Ring", "Lightning Bolt", "Sol Ring", "Counterspell", "Lightning Bolt", "Lightning Bolt"]
unique_names, duplicates = collapse_duplicates(card_names)
assert len(unique_names) == 3
assert "Sol Ring" in unique_names
assert "Lightning Bolt" in unique_names
assert "Counterspell" in unique_names
assert duplicates["Sol Ring"] == 2
assert duplicates["Lightning Bolt"] == 3
assert "Counterspell" not in duplicates # Only appeared once
def test_parse_card_list_input_newlines(self):
"""Test parsing card list input with newlines."""
input_text = "Sol Ring\nLightning Bolt\nCounterspell"
result = parse_card_list_input(input_text)
assert result == ["Sol Ring", "Lightning Bolt", "Counterspell"]
def test_parse_card_list_input_commas(self):
"""Test parsing card list input with commas (when no newlines)."""
input_text = "Sol Ring, Lightning Bolt, Counterspell"
result = parse_card_list_input(input_text)
assert result == ["Sol Ring", "Lightning Bolt", "Counterspell"]
def test_parse_card_list_input_mixed_prefers_newlines(self):
"""Test that newlines take precedence over commas to avoid splitting names with commas."""
input_text = "Sol Ring\nKrenko, Mob Boss\nLightning Bolt"
result = parse_card_list_input(input_text)
# Should not split "Krenko, Mob Boss" because newlines are present
assert result == ["Sol Ring", "Krenko, Mob Boss", "Lightning Bolt"]
class TestStrictEnforcement:
"""Test strict enforcement functionality."""
def test_strict_enforcement_with_missing_includes(self):
"""Test that strict mode raises error when includes are missing."""
builder = DeckBuilder()
builder.enforcement_mode = "strict"
builder.include_exclude_diagnostics = {
'missing_includes': ['Missing Card'],
'ignored_color_identity': [],
'illegal_dropped': [],
'illegal_allowed': [],
'excluded_removed': [],
'duplicates_collapsed': {},
'include_added': [],
'include_over_ideal': {},
'fuzzy_corrections': {},
'confirmation_needed': [],
'list_size_warnings': {}
}
with pytest.raises(RuntimeError, match="Strict mode: Failed to include required cards: Missing Card"):
builder._enforce_includes_strict()
def test_strict_enforcement_with_no_missing_includes(self):
"""Test that strict mode passes when all includes are present."""
builder = DeckBuilder()
builder.enforcement_mode = "strict"
builder.include_exclude_diagnostics = {
'missing_includes': [],
'ignored_color_identity': [],
'illegal_dropped': [],
'illegal_allowed': [],
'excluded_removed': [],
'duplicates_collapsed': {},
'include_added': ['Sol Ring'],
'include_over_ideal': {},
'fuzzy_corrections': {},
'confirmation_needed': [],
'list_size_warnings': {}
}
# Should not raise any exception
builder._enforce_includes_strict()
def test_warn_mode_does_not_enforce(self):
"""Test that warn mode does not raise errors."""
builder = DeckBuilder()
builder.enforcement_mode = "warn"
builder.include_exclude_diagnostics = {
'missing_includes': ['Missing Card'],
}
# Should not raise any exception
builder._enforce_includes_strict()
class TestJSONRoundTrip:
"""Test JSON export/import round-trip functionality."""
def test_json_export_includes_new_fields(self):
"""Test that JSON export includes include/exclude fields."""
builder = DeckBuilder()
builder.include_cards = ["Sol Ring", "Lightning Bolt"]
builder.exclude_cards = ["Chaos Orb"]
builder.enforcement_mode = "strict"
builder.allow_illegal = True
builder.fuzzy_matching = False
# Create temporary directory for export
with tempfile.TemporaryDirectory() as temp_dir:
json_path = builder.export_run_config_json(directory=temp_dir, suppress_output=True)
# Read the exported JSON
with open(json_path, 'r', encoding='utf-8') as f:
exported_data = json.load(f)
# Verify include/exclude fields are present
assert exported_data['include_cards'] == ["Sol Ring", "Lightning Bolt"]
assert exported_data['exclude_cards'] == ["Chaos Orb"]
assert exported_data['enforcement_mode'] == "strict"
assert exported_data['allow_illegal'] is True
assert exported_data['fuzzy_matching'] is False
if __name__ == "__main__":
pytest.main([__file__])