Files
Aislo/resources/tester/test_m01_easycalc_cond.py
T
eomsangdonandClaude Opus 5.5 3d52720759 feat(m01-easycalc): 조건 보고 고치기 창 · 시험
- openEasyCond(card, doc, trial) — 잠금 카드 수량 · 그 밖 덧붙임의 조건을 문장으로 · 고르기 · 숫자 칸으로 고쳐 새 식 글 · 취소 null
- 읽는 꼴: 만약(사슬) · 최소/최대 · 올림/버림/반올림 — 식 전체 또는 식 속 덩어리 · 중간 값 이름 하나면 따라감 · 못 읽으면 지금 화면 안내 한 줄
- 금액 미리 보기: Api calcLater · 준비 전이면 /calc 0.3초 뒤 최신만
- 표 칩은 Table 파일이 있으면 openEasyTable 로 엶(import.meta.glob)
- 마스터: 줄 자체에 조건이 있는 카드 824 가운데 767 읽음(93%)
- 시험 resources/tester/test_m01_easycalc_cond.py 19개

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
2026-10-01 09:52:20 +09:00

199 lines
8.2 KiB
Python

"""M01 쉬운 단가산출 조건 보고 고치기 시험(34-3c) — 읽기 · 짜기 구역을 떼어 Node 로 돌림 · 화면 조작은 ORCA.
· 견본 식은 이 시험 안에 둠 — 진짜 마스터에 기대지 않음
· 만약(사슬) · 최소/최대 · 올림 · 식 속 덩어리 · 중간 값 따라가기 · 못 읽는 꼴 · 고친 뒤 짜기 왕복
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
COND = ROOT / "M01_MasterData" / "EasyCalc" / "M01_EasyCalc_Cond.ts"
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
INPUTS = [
{"이름": "비탈구배", "고르기": ["해당 없음", "1:1.0", "1:1.5"]},
{"이름": "뒷길이", "단위": "㎝"},
{"이름": "교환측", "고르기": ["양측", "한측"]},
{"이름": "층수", "고르기": [1, 2, 3]},
]
ROW = {"입력": INPUTS, "중간": [{"이름": "석공품", "식": "만약(뒷길이 > 40, 0.4, 0.3)"}]}
def _run(body: str, *args: object) -> object:
"""읽기 · 짜기 구역 + 시험 글 — `A` = 넘긴 인자 · `out(x)` 로 답"""
src = COND.read_text(encoding="utf-8")
part = src[src.index("/* ── 읽기 · 짜기") : src.index("/* ── 화면")]
script = (
"type LogicInput = any; type LogicRow = any;\n"
+ part
+ "\nconst A = JSON.parse(process.argv[1]);\n"
+ "const out = (x: unknown) => console.log(JSON.stringify(x));\n"
+ body
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script, json.dumps(list(args))],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def _read(formula: str) -> dict | None:
return _run("out(readCond(A[0], A[1]));", formula, ROW)
def _edit(formula: str, js: str) -> str:
"""읽은 뒤 `js` 로 덩어리(`B` = 첫 덩어리 꼴)를 고치고 다시 짬"""
return _run(
f"const R = readCond(A[0], A[1]); const B = R.blocks[0].shape; {js}; out(writeRead(R));",
formula,
ROW,
)
# ── 읽기 ───────────────────────────────────────────────────────────────────
def test_만약_한겹_설계값_표() -> None:
f = "만약(비탈구배 == '해당 없음', 0, 찾기(QF000001, 구배=비탈구배).값)"
r = _read(f)
assert r["via"] is None and r["text"] == f and len(r["blocks"]) == 1
b = r["blocks"][0]
assert b["p"] == [0, len(f)]
assert b["shape"] == {
"type": "if",
"branches": [
{
"test": {
"left": {"kind": "input", "name": "비탈구배"},
"op": "==",
"right": {"kind": "text", "value": "해당 없음"},
},
"value": {"kind": "num", "value": 0},
}
],
"otherwise": {"kind": "table", "src": "찾기(QF000001, 구배=비탈구배).값"},
}
def test_만약_사슬() -> None:
r = _read("만약(층수 == 1, 2, 만약(층수 == 2, 3.5, -1))")
shape = r["blocks"][0]["shape"]
assert [x["value"] for x in shape["branches"]] == [
{"kind": "num", "value": 2},
{"kind": "num", "value": 3.5},
]
assert shape["otherwise"] == {"kind": "num", "value": -1}
assert shape["branches"][1]["test"]["right"] == {"kind": "num", "value": 2}
def test_최대_올림() -> None:
pick = _read("최대(시공량 * 0.5, 3)")["blocks"][0]["shape"]
assert pick == {
"type": "pick",
"fn": "최대",
"items": [{"kind": "calc", "src": "시공량 * 0.5"}, {"kind": "num", "value": 3}],
}
up = _read("올림(찾기(QC000001).값 / 시공량, 2)")["blocks"][0]["shape"]
assert (up["type"], up["fn"], up["places"], up["hadPlaces"]) == ("round", "올림", 2, True)
assert up["value"] == {"kind": "calc", "src": "찾기(QC000001).값 / 시공량"}
assert _read("버림(뒷길이)")["blocks"][0]["shape"]["hadPlaces"] is False
def test_식_속_덩어리와_중간값() -> None:
f = "찾기(QC001975, 구간=구간).궤도공 * 만약(교환측 == '양측', 1, 0.65)"
r = _read(f)
assert len(r["blocks"]) == 1 and r["blocks"][0]["p"] == [f.index("만약"), len(f)]
many = _read("(1 + 만약(층수 > 1, 10, 0) + 최대(뒷길이, 30)) / 100")
assert [b["shape"]["type"] for b in many["blocks"]] == ["if", "pick"]
via = _read("석공품")
assert via["via"] == "석공품" and via["text"] == "만약(뒷길이 > 40, 0.4, 0.3)"
@pytest.mark.parametrize(
"formula",
[
"만약(뒷길이 > 40 그리고 층수 < 2, 1, 0)",
"찾기(QC000001, 항목=만약(교환측 == '양측', 'a', 'b')).수량",
"찾기(QC000001).값 * 2",
"0.5",
"만약(뒷길이 > 40, 1",
"",
],
ids=["그리고", "찾기속", "조건없음", "숫자", "괄호깨짐", "빈글"],
)
def test_못_읽는_꼴(formula: str) -> None:
assert _read(formula) is None
# ── 짜기 왕복 ───────────────────────────────────────────────────────────────
@pytest.mark.parametrize(
"formula",
[
"만약(비탈구배 == '해당 없음', 0, 찾기(QF000001, 구배=비탈구배).값)",
"만약(층수 == 1, 2, 만약(층수 == 2, 3.5, -1))",
"최소(시공량 * 0.5, 3, 뒷길이)",
"반올림(찾기(QC000001).값 / 시공량, 2)",
"찾기(QC001975, 구간=구간).궤도공 * 만약(교환측 == '양측', 1, 0.65) * (1 + 할증 / 100)",
"(용접공 * LB000012 + 특별인부 * LB000003) * 만약(교환측 == '양측', 22, 18) / 100",
],
)
def test_안_고치면_같은_글(formula: str) -> None:
assert _run("out(writeRead(readCond(A[0], A[1])));", formula, ROW) == formula
def test_고친_뒤_짜기() -> None:
f = "만약(비탈구배 == '해당 없음', 0, 찾기(QF000001, 구배=비탈구배).값)"
assert (
_edit(f, "B.branches[0].test.right.value = '1:1.5'; B.branches[0].test.op = '!='")
== "만약(비탈구배 != '1:1.5', 0, 찾기(QF000001, 구배=비탈구배).값)"
)
assert (
_edit(f, "B.otherwise = { kind: 'num', value: 0.25 }")
== "만약(비탈구배 == '해당 없음', 0, 0.25)"
)
assert (
_edit(
"찾기(QC001975, 구간=구간).궤도공 * 만약(교환측 == '양측', 1, 0.65)",
"B.branches[0].value.value = 1.1",
)
== "찾기(QC001975, 구간=구간).궤도공 * 만약(교환측 == '양측', 1.1, 0.65)"
)
assert (
_edit("최대(시공량 * 0.5, 3)", "B.fn = '최소'; B.items[1].value = 4")
== "최소(시공량 * 0.5, 4)"
)
assert _edit("올림(뒷길이 / 3)", "B.places = 1; B.fn = '반올림'") == "반올림(뒷길이 / 3, 1)"
assert _edit("석공품", "B.branches[0].test.left.name = '층수'; B.otherwise.value = 0.2") == (
"만약(층수 > 40, 0.4, 0.2)"
)
def test_둘째_덩어리_고침() -> None:
f = "(1 + 만약(층수 > 1, 10, 0) + 최대(뒷길이, 30)) / 100"
got = _run(
"const R = readCond(A[0], A[1]); R.blocks[1].shape.items[1].value = 45; out(writeRead(R));",
f,
ROW,
)
assert got == "(1 + 만약(층수 > 1, 10, 0) + 최대(뒷길이, 45)) / 100"
# ── 화면 글 ─────────────────────────────────────────────────────────────────
def test_화면_글에_금지_낱말_없음() -> None:
src = COND.read_text(encoding="utf-8")
block = src[src.index("const TEXT = {") : src.index("} as const;")]
korean = [ko for _, ko in re.findall(r'(\w+):\s*\[\s*"([^"]*)"', block)]
assert len(korean) >= 30
for text in korean:
for bad in ("키", "식", "호표", "덧줄", "비목", "중간 값", "끝수", "정본", "자체", "별칭"):
assert bad not in text, (bad, text)