"""단가산출 흐름도 입력 판 — 재료 요소 방식 셋 · 쪽 넘기기 · 다른 로직 입력(`M02_DF_InputPanel_Build.ts`). 화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 순수 함수만 부름. 견본 상자 · 로직은 이 시험 안에 둠. """ import json import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] SOURCE = ROOT / "M02_MasterTemplete" / "DataFlow" / "M02_DF_InputPanel_Build.ts" RUNNER = r""" const fs = require("fs"); const path = require("path"); const [root, file] = process.argv.slice(1); const ts = require(path.join(root, "config", "node_modules", "typescript")); const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, () => ({})); const m = box.exports; const jobs = JSON.parse(fs.readFileSync(0, "utf8")); process.stdout.write(JSON.stringify(jobs.map((j) => m[j.name](...j.args)))); """ def call(name: str, *args): if shutil.which("node") is None: pytest.skip("node 가 없음") done = subprocess.run( ["node", "-e", RUNNER, str(ROOT), str(SOURCE)], input=json.dumps([{"name": name, "args": list(args)}], ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", check=True, ) return json.loads(done.stdout)[0] PLAIN = {"color": "#fff", "unit": "-", "x": 0, "y": 0} def logic() -> dict: return { "key": None, "name": "견본", "resultUnit": "원/㎡", "userElements": [], "boxes": [ { "id": "b1", "kind": "input", "name": "입력", **PLAIN, "input": {"mode": "variable", "elements": [], "variables": []}, }, { "id": "b2", "kind": "input", "name": "관경", **PLAIN, "input": { "mode": "variable", "elements": [], "variables": [{"name": "관경", "type": "text", "unit": "", "example": "300"}], }, }, ], "links": [], } HUME = {"name": "각형흄관", "spec": "D=300mm"} def test_규칙이_품목을_잡음_규격_이름_포함_같음(): in_spec = {"field": "규격", "op": "포함"} assert call("matches", HUME, in_spec, "300") is True assert call("matches", HUME, in_spec, "400") is False assert call("matches", HUME, {"field": "이름", "op": "포함"}, "흄관") is True assert call("matches", HUME, {"field": "이름", "op": "같음"}, "흄관") is False assert ( call("matches", HUME, {"field": "이름", "op": "같음"}, "각형 흄관") is True ) # 띄어쓰기는 가리지 않음 assert call("matches", HUME, in_spec, "") is False # 알약 값이 비면 안 잡음 def test_잡힌_수에_따른_알림(): assert call("catchNote", 0)["kind"] == "none" one = call("catchNote", 1, "각형흄관 D=300mm") assert one["kind"] == "one" and "각형흄관" in one["text"] many = call("catchNote", 3) assert many["kind"] == "many" and "3 개" in many["text"] def test_쪽_넘기기_범위(): assert call("pageRange", 25, 1, 12) == {"from": 1, "to": 12, "pages": 3} assert call("pageRange", 25, 3, 12) == {"from": 25, "to": 25, "pages": 3} assert call("pageRange", 25, 9, 12) == { "from": 25, "to": 25, "pages": 3, } # 쪽이 넘치면 마지막 쪽 assert call("pageRange", 0, 1, 12) == {"from": 0, "to": 0, "pages": 1} def test_변수_알약_목록(): pills = call("variablePills", logic()) assert pills == [{"port": {"box": "b2", "name": "관경"}, "unit": "", "example": "300"}] def test_선택_자리와_조건_찾기_요소(): item = {"ref": "MT009686", "spec": "D=300mm", "unit": "M"} slot = call( "materialElement", "choice", "흄관", {"구분": "관·이음관", "상세구분": "", "찾는말": "흄관"}, {"item": item}, ) assert slot["mode"] == "choice" and slot["name"] == "흄관" and slot["slot"] == "흄관" assert slot["default"] == "MT009686" and slot["scope"] == { "구분": "관·이음관", "찾는말": "흄관", } rule = {"field": "규격", "op": "포함", "from": {"box": "b2", "name": "관경"}} found = call("materialElement", "rule", "흄관", {"찾는말": "흄관"}, {"rule": rule}) assert found["mode"] == "rule" and found["ref"] == "" and found["rule"] == rule assert "default" not in found def test_재료_요소_검사(): ok = call("materialElement", "choice", "흄관", {"찾는말": "흄관"}) assert call("checkMaterial", ok, []) is None assert call("checkMaterial", {**ok, "name": ""}, []) == "자리 이름을 적어 주세요" assert call("checkMaterial", ok, [ok]) == "같은 이름의 알약이 이미 있음" assert "범위" in call("checkMaterial", call("materialElement", "choice", "흄관", {}), []) rule_no_pill = call( "materialElement", "rule", "흄관", {"찾는말": "흄관"}, {"rule": {"field": "규격", "op": "포함", "from": {"box": "", "name": ""}}}, ) assert "변수 알약" in call("checkMaterial", rule_no_pill, []) def test_조건_찾기_요소는_품목_키가_없어도_따로_넣고_이름으로_뺌(): rule = {"field": "규격", "op": "포함", "from": {"box": "b2", "name": "관경"}} a = call("materialElement", "rule", "흄관", {"찾는말": "흄관"}, {"rule": rule}) b = call("materialElement", "rule", "받침", {"찾는말": "받침"}, {"rule": rule}) lg = call("addElement", logic(), "b1", a) lg = call("addElement", lg, "b1", b) assert [e["name"] for e in lg["boxes"][0]["input"]["elements"]] == ["흄관", "받침"] assert call("addElement", lg, "b1", a) == lg # 같은 이름은 한 번만 gone = call("removeElementNamed", lg, "b1", "흄관") assert [e["name"] for e in gone["boxes"][0]["input"]["elements"]] == ["받침"] def other_logic() -> dict: return { "key": "DF-0009", "name": "시간당 작업량", "resultUnit": "㎥/시간", "userElements": [], "links": [], "boxes": [ { "id": "i1", "kind": "input", "name": "조건", **PLAIN, "input": { "mode": "variable", "elements": [], "variables": [{"name": "q", "type": "number", "unit": "㎥", "example": 6}], }, }, { "id": "i2", "kind": "input", "name": "재료", **PLAIN, "input": { "mode": "group", "group": "재료", "elements": [ { "ref": "", "name": "흄관", "spec": "", "unit": "M", "mode": "rule", "slot": "흄관", } ], "variables": [], }, }, { "id": "o1", "kind": "output", "name": "작업량", **PLAIN, "unit": "㎥/시간", "output": {"cost": "값", "unit": "㎥/시간"}, }, ], } def test_다른_로직_입력_알약(): ref = call("logicRefOf", "DF-0009", other_logic()) assert ref["key"] == "DF-0009" and ref["name"] == "시간당 작업량" assert ref["outputs"] == [{"name": "작업량", "unit": "㎥/시간"}] assert ref["designs"] == [{"name": "q", "unit": "㎥"}, {"name": "흄관", "unit": "M"}] def test_다른_로직_상자는_출력_단위를_따르고_빼면_변수로_돌아감(): ref = call("logicRefOf", "DF-0009", other_logic()) lg = call("setLogicInput", logic(), "b1", ref) box = lg["boxes"][0] assert box["input"]["mode"] == "logic" and box["input"]["logic"]["key"] == "DF-0009" assert box["unit"] == "㎥/시간" assert call("pillNames", box) == ["작업량"] lg["links"] = [ {"id": "l1", "from": {"box": "b1", "name": "작업량"}, "to": {"box": "b2", "name": "x"}}, {"id": "l2", "from": {"box": "b2", "name": "관경"}, "to": {"box": "b1", "name": "q"}}, {"id": "l3", "from": {"box": "b2", "name": "관경"}, "to": {"box": "b3", "name": "y"}}, ] back = call("setLogicInput", lg, "b1", None) assert back["boxes"][0]["input"]["mode"] == "variable" assert [x["id"] for x in back["links"]] == ["l3"] assert call("reconcileUnit", lg, "b1") == lg # 다른 로직 상자 단위는 안 건드림 def test_갈래를_다른_로직으로_바꾸면_고른_것을_비움(): el = {"ref": "LB1", "name": "석공", "spec": "", "unit": "원/일"} lg = call("addElement", call("setKind", logic(), "b1", "인력"), "b1", el) out = call("setKind", lg, "b1", "다른 로직") assert ( out["boxes"][0]["input"]["mode"] == "logic" and out["boxes"][0]["input"]["elements"] == [] )