mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 23:50:12 +01:00
92 lines
3 KiB
Python
92 lines
3 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from code.deck_builder.theme_catalog_loader import ThemeCatalogEntry
|
|
from code.deck_builder.theme_matcher import (
|
|
ACCEPT_MATCH_THRESHOLD,
|
|
SUGGEST_MATCH_THRESHOLD,
|
|
ThemeMatcher,
|
|
normalize_theme,
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def sample_entries() -> list[ThemeCatalogEntry]:
|
|
themes = [
|
|
"Aristocrats",
|
|
"Sacrifice Matters",
|
|
"Life Gain",
|
|
"Token Swarm",
|
|
"Control",
|
|
"Superfriends",
|
|
"Spellslinger",
|
|
"Artifact Tokens",
|
|
"Treasure Storm",
|
|
"Graveyard Loops",
|
|
]
|
|
return [ThemeCatalogEntry(theme=theme, commander_count=0, card_count=0) for theme in themes]
|
|
|
|
|
|
def test_normalize_theme_collapses_spaces() -> None:
|
|
assert normalize_theme(" Life Gain \t") == "life gain"
|
|
|
|
|
|
def test_exact_match_case_insensitive(sample_entries: list[ThemeCatalogEntry]) -> None:
|
|
matcher = ThemeMatcher(sample_entries)
|
|
result = matcher.resolve("aristocrats")
|
|
assert result.matched_theme == "Aristocrats"
|
|
assert result.score == pytest.approx(100.0)
|
|
assert result.reason == "high_confidence"
|
|
|
|
|
|
def test_minor_typo_accepts_with_high_score(sample_entries: list[ThemeCatalogEntry]) -> None:
|
|
matcher = ThemeMatcher(sample_entries)
|
|
result = matcher.resolve("aristrocrats")
|
|
assert result.matched_theme == "Aristocrats"
|
|
assert result.score >= ACCEPT_MATCH_THRESHOLD
|
|
assert result.reason in {"high_confidence", "accepted_confidence"}
|
|
|
|
|
|
def test_multi_typo_only_suggests(sample_entries: list[ThemeCatalogEntry]) -> None:
|
|
matcher = ThemeMatcher(sample_entries)
|
|
result = matcher.resolve("arzstrcrats")
|
|
assert result.matched_theme is None
|
|
assert result.score >= SUGGEST_MATCH_THRESHOLD
|
|
assert result.reason == "suggestions"
|
|
assert any(s.theme == "Aristocrats" for s in result.suggestions)
|
|
|
|
|
|
def test_no_match_returns_empty(sample_entries: list[ThemeCatalogEntry]) -> None:
|
|
matcher = ThemeMatcher(sample_entries)
|
|
result = matcher.resolve("planeship")
|
|
assert result.matched_theme is None
|
|
assert result.suggestions == []
|
|
assert result.reason in {"no_candidates", "no_match"}
|
|
|
|
|
|
def test_short_input_requires_exact(sample_entries: list[ThemeCatalogEntry]) -> None:
|
|
matcher = ThemeMatcher(sample_entries)
|
|
result = matcher.resolve("ar")
|
|
assert result.matched_theme is None
|
|
assert result.reason == "input_too_short"
|
|
|
|
result_exact = matcher.resolve("lo")
|
|
assert result_exact.matched_theme is None
|
|
|
|
|
|
def test_resolution_speed(sample_entries: list[ThemeCatalogEntry]) -> None:
|
|
many_entries = [
|
|
ThemeCatalogEntry(theme=f"Theme {i}", commander_count=0, card_count=0) for i in range(400)
|
|
]
|
|
matcher = ThemeMatcher(many_entries)
|
|
matcher.resolve("theme 42")
|
|
|
|
start = time.perf_counter()
|
|
for _ in range(20):
|
|
matcher.resolve("theme 123")
|
|
duration = time.perf_counter() - start
|
|
# Observed ~0.03s per resolution (<=0.65s for 20 resolves) on dev machine (2025-10-02).
|
|
assert duration < 0.7
|