Files
Aislo/resources/tester/test_stmate_ui_round3.py
T

251 lines
10 KiB
Python

"""STMATE 재현 앱 화면 3차 — 직종·노임 적용코드(wM_Set_JLab) · 공종번호 자동입력(wITNum 하위 전체공종 반영).
JLab: 적용코드 = Start8x.Ini CONLD(6자리 코드 이어 붙임 · REA 14 TWM_SET_SYS/TWM_SET_JLAB) · 적용단가 = 일 노임 × 환산식
· 추가·삭제 왕복 = 원래 글 · 저장 = Start8x.Ini 그 키만.
wITNum: cBx_Bl_Lev 「하위 전체공종 반영」 = 묶음 안 공종(계표시)·단가코드를 한 차례로 · 직전공종 번호 + 첫 글자 + 번호.
"""
import json
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
import server # noqa: E402
from smt_parser import parse_ini_bytes # noqa: E402
from stc_save import apply_save # noqa: E402
HELPER = Path(__file__).with_name("helper_stmate_ui_r3.cjs")
STCS = sorted((ROOT / "resources/knowledge/original/실무문서").rglob("*.STC"))
IDS = [p.stem[:16] for p in STCS]
def node(payload):
raw = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
return json.loads(subprocess.check_output(["node", str(HELPER)], input=raw))
def _f(v):
return float(v or 0)
@pytest.mark.parametrize("stc", STCS, ids=IDS)
def test_jlab_codes_hourly_and_save(stc):
load = server.api_load(server.LoadReq(source_path=str(stc)))
ini = load["start_ini"]
r = node({"mode": "jlab", "load": load})
conld = ini.get("CONLD", "")
assert [t["code"] for t in r["j"]["target"]] == [
conld[i : i + 6] for i in range(0, len(conld), 6)
]
assert r["j"]["formula"] == ini.get("RXXNM", "").strip()
costn = {x["CODE"].strip(): x for x in load["costn_rows"]}
for t in r["j"]["target"]: # 적용단가 = 일 노임 × 환산식(왼쪽부터) 버림
assert t["price"] == _f(costn.get(t["code"], {}).get("LAMT1"))
assert t["hourly"] <= t["price"]
assert all(s["code"][0] == "L" and s["code"] not in conld for s in r["j"]["source"])
assert r["back"] == conld and r["dup"] == conld
if not r["add"]:
pytest.skip("고를 노무비 코드 없음")
assert r["added"] == conld + r["add"]
files = server.read_project_files(str(stc))
out, log = apply_save(files, {"source_path": str(stc), "start_ini": {"CONLD": r["added"]}})
names = [n for n in ("START8X.INI", "STINI.MEM") if n in files]
assert names and {(x["table"], x["field"]) for x in log} == {(n, "CONLD") for n in names}
for n in names:
assert parse_ini_bytes(out[n])["CONLD"] == r["added"]
# 공종번호 계획 = 사용자가 wITNum 에서 고를 선택지(뿌리 레벨 · 끝 글자 · 대상 · 마디 한도)와 선택 범위(그 머리 묶음)
# under = [레벨, n번째] — 그 레벨 n번째 머리 묶음만 선택 범위로
def _tree(level, last, target="group", **k):
return {"tree": True, "level": level, "last": last, "target": target, **k}
ITNUM_PLANS = {
"13-2024년 산불진화임도 실시": {"BOQ11.DBF": [_tree(4, ".")]},
"2024년 임도시설사업2차(산불": {
"BOQ11.DBF": [
_tree(3, "", "both", maxDepth=3, under=[4, 1]),
_tree(3, "", "both", maxDepth=3, under=[4, 2]),
_tree(3, ".", "both", maxDepth=3, under=[4, 3]),
_tree(3, ".", "both", maxDepth=3, under=[4, 4]),
]
},
"2024년 간선임도사업(기번3)": {"BOQ11.DBF": [_tree(3, ".")]},
"2025년 계류보전사업(기번1 ": {"BOQ11.DBF": [_tree(3, ".")]},
"22-2025년 산불진화임도사업(": {"BOQ11.DBF": [_tree(3, ".")]},
"2025년 산불진화임도 신설사업": {"BOQ11.DBF": [_tree(3, ".")], "BOQ12.DBF": [_tree(3, ".")]},
"변경 1차분-2025년 산불진화임": {"BOQ11.DBF": [_tree(3, ".")]},
}
STRUCT_E = ("E", "C")
def _ranges(rows, plan):
"""under → range(그 머리 행 ~ 짝 닫는 행)."""
out = []
for o in plan:
o = dict(o)
if "under" in o:
lv, nth = o.pop("under")
heads = [i for i, r in enumerate(rows) if (r.get("BDCODE1") or "").strip() == f"T{lv}"]
i = heads[nth - 1]
end = next(
j for j, r in enumerate(rows) if j > i and r.get("SORTCODE") == rows[i]["ENDCODE"]
)
o["range"] = [i, end]
out.append(o)
return out
def _groups(res, rows):
"""번호 붙은 행을 바로 위 머리(부모)별 차례로 묶음."""
stack, groups, parent_of = [], {}, {}
ends = {}
for i, r in enumerate(rows):
bd = (r.get("BDCODE1") or "").strip()
while stack and i > ends[stack[-1]]:
stack.pop()
if bd[:1] in STRUCT_E or not bd or bd == ":":
continue
par = stack[-1] if stack else -1
if res[i]["want"].strip():
groups.setdefault(par, []).append(i)
parent_of[i] = par
if bd[:1] == "T" and len(bd) == 2:
end = next(
(j for j, q in enumerate(rows) if j > i and q.get("SORTCODE") == r["ENDCODE"]), i
)
ends[i] = end
stack.append(i)
return groups, parent_of
def _clean_prefix(res, idx, parent_want):
"""묶음 앞에서부터 원본 규칙 번호인 행 — 숫자 없음·겹침·빈 번호·순서 바뀜·직전공종 번호 다름에서 멈춤."""
import re as _re
ok = []
for k, i in enumerate(idx, 1):
w = res[i]["want"].strip()
m = _re.match(r"^(.*?)(\d+)\.?$", w)
if not m or int(m[2]) != k:
break
head = m[1].strip().rstrip(".")
if head != parent_want.strip().rstrip("."):
break
ok.append(i)
return ok
@pytest.mark.parametrize("stc", STCS, ids=IDS)
def test_itnum_tree_reproduces_every_rule_number(stc):
load = server.api_load(server.LoadReq(source_path=str(stc)))
plans = next(v for k, v in ITNUM_PLANS.items() if stc.stem.startswith(k))
tables = {t: _ranges(load["boq_tables"][t], p) for t, p in plans.items()}
out = node({"mode": "itnum", "load": load, "plans": tables})
total = checked = 0
for t, res in out.items():
rows = load["boq_tables"][t]
groups, _par = _groups(res, rows)
good = set()
# 뿌리 쪽부터 — 부모 번호가 원본 규칙 번호로 맞았거나 규칙 꼴이 아닌 머리(□ · [1] 등 = 손 입력)일 때 하위 묶음 대조
for par in sorted(groups):
pw = res[par]["want"] if par != -1 else ""
if _re_digit(pw) and par not in good:
continue
for i in _clean_prefix(res, groups[par], pw if _re_digit(pw) else ""):
assert res[i]["got"] == res[i]["want"], (t, i, res[i])
good.add(i)
total += sum(1 for r in res if r["want"].strip() and r["kind"] in ("T", "LEAF"))
checked += len(good)
# 원본 번호 대부분이 규칙 번호 — 나머지는 손으로 고친 번호(□ · 겹침 · 지운 공종 빈자리 뒤)
assert checked >= 0.75 * total, (checked, total)
def _re_digit(s):
"""규칙 꼴 번호(…숫자 · …숫자.) — 숫자 없는 □ · 「[1]」 같은 손 입력 머리는 아님."""
import re as _re
return bool(_re.match(r"^\s*(.*?)(\d+)\.?$", str(s)))
RATE_AXES = [
"I_KONG_TYPE",
"I_KONG_KIKAN",
"I_ANJUN_TYPE",
"I_KOBO_TYPE",
"I_HWAN",
"I_EQIP",
"I_TBUGUM",
"I_ILBAN",
]
def test_rate_axis_combos_recalculate():
"""제잡비 화면 요율 축 콤보(wM_KanJub) — 바꾸면 제비율표(RATE_R) 다시 조회 → 총공사비가 다시 셈해짐(실무 7건 중 하나 이상)."""
moved = {k: 0 for k in RATE_AXES}
for stc in STCS:
load = server.api_load(server.LoadReq(source_path=str(stc)))
if not load["rate_tables"].get("RATE_R.DBF"):
continue
out = node({"mode": "rateAxes", "load": load, "axes": RATE_AXES})
for k, deltas in out.items():
assert all(isinstance(d, (int, float)) for d in deltas), k
moved[k] += sum(1 for d in deltas if d != 0)
assert all(v > 0 for v in moved.values()), moved
@pytest.mark.parametrize("stc", STCS, ids=IDS)
def test_rate_table_edit_saves_only_changed_cell(stc):
"""제비율표 수정(wM_KAN_RATE) → /api/ui/save rate_tables — 고친 칸만 RATE_R.DBF 에 · 안 고치면 바이트 그대로."""
from api_ui_code import apply_struct
from smt_parser import parse_dbf_bytes
load = server.api_load(server.LoadReq(source_path=str(stc)))
rows = load["rate_tables"].get("RATE_R.DBF")
if not rows:
pytest.skip("RATE_R 없음")
files = server.read_project_files(str(stc))
same, log0 = apply_struct(files, {"rate_tables": {"RATE_R.DBF": rows}})
assert log0 == [] and same["RATE_R.DBF"] is files["RATE_R.DBF"]
edit = [dict(r) for r in rows]
r, k = 20, "RATE_01"
edit[r][k] = str(_f(edit[r][k]) + 1.25)
out, log = apply_struct(files, {"rate_tables": {"RATE_R.DBF": edit}})
assert [(x["table"], x["recno"], x["field"]) for x in log] == [("RATE_R.DBF", r, k)]
back = parse_dbf_bytes(out["RATE_R.DBF"])[1]
assert _f(back[r][k]) == _f(rows[r][k]) + 1.25
assert all(back[i] == rows[i] for i in range(len(rows)) if i != r)
def test_editor_function_list_and_highlight_follow_dfm():
"""산출근거 편집기 함수 단추(cBoxFunc)·문법 강조 낱말(Syn1.KeyWords) = 원본 TWM_EDIT_SAN DFM."""
dfm = (
ROOT / "resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/"
"30_수량문자열_수신편집기_작업자료_20261026/ST_EdSan/resource-TWM_EDIT_SAN.dfm.json"
)
nodes = {n.get("name"): n for n in json.loads(dfm.read_text(encoding="utf-8"))["nodes"]}
funcs = [x.strip() for x in nodes["cBoxFunc"]["properties"]["Items.Strings"][1:]]
keys = nodes["Syn1"]["properties"]["KeyWords.Strings"]
js = (
"globalThis.window=globalThis;"
"const L=require('../../tools/stmate-test/scr_edit_lang.js');"
"const X=require('../../tools/stmate-test/scr_edit_expr.js');"
"const ks=L.SYN_KEYWORDS.split(' ');"
"process.stdout.write(JSON.stringify({f:X.FUNC_LIST,k:ks,"
"h:ks.map(k=>L.highlight(k.toLowerCase()+'(1)')),v:L.highlight('Qo = 1')}))"
)
out = json.loads(
subprocess.check_output(
["node", "-e", js], cwd=str(HELPER.parent), text=True, encoding="utf-8"
)
)
assert out["f"] == funcs
assert out["k"] == keys
assert all('class="syn-key"' in h for h in out["h"])
assert 'class="syn-var"' in out["v"] and "syn-key" not in out["v"]