mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 23:50:12 +01:00
278 lines
12 KiB
Python
278 lines
12 KiB
Python
"""
|
|
Test JSON persistence functionality for include/exclude configuration.
|
|
|
|
Verifies that include/exclude configurations can be exported to JSON and then imported
|
|
back with full fidelity, supporting the persistence layer of the include/exclude system.
|
|
"""
|
|
|
|
import json
|
|
import hashlib
|
|
import tempfile
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from headless_runner import _load_json_config
|
|
from deck_builder.builder import DeckBuilder
|
|
|
|
|
|
class TestJSONRoundTrip:
|
|
"""Test complete JSON export/import round-trip for include/exclude config."""
|
|
|
|
def test_complete_round_trip(self):
|
|
"""Test that a complete config can be exported and re-imported correctly."""
|
|
# Create initial configuration
|
|
original_config = {
|
|
"commander": "Aang, Airbending Master",
|
|
"primary_tag": "Exile Matters",
|
|
"secondary_tag": "Airbending",
|
|
"tertiary_tag": "Token Creation",
|
|
"bracket_level": 4,
|
|
"use_multi_theme": True,
|
|
"add_lands": True,
|
|
"add_creatures": True,
|
|
"add_non_creature_spells": True,
|
|
"fetch_count": 3,
|
|
"ideal_counts": {
|
|
"ramp": 8,
|
|
"lands": 35,
|
|
"basic_lands": 15,
|
|
"creatures": 25,
|
|
"removal": 10,
|
|
"wipes": 2,
|
|
"card_advantage": 10,
|
|
"protection": 8
|
|
},
|
|
"include_cards": ["Sol Ring", "Lightning Bolt", "Counterspell"],
|
|
"exclude_cards": ["Chaos Orb", "Shahrazad", "Time Walk"],
|
|
"enforcement_mode": "strict",
|
|
"allow_illegal": True,
|
|
"fuzzy_matching": False,
|
|
"secondary_commander": "Alena, Kessig Trapper",
|
|
"background": None,
|
|
"enable_partner_mechanics": True,
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Write initial config
|
|
config_path = os.path.join(temp_dir, "test_config.json")
|
|
with open(config_path, 'w', encoding='utf-8') as f:
|
|
json.dump(original_config, f, indent=2)
|
|
|
|
# Load config using headless runner logic
|
|
loaded_config = _load_json_config(config_path)
|
|
|
|
# Verify all include/exclude fields are preserved
|
|
assert loaded_config["include_cards"] == ["Sol Ring", "Lightning Bolt", "Counterspell"]
|
|
assert loaded_config["exclude_cards"] == ["Chaos Orb", "Shahrazad", "Time Walk"]
|
|
assert loaded_config["enforcement_mode"] == "strict"
|
|
assert loaded_config["allow_illegal"] is True
|
|
assert loaded_config["fuzzy_matching"] is False
|
|
assert loaded_config["secondary_commander"] == "Alena, Kessig Trapper"
|
|
assert loaded_config["background"] is None
|
|
assert loaded_config["enable_partner_mechanics"] is True
|
|
|
|
# Create a DeckBuilder with this config and export again
|
|
builder = DeckBuilder()
|
|
builder.commander_name = loaded_config["commander"]
|
|
builder.include_cards = loaded_config["include_cards"]
|
|
builder.exclude_cards = loaded_config["exclude_cards"]
|
|
builder.enforcement_mode = loaded_config["enforcement_mode"]
|
|
builder.allow_illegal = loaded_config["allow_illegal"]
|
|
builder.fuzzy_matching = loaded_config["fuzzy_matching"]
|
|
builder.bracket_level = loaded_config["bracket_level"]
|
|
builder.partner_feature_enabled = loaded_config["enable_partner_mechanics"]
|
|
builder.partner_mode = "partner"
|
|
builder.secondary_commander = loaded_config["secondary_commander"]
|
|
builder.requested_secondary_commander = loaded_config["secondary_commander"]
|
|
|
|
# Export the configuration
|
|
exported_path = builder.export_run_config_json(directory=temp_dir, suppress_output=True)
|
|
|
|
# Load the exported config
|
|
with open(exported_path, 'r', encoding='utf-8') as f:
|
|
re_exported_config = json.load(f)
|
|
|
|
# Verify round-trip fidelity for include/exclude fields
|
|
assert re_exported_config["include_cards"] == ["Sol Ring", "Lightning Bolt", "Counterspell"]
|
|
assert re_exported_config["exclude_cards"] == ["Chaos Orb", "Shahrazad", "Time Walk"]
|
|
assert re_exported_config["enforcement_mode"] == "strict"
|
|
assert re_exported_config["allow_illegal"] is True
|
|
assert re_exported_config["fuzzy_matching"] is False
|
|
assert re_exported_config["additional_themes"] == []
|
|
assert re_exported_config["theme_match_mode"] == "permissive"
|
|
assert re_exported_config["theme_catalog_version"] is None
|
|
assert re_exported_config["userThemes"] == []
|
|
assert re_exported_config["themeCatalogVersion"] is None
|
|
assert re_exported_config["secondary_commander"] == "Alena, Kessig Trapper"
|
|
assert re_exported_config["background"] is None
|
|
assert re_exported_config["enable_partner_mechanics"] is True
|
|
|
|
def test_empty_lists_round_trip(self):
|
|
"""Test that empty include/exclude lists are handled correctly."""
|
|
builder = DeckBuilder()
|
|
builder.commander_name = "Test Commander"
|
|
builder.include_cards = []
|
|
builder.exclude_cards = []
|
|
builder.enforcement_mode = "warn"
|
|
builder.allow_illegal = False
|
|
builder.fuzzy_matching = True
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Export configuration
|
|
exported_path = builder.export_run_config_json(directory=temp_dir, suppress_output=True)
|
|
|
|
# Load the exported config
|
|
with open(exported_path, 'r', encoding='utf-8') as f:
|
|
exported_config = json.load(f)
|
|
|
|
# Verify empty lists are preserved (not None)
|
|
assert exported_config["include_cards"] == []
|
|
assert exported_config["exclude_cards"] == []
|
|
assert exported_config["enforcement_mode"] == "warn"
|
|
assert exported_config["allow_illegal"] is False
|
|
assert exported_config["fuzzy_matching"] is True
|
|
assert exported_config["userThemes"] == []
|
|
assert exported_config["themeCatalogVersion"] is None
|
|
assert exported_config["secondary_commander"] is None
|
|
assert exported_config["background"] is None
|
|
assert exported_config["enable_partner_mechanics"] is False
|
|
|
|
def test_default_values_export(self):
|
|
"""Test that default values are exported correctly."""
|
|
builder = DeckBuilder()
|
|
# Only set commander, leave everything else as defaults
|
|
builder.commander_name = "Test Commander"
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Export configuration
|
|
exported_path = builder.export_run_config_json(directory=temp_dir, suppress_output=True)
|
|
|
|
# Load the exported config
|
|
with open(exported_path, 'r', encoding='utf-8') as f:
|
|
exported_config = json.load(f)
|
|
|
|
# Verify default values are exported
|
|
assert exported_config["include_cards"] == []
|
|
assert exported_config["exclude_cards"] == []
|
|
assert exported_config["enforcement_mode"] == "warn"
|
|
assert exported_config["allow_illegal"] is False
|
|
assert exported_config["fuzzy_matching"] is True
|
|
assert exported_config["additional_themes"] == []
|
|
assert exported_config["theme_match_mode"] == "permissive"
|
|
assert exported_config["theme_catalog_version"] is None
|
|
assert exported_config["secondary_commander"] is None
|
|
assert exported_config["background"] is None
|
|
assert exported_config["enable_partner_mechanics"] is False
|
|
|
|
def test_backward_compatibility_no_include_exclude_fields(self):
|
|
"""Test that configs without include/exclude fields still work."""
|
|
legacy_config = {
|
|
"commander": "Legacy Commander",
|
|
"primary_tag": "Legacy Tag",
|
|
"bracket_level": 3,
|
|
"ideal_counts": {
|
|
"ramp": 8,
|
|
"lands": 35
|
|
}
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Write legacy config (no include/exclude fields)
|
|
config_path = os.path.join(temp_dir, "legacy_config.json")
|
|
with open(config_path, 'w', encoding='utf-8') as f:
|
|
json.dump(legacy_config, f, indent=2)
|
|
|
|
# Load config using headless runner logic
|
|
loaded_config = _load_json_config(config_path)
|
|
|
|
# Verify legacy fields are preserved
|
|
assert loaded_config["commander"] == "Legacy Commander"
|
|
assert loaded_config["primary_tag"] == "Legacy Tag"
|
|
assert loaded_config["bracket_level"] == 3
|
|
|
|
# Verify include/exclude fields are not present (will use defaults)
|
|
assert "include_cards" not in loaded_config
|
|
assert "exclude_cards" not in loaded_config
|
|
assert "enforcement_mode" not in loaded_config
|
|
assert "allow_illegal" not in loaded_config
|
|
assert "fuzzy_matching" not in loaded_config
|
|
assert "additional_themes" not in loaded_config
|
|
assert "theme_match_mode" not in loaded_config
|
|
assert "theme_catalog_version" not in loaded_config
|
|
assert "userThemes" not in loaded_config
|
|
assert "themeCatalogVersion" not in loaded_config
|
|
|
|
def test_export_backward_compatibility_hash(self):
|
|
"""Ensure exports without user themes remain hash-compatible with legacy payload."""
|
|
builder = DeckBuilder()
|
|
builder.commander_name = "Test Commander"
|
|
builder.include_cards = ["Sol Ring"]
|
|
builder.exclude_cards = []
|
|
builder.enforcement_mode = "warn"
|
|
builder.allow_illegal = False
|
|
builder.fuzzy_matching = True
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
exported_path = builder.export_run_config_json(directory=temp_dir, suppress_output=True)
|
|
|
|
with open(exported_path, 'r', encoding='utf-8') as f:
|
|
exported_config = json.load(f)
|
|
|
|
legacy_expected = {
|
|
"commander": "Test Commander",
|
|
"primary_tag": None,
|
|
"secondary_tag": None,
|
|
"tertiary_tag": None,
|
|
"bracket_level": None,
|
|
"tag_mode": "AND",
|
|
"use_multi_theme": True,
|
|
"add_lands": True,
|
|
"add_creatures": True,
|
|
"add_non_creature_spells": True,
|
|
"prefer_combos": False,
|
|
"combo_target_count": None,
|
|
"combo_balance": None,
|
|
"include_cards": ["Sol Ring"],
|
|
"exclude_cards": [],
|
|
"enforcement_mode": "warn",
|
|
"allow_illegal": False,
|
|
"fuzzy_matching": True,
|
|
"additional_themes": [],
|
|
"theme_match_mode": "permissive",
|
|
"theme_catalog_version": None,
|
|
"fetch_count": None,
|
|
"ideal_counts": {},
|
|
}
|
|
|
|
sanitized_payload = {k: exported_config.get(k) for k in legacy_expected.keys()}
|
|
|
|
assert sanitized_payload == legacy_expected
|
|
assert exported_config["userThemes"] == []
|
|
assert exported_config["themeCatalogVersion"] is None
|
|
|
|
legacy_hash = hashlib.sha256(json.dumps(legacy_expected, sort_keys=True).encode("utf-8")).hexdigest()
|
|
sanitized_hash = hashlib.sha256(json.dumps(sanitized_payload, sort_keys=True).encode("utf-8")).hexdigest()
|
|
assert sanitized_hash == legacy_hash
|
|
|
|
def test_export_background_fields(self):
|
|
builder = DeckBuilder()
|
|
builder.commander_name = "Test Commander"
|
|
builder.partner_feature_enabled = True
|
|
builder.partner_mode = "background"
|
|
builder.secondary_commander = "Scion of Halaster"
|
|
builder.requested_background = "Scion of Halaster"
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
exported_path = builder.export_run_config_json(directory=temp_dir, suppress_output=True)
|
|
|
|
with open(exported_path, 'r', encoding='utf-8') as f:
|
|
exported_config = json.load(f)
|
|
|
|
assert exported_config["enable_partner_mechanics"] is True
|
|
assert exported_config["background"] == "Scion of Halaster"
|
|
assert exported_config["secondary_commander"] is None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__])
|