mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
270 lines
10 KiB
Python
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__])
|