Files
Aislo/resources/tester/test_stmate_conv_change.py
T

174 lines
6.8 KiB
Python

"""STMATE 재현 앱 분리형 변경 STC(차수 인계) · 코드 일괄 변경 · JB_* 기준 제비율표 · STC 검사.
정답: 소광 원설계 STC(BOQ11·BOQ12) → 실제 「변경 1차분」 STC(16번 §4.6) · 원본 STC 바이트(코드 되돌림) ·
10번 §4 문서의 RATE ↔ JB 일치 사례(설치 폴더에 JB_* 가 있을 때만).
"""
import collections
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
from bdqty import decode_bdqty # noqa: E402
from conv_jb import JB_DIR, list_jb, match_project # noqa: E402
from dbf_rw import DbfTable # noqa: E402
from smt_parser import extract_costn_memos, parse_dbf_bytes, parse_prjlst_smt # noqa: E402
from stc_archive import build_stc, check_stc # noqa: E402
from stc_change import split_change # noqa: E402
from stc_codechg import change_codes # noqa: E402
from stc_read import Source # noqa: E402
from stc_save import _leaf, apply_save # noqa: E402
DOCS = ROOT / "resources/knowledge/original/실무문서"
SO = DOCS / "25년 산불진화임도(기번8)(울진소광)"
ORIG = SO / "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC"
CHG1 = SO / "변경 1차분-2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC"
STCS = sorted(DOCS.rglob("*.STC"))
def _kv(files):
_, rows, _ = parse_dbf_bytes(files["PRJLST.DBF"])
return parse_prjlst_smt(files["PRJLST.SMT"], rows[0]["PRJ_MEMO_HEX"])
def test_split_change_matches_real_next_file():
a, b = Source(str(ORIG)).files, Source(str(CHG1)).files
out, rep = split_change(a)
assert rep["from_pcode"] == "12" # 통합형 — BOQ12 가 현재안
assert set(k for k in out if k.startswith("BOQ1")) == {"BOQ11.DBF"}
pg, pb = DbfTable(out["PRJLST.DBF"]), DbfTable(b["PRJLST.DBF"])
assert len(pg) == 1
for f in ("PNAME", "PCODE", "K_ORD", "MCOST1", "LCOST1", "SCOST1", "TCOST1", "COSTW1"):
assert pg.get(0, f) == pb.get(0, f), f
g, t = DbfTable(out["BOQ11.DBF"]), DbfTable(b["BOQ11.DBF"])
def key(tb, i, n):
return (
tb.get(i, "NAME1"),
tb.get(i, "SIZE1"),
round(float(tb.get(i, f"QTY{n}") or 0), 4),
int(float(tb.get(i, f"M_AMT{n}") or 0)),
int(float(tb.get(i, f"L_AMT{n}") or 0)),
int(float(tb.get(i, f"S_AMT{n}") or 0)),
)
mine = collections.Counter(key(g, i, 1) for i in range(len(g)) if _leaf(g.get(i, "BDCODE1")))
real = collections.Counter(
key(t, i, 1)
for i in range(len(t))
if _leaf(t.get(i, "BDCODE1")) and float(t.get(i, "QTY1") or 0)
)
assert mine == real
assert all(key(g, i, 1) == key(g, i, 2) for i in range(len(g))) # 차수2 = 출발점(차수1 복사)
kv1, kv2 = _kv(out), _kv(b)
rk = [k for k in kv2 if k.startswith("1_R_")]
assert rk and all(kv1.get(k) == kv2[k] for k in rk)
assert any(k.startswith("2_R_") for k in kv1)
assert check_stc(build_stc(Source(str(ORIG)).entries, out))["ok"]
@pytest.mark.parametrize("stc", STCS, ids=[p.stem[:16] for p in STCS])
def test_code_change_everywhere_and_back(stc):
f = Source(str(stc)).files
memos = extract_costn_memos(f["COSTN.DBF"], f["COSTN.SMT"])
xc = next(r["code"] for m in memos.values() for r in m["refs"] if r["code"].startswith("X"))
out, log = change_codes(f, {xc: "X99999"})
assert xc not in {r["CODE"].strip() for r in parse_dbf_bytes(out["COSTN.DBF"])[1]}
dq = decode_bdqty(out["BDQTY.DBF"])
assert not any(xc in (r["CODE"], r["BDCODE"]) for r in dq) and any(
"X99999" in (r["CODE"], r["BDCODE"]) for r in dq
)
m2 = extract_costn_memos(out["COSTN.DBF"], out["COSTN.SMT"])
assert not any(f"&{xc}" in m["text"] for m in m2.values()) and any(
"&X99999" in m["text"] for m in m2.values()
)
back, _ = change_codes(out, {"X99999": xc})
assert all(back[k][32:] == f[k][32:] for k in f) # 머리 날짜 밖 원래 바이트
with pytest.raises(ValueError):
change_codes(
f,
{
xc: next(
r["CODE"].strip()
for r in parse_dbf_bytes(f["COSTN.DBF"])[1]
if r["CODE"].strip() != xc
)
},
)
@pytest.mark.parametrize("stc", STCS, ids=[p.stem[:16] for p in STCS])
def test_check_stc_rules(stc):
r = check_stc(stc.read_bytes())
assert r["ok"] and r["warnings"] == [], (r["problems"], r["warnings"])
def test_prjlst_rows_edit_other_project_row():
f = Source(str(ORIG)).files
rows = parse_dbf_bytes(f["PRJLST.DBF"])[1]
assert len(rows) == 2
edited = [dict(r) for r in rows]
edited[1]["PNAME"] = "둘째 공사 이름 고침"
out, log = apply_save(f, {"prjlst_rows": edited})
assert [(x["recno"], x["field"]) for x in log] == [(1, "PNAME")]
assert DbfTable(out["PRJLST.DBF"]).get(1, "PNAME") == "둘째 공사 이름 고침"
out2, log2 = apply_save(f, {"prjlst_rows": rows})
assert log2 == [] and out2["PRJLST.DBF"] is f["PRJLST.DBF"]
needs_jb = pytest.mark.skipif(not JB_DIR.exists(), reason="STMATE 설치 폴더 JB_* 없음")
@needs_jb
def test_jb_files_shapes():
files = list_jb()
assert len(files) >= 43 and all(f["shape_ok"] for f in files)
kinds = collections.Counter(f["kind"] for f in files)
assert (
kinds["JB_1"] >= 1 and kinds["JB_M"] >= 1 and kinds["JB_R"] >= 27 and kinds["JB_RA"] >= 14
)
jr = {f["date"]: f["cols"] for f in files if f["kind"] == "JB_R"}
assert jr["2020-01-02"] == 20 and jr["2020-05-27"] == 35 # 2020-05-27 확장(10번 §4)
@needs_jb
@pytest.mark.parametrize(
"rel,table,date",
[
(
"05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC",
"RATE_R.DBF",
"2023-01-02",
),
(
"05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC",
"RATE.DBF",
"2011-01-12",
),
(
"04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC",
"RATE_R.DBF",
"2024-03-15",
),
(
"2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC",
"RATE_R.DBF",
"2020-05-27",
),
],
)
def test_jb_match_documented_cases(rel, table, date):
m = match_project(Source(str(DOCS / rel)).files)
assert m[table]["exact"] and m[table]["date"] == date
@needs_jb
def test_jb_sogwang_five_cells():
m = match_project(Source(str(ORIG)).files)
assert (
m["RATE_R.DBF"]["date"] == "2025-01-01" and m["RATE_R.DBF"]["diff_cells"] == 5
) # 10번 §4 「5개 셀만 다름」