Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
247 lines
9.1 KiB
Python
247 lines
9.1 KiB
Python
"""단가산출 흐름도 입력 판 — 재료 요소 방식 셋 · 쪽 넘기기 · 다른 로직 입력(`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"}}
|
|
]
|
|
back = call("setLogicInput", lg, "b1", None)
|
|
assert back["boxes"][0]["input"]["mode"] == "variable" and back["links"] == []
|
|
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"] == []
|
|
)
|