Files
Aislo/resources/tester/test_802_3_doc_screen.py
T
eomsangdonandClaude Sonnet 5.5 2094436577 feat(802-2a): GC000100 암파쇄(유압식 할암공법) 수식 문서 넣음 — 단가산출 부름 값 = 끝수 뒤 채택(요구 714-3)
- 빌더 ADOPTED(채택된 차이) — GC000100 지역=경북 재료비 1 원 · 목록에 남기고 저장은 막지 않음 · 요약에 「채택된 차이」
- 수식 문서 1,518 → 1,519(건설품셈 03장 토공사 파일에 GC000100 49 줄 더함 · 다른 문서 그대로)
- test_802_2 기대 바꿈(채택 차이 · 저장 1,519) · test_802_3 「빈 문서」 길은 임시 사본에서 GC000100 문서를 지워 봄(낡은 기대: GC000100 문서 없음)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 13:19:17 +09:00

238 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""PLAN 802-3 — M01 단가산출 기초 수식 문서 화면 서버 길(요구 660~665 · 요구서_664).
① 읽기 {문서, 셈, 연결} · 없는 문서 = 빈 문서 · 없는 키 404
② 미리 셈 = 쓰기 없음 · 연결 기록(표 통째 · 단가산출 문서 · 요소 값)
③ 저장 = 서버가 정본으로 다시 셈(보낸 셈 값 무시) · 임시 폴더 파일에만 씀 · 다시 읽어 같음
④ 시스템 관리자 아니면 막힘(main.py 와 같은 의존성)
⑤ 지움 견딤 — 기호 · 줄 · 금액 줄 · 연결 대상 지움 · 순환 · 0 나눔 · 목록 밖 · 전부 지움 = 표지 · 금액 없음(500 없음)
"""
from __future__ import annotations
import copy
import shutil
from pathlib import Path
import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from common_util import common_util_auth as auth
from M01_MasterData import M01_MasterData_Doc as doc_screen
from M01_MasterData import M01_MasterData_Router as router_module
ROOT = Path(__file__).resolve().parents[2]
KEY = "GC000211" # 합판거푸집 설치 및 해체 — 계 124,419
@pytest.fixture
def client(tmp_path: Path, monkeypatch) -> TestClient:
for path in doc_screen.folder().glob("수식_*.json"):
shutil.copy(path, tmp_path / path.name)
monkeypatch.setattr(doc_screen, "folder", lambda: tmp_path)
doc_screen._memo.clear()
app = FastAPI()
app.include_router(router_module.router)
yield TestClient(app)
doc_screen._memo.clear()
def _read(c: TestClient, key: str = KEY) -> dict:
res = c.get("/api/m01/logic/doc", params={"key": key})
assert res.status_code == 200, res.text
return res.json()
def _prev(c: TestClient, doc: dict, given: dict | None = None) -> dict:
res = c.post("/api/m01/logic/doc/preview", json={"문서": doc, "입력": given or {}})
assert res.status_code == 200, res.text
return res.json()
def test_읽기_미리셈_연결기록(client):
got = _read(client)
assert got["셈"]["계"] == 124419 and got["셈"]["머리"] == "계" and got["새"] is False
rec = got["연결"]
assert {"QC000428", "QC000429", "QC000431"} <= set(
rec["표"]
) # 표 통째(옵션 바뀌어도 화면이 찾음)
assert all("줄" in t for t in rec["표"].values() if t)
assert any(e["p"].get("키") == "LB000007" and "값" in e for e in rec["요소"])
assert rec["이름"]["QC000431"].startswith("합판거푸집")
# 옵션(입력) 바꿈 = 문서 안 바뀜 · 값만
other = _prev(client, got["문서"], {"폼타이": "사용", "측압": "4t/㎡"})
assert got["셈"]["값"]["Q_조"] == 0
assert other["셈"]["값"]["Q_조"] == other["셈"]["값"]["Q_4t^표"] > 0
def test_없는문서_빈문서_없는키_404(client):
assert (
_read(client, "GC000100")["새"] is False
) # 802-2a 로 문서 넣음 — 임시 사본에서 지워 빈 문서 길을 봄
doc_screen.drop("GC000100")
fresh = _read(client, "GC000100")
assert fresh["새"] is True and fresh["문서"]["식"] == [] and fresh["문서"]["금액"] == []
assert fresh["셈"]["셈됨"] is False and fresh["셈"]["계"] is None
assert client.get("/api/m01/logic/doc", params={"key": "GX999999"}).status_code == 404
def test_저장_서버가_다시셈_파일에씀(client, tmp_path):
doc = _read(client)["문서"]
doc["식"].append({"기호": "Z_시험", "식": "3600 × 2 ÷ 4"})
doc["셈"] = {"계": 1} # 브라우저 값 — 서버는 안 받음
res = client.post("/api/m01/logic/doc/save", json={"문서": doc})
assert res.status_code == 200, res.text
out = res.json()
assert out["셈"]["값"]["Z_시험"] == 1800 and out["셈"]["계"] == 124419
assert out["파일"] == "수식_건설품셈_06장_철근콘크리트공사.json"
assert "Z_시험" in (tmp_path / out["파일"]).read_text(encoding="utf-8")
again = _read(client)
assert again["문서"]["식"][-1] == {"기호": "Z_시험", "식": "3600 × 2 ÷ 4"}
assert again["셈"]["계"] == 124419
# 정본 폴더는 안 바뀜
real = (ROOT / "resources/master_data/수식문서" / out["파일"]).read_text(encoding="utf-8")
assert "Z_시험" not in real
# 꼴 아님 · 키 없음 = 400
assert client.post("/api/m01/logic/doc/save", json={"문서": {"꼴": "x"}}).status_code == 400
assert (
client.post("/api/m01/logic/doc/save", json={"문서": {"꼴": "수식문서"}}).status_code == 400
)
def test_새문서_저장_빈데서_시작(client, tmp_path):
blank = _read(client, "GC000100")["문서"]
blank["식"] = [{"기호": "Q", "식": "3600 × q ÷ C_m"}]
blank["변수"] = [
{"열쇠": "q", "기호": "q", "연결": {"갈래": "숫자", "값": 0.7}},
{"열쇠": "C_m", "기호": "C", "아래": "m", "연결": {"갈래": "숫자", "값": 18}},
{"열쇠": "w", "기호": "w", "연결": {"갈래": "요소", "키": "LB000002"}},
]
blank["금액"] = [{"이름": "보통인부", "비목": "노무비", "단가": "w ÷ Q"}]
out = client.post("/api/m01/logic/doc/save", json={"문서": blank}).json()
assert out["셈"]["셈됨"] and out["셈"]["값"]["Q"] == 140
assert _read(client, "GC000100")["새"] is False
def test_관리자_아니면_막힘(monkeypatch):
for role, code in (("USER", 403), ("COMPANY_ADMIN", 403), ("SYSTEM_ADMIN", 200)):
app = FastAPI()
app.include_router(
router_module.router,
dependencies=[Depends(auth.verify_session), Depends(auth.require_system_admin)],
)
app.dependency_overrides[auth.verify_session] = lambda role=role: {
"role": role,
"is_master": False,
"company_id": 1,
}
c = TestClient(app)
assert c.get("/api/m01/logic/doc", params={"key": KEY}).status_code == code
if code == 403:
res = c.post("/api/m01/logic/doc/save", json={"문서": {"꼴": "수식문서", "키": KEY}})
assert res.status_code == 403
main = (ROOT / "main.py").read_text(encoding="utf-8")
assert "app.include_router(m01_master_data_router, dependencies=system_admin_only)" in main
def _cuts(doc: dict):
"""지운 · 망가뜨린 문서들."""
for i in range(len(doc["식"])):
one = copy.deepcopy(doc)
del one["식"][i] # 줄 하나 지움
yield one
one = copy.deepcopy(doc)
one["식"][i]["식"] = "없는기호 × 2" # 없는 기호
yield one
for i in range(len(doc["금액"])):
one = copy.deepcopy(doc)
del one["금액"][i]
yield one
for i in range(len(doc["변수"])):
one = copy.deepcopy(doc)
del one["변수"][i] # 기호 하나 지움 → 정의 필요
yield one
one = copy.deepcopy(doc)
kind = one["변수"][i]["연결"]["갈래"]
one["변수"][i]["연결"] = {"갈래": kind} # 연결 대상 아직 안 고름
yield one
one = copy.deepcopy(doc)
one["식"].append({"기호": "Q_시공^표", "식": "Q_시공 × 2"}) # 겹침
yield one
one = copy.deepcopy(doc)
for v in one["변수"]:
if v["열쇠"] == "유형":
v["연결"]["목록"] = ["보통"] # 목록 밖
yield one
one = copy.deepcopy(doc)
one["변수"], one["식"], one["금액"] = [], [], [] # 전부 지움
yield one
def test_지움_견딤_표지_금액없음(client):
doc = _read(client)["문서"]
n = 0
for one in _cuts(doc):
got = _prev(client, one)["셈"]
n += 1
if not got["셈됨"]:
assert got["계"] is None and (got["표지"] or got.get("까닭"))
assert n > 80
@pytest.mark.parametrize(
"line, kind",
[
({"기호": "Q_시공", "식": "Q_형틀목공 × 2"}, "순환"),
({"기호": "Q_시공", "식": "Q_시공^표 ÷ 0"}, "0 나눔"),
({"기호": "Q_시공", "식": "Q_시공^표 × ("}, "식 틀림"),
({"기호": "Q_시공", "식": "Z_없음 × 2"}, "정의 필요"),
],
)
def test_표지_갈래(client, line, kind):
doc = _read(client)["문서"]
doc["식"] = [line if x["기호"] == line["기호"] else x for x in doc["식"]]
got = _prev(client, doc)["셈"]
assert not got["셈됨"] and got["계"] is None
assert kind in {m["갈래"] for m in got["표지"]}
def test_연결_후보_표(client):
res = client.post(
"/api/m01/logic/doc/links", json={"갈래": "단가산출", "검색": "굴착기 시간당"}
)
hits = res.json()["후보"]
assert hits and all("입력" in h for h in hits)
res = client.post("/api/m01/logic/doc/links", json={"갈래": "표", "검색": "토사깍기 기계"})
assert any(h["키"] == "QF000249" for h in res.json()["후보"])
table = client.get("/api/m01/logic/doc/table", params={"key": "QF000249"}).json()
assert "조건" in table and table["줄"]
assert "끊김" in client.get("/api/m01/logic/doc/table", params={"key": "LB000002"}).json()
def test_옮김_군더더기_없음():
"""저장 문서 식 · 금액 글에 `+ 0` · `× 1` · `÷ 1` · `× k ÷ 100` 없음(옮김 `slim` · 값 같음)."""
import master_doc as md
import master_doc_convert as cv
import master_doc_link as ml
def junk(n) -> bool:
if n[0] == "bin":
a, b = n[2], n[3]
if n[1] in ("+", "-", "*", "/") and cv.slim(n) != n:
return True
return junk(a) or junk(b)
if n[0] in ("neg", "pct", "field"):
return junk(n[1])
return n[0] == "call" and any(junk(x) for x in n[2])
bad = []
for key, doc in ml.load_all().items():
texts = [x.get("식") for x in doc.get("식") or []]
texts += [x.get(k) for x in doc.get("금액") or [] for k in ("수량", "단가")]
for t in texts:
if t and junk(md.parse(t)):
bad.append((key, t))
assert not bad, bad[:5]
assert md.text_of(cv.slim(md.parse("(r + 0) × 1 × 2 ÷ 100 ÷ 1"))) == "r × 2%"