137 lines
6.2 KiB
Python
137 lines
6.2 KiB
Python
"""STMATE 재현 앱 — 조달청 JDL TEXT 형식(지식DB REA 분석/37 §1 · 34 §17.9 바로잡음) 지킴 시험 · 할증율 4바이트 계수 ·
|
||
변경 4차 칸.
|
||
|
||
근거: JDLCHK.Tab_33.dll 0x5051e0(고정폭 탭 넣기 고리를 0x505218 jmp 가 건너뜀) · 0x504d20(첫 0x05 앞을 탭으로 ·
|
||
뒤를 0x07 로 나눔 · 열마다 Trim) · 열 개수 검사(0x405bfc 뒤 cmp) · FieldDefs(0x491dec) · L.Txt 오류 203002
|
||
만드는 쪽 ST_U_Convert 0x4b0904(코드 줄) · 0x4bc1a9(Main.TXT) · DP_Common EXTTOSTR 0x453e4c
|
||
할증율 = ST_Boq 0x4e9b20 fstp dword · 0x4e9bde fmul dword · 변경 4차 = PRJLST MCOST5·XX5NM · ST_Prt 0x4b3183
|
||
"""
|
||
|
||
import json
|
||
import math
|
||
import struct
|
||
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 jdl_format as J # noqa: E402
|
||
from dbf_rw import DbfTable # noqa: E402
|
||
from jdl_export import code_line, export_bundle # noqa: E402
|
||
from kanjub_cfg import stage_slots # noqa: E402
|
||
from stc_read import Source # noqa: E402
|
||
|
||
# 검사기 cmp 값(열 개수)
|
||
CMP = {"Main.Txt": 3, "M.Txt": 0x13, "L.Txt": 7, "E.Txt": 9, "W.Txt": 9, "J.Txt": 9, "GongJong.Txt": 4}
|
||
CMP |= {"Unit.Txt": 10, "JUnit.Txt": 10, "Danga.Txt": 10, "MainPart.Txt": 0x1F, "UnitPart.Txt": 0x1C}
|
||
CMP |= {"JPart.Txt": 0x13}
|
||
STC = next((ROOT / "tools/stmate-test/work_dbf").glob("*.STC"), None)
|
||
TAB, EXT, SUB, CRLF = "\t", "\x05", "\x07", b"\r\n"
|
||
|
||
|
||
def test_counts_and_column_tables():
|
||
assert J.COUNTS == CMP
|
||
for name, cols in J.COLUMNS.items():
|
||
assert len(cols) == CMP[name], name
|
||
|
||
|
||
def test_split_follows_checker():
|
||
"""탭 나눔 · 끝 탭 = 빈 열 · 0x05 뒤는 덧붙임 칸(0x07) · 열마다 #0~#32 걷음."""
|
||
raw = f"A{TAB} 이름 {TAB}\x01{TAB}{EXT}B{SUB}True{SUB} "
|
||
cols, ext = J.split_line(raw.encode("cp949"))
|
||
assert cols == ["A", "이름", "", ""] and ext == ["B", "True", ""]
|
||
assert J.split_line(f"X{TAB}Y") == (["X", "Y"], [])
|
||
line = J.build_line(["M00001", "철근", "", "TON", 1665.0, "", 0, ""], ["Q", True])
|
||
assert J.split_line(line) == (["M00001", "철근", "", "TON", "1665", "", "0", ""], ["Q", "True"])
|
||
|
||
|
||
def test_check_lines_counts_and_code_chars():
|
||
good = J.build_line(["L00001", "보통인부", "", "인", 150000, "", ""], ["", False, J.Raw(" ")])
|
||
assert J.check_lines("L.Txt", good + CRLF) == []
|
||
bad = J.build_line(["l0001", f"가{TAB}나", "", "인", 1, "", ""])
|
||
errs = J.check_lines("L.Txt", good + CRLF + bad)
|
||
assert any("203002" in e and "Line : 2" in e for e in errs) and any("코드 글자" in e for e in errs)
|
||
|
||
|
||
def test_num_text_exttostr():
|
||
got = [J.num_text(v) for v in (268058.0, 0, 0.1 + 0.2, 1e20, 1.5e-7, -12.5)]
|
||
assert got == ["268058", "0", "0.3", "100000000000000000000", "0.00000015", "-12.5"]
|
||
|
||
|
||
def test_export_bundle_lines_follow_rules():
|
||
if STC is None:
|
||
pytest.skip("표본 STC 없음")
|
||
src = Source(str(STC))
|
||
r = export_bundle(str(STC))
|
||
assert r["series"] == 1 and all(not v for v in r["checks"].values())
|
||
t = DbfTable(src.files["COSTN.DBF"])
|
||
want = {"L": "L.Txt", "M": "M.Txt", "S": "E.Txt", "W": "W.Txt", "B": "Unit.Txt", "X": "JUnit.Txt"}
|
||
want["D"] = "Danga.Txt"
|
||
lines = {k: v.split(CRLF) for k, v in r["files"].items()}
|
||
seen: dict[str, int] = {}
|
||
for i in range(len(t)):
|
||
code = t.get(i, "CODE")
|
||
name = want.get(code[:1])
|
||
if not name or t.deleted(i):
|
||
continue
|
||
cols, ext = J.split_line(lines[name][seen.get(name, 0)])
|
||
seen[name] = seen.get(name, 0) + 1
|
||
assert cols[0] == code and len(cols) == CMP[name]
|
||
k = code[0]
|
||
lam, mam, sam = (float(t.get(i, f) or 0) for f in ("LAMT1", "MAMT1", "SAMT1"))
|
||
if k == "L":
|
||
assert cols[4] == J.num_text(lam) and ext[0] == t.get(i, "BQCODE")
|
||
if k == "M":
|
||
assert cols[4] == J.num_text(mam) and cols[6] == J.num_text(float(t.get(i, "AMT_11") or 0))
|
||
if k == "S":
|
||
assert cols[4:7] == ["0", "0", J.num_text(sam)]
|
||
if k in "BXD":
|
||
assert cols[4:7] == [J.num_text(mam), J.num_text(lam), J.num_text(sam)]
|
||
if k == "X":
|
||
assert cols[3] == "시간"
|
||
assert sum(seen.values()) == sum(r["counts"].get(n, 0) for n in set(want.values()))
|
||
main = J.split_line(r["files"]["Main.Txt"].split(CRLF)[0])[0]
|
||
p = DbfTable(src.files["PRJLST.DBF"])
|
||
assert main == [p.get(0, "PNUM"), p.get(0, "PNAME"), "02"]
|
||
ini = r["files"]["Global.Ini"].decode("cp949")
|
||
assert "MAKER=STM" in ini and "JDL VERSION=3.3" in ini and f"TCOST={p.get(0, 'TCOST1')}" in ini
|
||
|
||
|
||
def test_unit_rules():
|
||
rec = {"CODE": "B00001", "NAME": "a", "SIZE": "", "UNIT": "l.s.", "EQCODE": "", "BQCODE": "", "UC": False}
|
||
rec |= {"L": 1.0, "M": 2.0, "E": 3.0, "AMT": [0] * 5, "PG": [""] * 5, "PG_J": "", "PG_S": ""}
|
||
assert J.split_line(code_line(rec, False, "")[1])[0][3] == "l.s.3"
|
||
assert J.split_line(code_line(rec | {"CODE": "W00001", "UNIT": "T%"}, False, "")[1])[0][3] == "T%*100"
|
||
assert J.split_line(code_line(rec | {"CODE": "S00001", "UNIT": " $ "}, False, "")[1])[0][3] == "$"
|
||
assert code_line(rec | {"CODE": "I00001"}, False, "") is None
|
||
|
||
|
||
def _f32(x):
|
||
return struct.unpack("<f", struct.pack("<f", x))[0]
|
||
|
||
|
||
def test_hal_rate_float32_factor():
|
||
"""할증 계수 4바이트 실수 — 70%·115% 에서 십진 계산보다 1원 적음."""
|
||
helper = Path(__file__).with_name("helper_stmate_kanjub_rea.cjs")
|
||
rows = [{"BDCODE1": "M00001", "_recno": 1, "SORTCODE": "1", "ENDCODE": "0"}]
|
||
for pct, base in ((70, 1000), (115, 1000), (107.5, 2000)):
|
||
want = math.trunc(base * _f32(pct / 100))
|
||
out = json.loads(
|
||
subprocess.check_output(
|
||
["node", str(helper)],
|
||
input=json.dumps({"mode": "halbase", "rows": rows, "pct": pct, "base": base}),
|
||
text=True,
|
||
encoding="utf-8",
|
||
)
|
||
)
|
||
assert out == [want] * 3, (pct, out)
|
||
assert math.trunc(1000 * _f32(0.7)) == 699 and math.trunc(1000 * _f32(1.15)) == 1149
|
||
|
||
|
||
def test_fifth_series_slot():
|
||
"""변경 4차 = 계열 5 — 저장 칸 5 를 차수로 읽음."""
|
||
assert stage_slots({"1_C_KANNO": 1, "5_C_KANNO": 2}) == [1, 5]
|