"""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개 셀만 다름」