- 구조물 기록 · 관 지점 기록에 group_items {그룹 id: summary_item} 칸(검사 parse_group_items)
- 폼: 종류의 그룹마다 상자(제목 = 그룹 이름) · 그 그룹 옵션 칸을 옮겨 담음 · 「집계 항목」 [고르기][해제] · 빈 그룹도 상자
- 정의가 사라진 그룹 연결 = 값 보존 · 경고 한 줄
- 복사본 = 그룹 연결마다 {구조물 id}__{그룹 id} 폴더(관 pipe@측점__그룹)
- 집계표 채움 = 정의된 그룹마다 한 건 더(그 그룹이 고른 열) · 미지정 그룹은 「미지정」 알림에 셈
- B06 관 초안 · B05 시설 보관소 · 병합도 그룹 칸을 이음
- 시험 test_structure_group_items.py · 코드 글자를 보던 시험 셋은 바뀐 자리로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPcuokNm1ZR2kqgGGDfaJA
132 lines
5.2 KiB
Python
132 lines
5.2 KiB
Python
"""시설 속성 붙이기 거울 시험 — 화면 `carryIndices`(B05_Profile_UI_Drainage_Facility.ts) ↔
|
|
서버 `carry_indices`(common_util_drainage_pipes.py) 가 같은 답을 내는지.
|
|
|
|
규칙: 허용 오차(0.05m) 안 가장 가까운 원본만 · 한 원본은 한 번만 · 못 붙인 관은 속성 없음.
|
|
화면 보관소는 관을 끌면(`move`) · 되돌리기로 옮기면(`follow`) 속성을 따라 옮김.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from common_util.common_util_drainage_pipes import CARRY_TOLERANCE_M, carry_indices
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
FACILITY = ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility.ts"
|
|
CULVERT = ROOT / "B06_Section" / "B06_Section_Api_Culvert_Options.ts"
|
|
|
|
CASES = [
|
|
([100.0, 200.0], [100.0, 200.0]), # 제자리
|
|
([100.004], [100.0]), # 반올림 차이
|
|
([95.0, 100.0], [100.0]), # 관 수 > 원본 수 — 앞 관이 BOX 를 가져가지 않음
|
|
([150.37], [150.0]), # 허용 오차 밖
|
|
([100.01, 100.02], [100.0]), # 한 원본은 한 번만
|
|
([100.0], [99.97, 100.02]), # 허용 오차 안 가장 가까운 것
|
|
([100.0, 100.0], [100.06, 100.04]), # 경계 — 0.06 은 밖
|
|
([100.0], []), # 원본 없음
|
|
]
|
|
|
|
|
|
def _ts_block(body: str, head: str) -> str:
|
|
"""최상위 선언 하나를 떼어 냄 — 상수는 한 줄 · 함수는 첫 칸 `}` 까지."""
|
|
tail = r"[^\n]*;$" if head.startswith("const") else r"[\s\S]*?^\}$"
|
|
found = re.search(rf"^export {head}{tail}", body, re.M)
|
|
assert found, head
|
|
return found.group(0).replace("export ", "")
|
|
|
|
|
|
def _run_node(script: str) -> object:
|
|
result = subprocess.run(
|
|
["node", "--experimental-strip-types", "--no-warnings",
|
|
"--input-type=module-typescript", "-e", script],
|
|
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
|
) # fmt: skip
|
|
assert result.returncode == 0, result.stderr
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
def test_서버_규칙():
|
|
assert CARRY_TOLERANCE_M == 0.05
|
|
assert [carry_indices(t, s) for t, s in CASES] == [
|
|
[0, 1],
|
|
[0],
|
|
[None, 0],
|
|
[None],
|
|
[0, None],
|
|
[1],
|
|
[1, None],
|
|
[None],
|
|
]
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
|
def test_화면과_서버가_같은_답():
|
|
body = FACILITY.read_text(encoding="utf-8")
|
|
script = (
|
|
_ts_block(body, "const CARRY_TOLERANCE_M")
|
|
+ "\n"
|
|
+ _ts_block(body, "function carryIndices")
|
|
+ f"\nconst cases = {json.dumps(CASES)};"
|
|
+ "\nconsole.log(JSON.stringify([CARRY_TOLERANCE_M,"
|
|
+ " cases.map(([t, s]) => carryIndices(t, s))]));"
|
|
)
|
|
tolerance, picks = _run_node(script)
|
|
assert tolerance == CARRY_TOLERANCE_M
|
|
assert picks == [carry_indices(t, s) for t, s in CASES]
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
|
def test_화면_보관소_끌기_되돌리기_때_속성이_따라감():
|
|
"""BOX(100) · 옵션 없는 관(80) — 관을 끌거나 되돌려도 BOX 속성은 BOX 관에만."""
|
|
body = FACILITY.read_text(encoding="utf-8")
|
|
# 연결 칸 도우미(`linkFields` · `hasLinks` · PLAN 46-3) — import 없는 파일이라 통째로 붙임
|
|
links = (ROOT / "common_util" / "common_util_summary_item.ts").read_text(encoding="utf-8")
|
|
script = (
|
|
links
|
|
+ "\ntype PipeFacility = string; type PipeSource = string; type DetailPipeInput = any;\n"
|
|
"interface FacilityAttributes { facility: string; start_m?: number; end_m?: number;"
|
|
" options?: Record<string, string | number> }\n"
|
|
+ "\n".join(
|
|
_ts_block(body, head)
|
|
for head in (
|
|
"const CARRY_TOLERANCE_M",
|
|
"function carryIndices",
|
|
"function createFacilityStore",
|
|
)
|
|
)
|
|
+ "\n"
|
|
+ re.search(r"^function hasExtras[\s\S]*?^\}$", body, re.M).group(0)
|
|
+ """
|
|
const store = createFacilityStore();
|
|
store.replaceFromResponse([{ chainage_m: 80 }, { chainage_m: 100, facility: "box_culvert" }]);
|
|
const kinds = (list) => store.attach(list.map((c) => ({ chainage_m: c, source: "user" })))
|
|
.map((p) => p.facility ?? "-");
|
|
const out = {};
|
|
out.관을_BOX_곁으로 = kinds([99, 100]); // 옵션 없는 관을 BOX 곁으로 끌기(보관소 변화 없음)
|
|
store.move(100, 130); // BOX 를 끌기
|
|
out.BOX_끌기 = kinds([80, 130]);
|
|
store.follow([80, 130], [80, 100]); // 되돌리기 — BOX 가 제자리로
|
|
out.되돌리기 = kinds([80, 100]);
|
|
store.follow([80, 100], [81, 120]); // 둘이 한꺼번에 바뀜 — 옮기지 않음
|
|
out.둘_바뀜 = kinds([81, 120]);
|
|
console.log(JSON.stringify(out));
|
|
"""
|
|
)
|
|
assert _run_node(script) == {
|
|
"관을_BOX_곁으로": ["-", "box_culvert"],
|
|
"BOX_끌기": ["-", "box_culvert"],
|
|
"되돌리기": ["-", "box_culvert"],
|
|
"둘_바뀜": ["-", "-"],
|
|
}
|
|
|
|
|
|
def test_옛_관_이동_예약은_저장_때_안_씀():
|
|
body = CULVERT.read_text(encoding="utf-8")
|
|
assert "readMoves" not in body and "movedTo" not in body and "moveKey" not in body
|