"""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)