Files
web-page-backend/saju-lab/tests/test_shinsal.py

157 lines
5.7 KiB
Python

"""shinsal.py — 지장간/신살/공망/지지 상호작용 검증.
fixtures/reference_saju.json 의 30 case (TS) 와 Python 구현 1:1 일치 검증.
"""
import json
from pathlib import Path
import pytest
from app.calculator.core import calculate_saju
from app.calculator.shinsal import (
get_hidden_stems,
get_all_hidden_stems,
analyze_branch_interactions,
calculate_shinsal,
calculate_gongmang,
)
REF_PATH = Path(__file__).parent / "fixtures" / "reference_saju.json"
REF = json.loads(REF_PATH.read_text(encoding="utf-8"))
def _camel_to_snake(name: str) -> str:
out = []
for ch in name:
if ch.isupper():
out.append("_" + ch.lower())
else:
out.append(ch)
return "".join(out)
def _normalize(d):
"""TS camelCase → Python snake_case (deep). 값은 변경하지 않음."""
if isinstance(d, dict):
return {_camel_to_snake(k): _normalize(v) for k, v in d.items()}
if isinstance(d, list):
return [_normalize(x) for x in d]
return d
def _canonical(item):
"""dict/list 비교용 — 안정 정렬을 위한 JSON 직렬화."""
return json.dumps(item, ensure_ascii=False, sort_keys=True)
# ─── 1. 지장간 단순 테스트 ────────────────────────────────────────────
def test_hidden_stems_basic():
assert get_hidden_stems("") == [""]
assert get_hidden_stems("") == ["", "", ""]
assert get_hidden_stems("") == ["", "", ""]
assert get_hidden_stems("") == [""]
assert get_hidden_stems("") == ["", "", ""]
assert get_hidden_stems("") == ["", "", ""]
assert get_hidden_stems("") == ["", ""]
assert get_hidden_stems("") == ["", "", ""]
assert get_hidden_stems("") == ["", "", ""]
assert get_hidden_stems("") == [""]
assert get_hidden_stems("") == ["", "", ""]
assert get_hidden_stems("") == ["", ""]
# ─── 2. hiddenStems reference 매칭 ───────────────────────────────────
@pytest.mark.parametrize(
"case",
REF,
ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_hidden_stems_match_reference(case):
inp = case["input"]
expected_raw = case["expected"]["analysis"].get("hiddenStems")
if expected_raw is None:
pytest.skip("no hiddenStems in expected")
expected = _normalize(expected_raw)
saju = calculate_saju(
inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
)
actual = get_all_hidden_stems(saju)
assert actual == expected, f"mismatch for {inp}\nactual={actual}\nexpected={expected}"
# ─── 3. branchInteractions reference 매칭 ────────────────────────────
@pytest.mark.parametrize(
"case",
REF,
ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_branch_interactions_match_reference(case):
inp = case["input"]
expected_raw = case["expected"]["analysis"].get("branchInteractions")
if expected_raw is None:
pytest.skip("no branchInteractions")
expected = _normalize(expected_raw)
saju = calculate_saju(
inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
)
actual = analyze_branch_interactions(saju)
# 순서가 다를 수 있으므로 정렬 후 비교
assert sorted(map(_canonical, actual)) == sorted(map(_canonical, expected)), (
f"mismatch for {inp}\nactual={actual}\nexpected={expected}"
)
# ─── 4. shinsal reference 매칭 ───────────────────────────────────────
@pytest.mark.parametrize(
"case",
REF,
ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_shinsal_match_reference(case):
inp = case["input"]
expected_raw = case["expected"]["analysis"].get("shinsal")
if expected_raw is None:
pytest.skip("no shinsal")
expected = _normalize(expected_raw)
saju = calculate_saju(
inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
)
actual = calculate_shinsal(saju)
assert sorted(map(_canonical, actual)) == sorted(map(_canonical, expected)), (
f"mismatch for {inp}\nactual={actual}\nexpected={expected}"
)
# ─── 5. gongmang reference 매칭 ──────────────────────────────────────
@pytest.mark.parametrize(
"case",
REF,
ids=lambda c: f"{c['input']['year']}-{c['input']['month']:02d}-{c['input']['day']:02d}",
)
def test_gongmang_match_reference(case):
inp = case["input"]
expected_raw = case["expected"]["analysis"].get("gongmang")
if expected_raw is None:
pytest.skip("no gongmang")
expected = _normalize(expected_raw)
saju = calculate_saju(
inp["year"], inp["month"], inp["day"], inp.get("hour"), inp["gender"]
)
actual = calculate_gongmang(saju["day_stem"], saju["day"]["branch"])
# branches / branches_kr / description 전체 비교
assert actual.get("branches") == expected.get("branches"), (
f"branches mismatch for {inp}\nactual={actual}\nexpected={expected}"
)
assert actual.get("branches_kr") == expected.get("branches_kr"), (
f"branches_kr mismatch for {inp}\nactual={actual}\nexpected={expected}"
)
assert actual.get("description") == expected.get("description"), (
f"description mismatch for {inp}\nactual={actual}\nexpected={expected}"
)