- 화면 검증 원인 셋: ① 초안 첫 계획선 꼭짓점이 보기가 심은 측점(초안 자리 관)으로 흔들린 저장 기준을 씀 → 서버 저장 종단 꼭짓점(storedPvi) ② 묶음 cross 설계 칸이 다시 셈이 고쳐 쓴 화면 상세(옛 측구 결과 → 선택)를 읽음 → 세션 저장 상세 + 옛 측구 결과(ditch_enabled)도 실음 ③ 초안 자리로 미리 뜬 관 측점 제원이 누가거리 짝에서 빠짐 → 남은 저장 시설 측점과 차례 짝(349-0d 규칙) - 저장 노선 기준이 바뀌면(보기가 늦게 심은 측점 · 초안 · 되돌린 뒤 빼고) 처음부터 · 제원 지문을 cross 입력에 · 보기 첫 마운트 뒤도 고리 - placed → common_util_route_follow placedOnLine(700줄) - ORCA 검증_sub7: B05 끌기 길 · B06 먼저 새로고침 · B05 먼저 새로고침 = 67 측점 계획고 · 절토 · 성토 같음 · 열 때 1바퀴 - 시험 test_365(설계 칸 = 저장 상세 · 차례 짝 · 제원 지문 · 초안 계획선 = 저장 종단) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
233 lines
9.8 KiB
Python
233 lines
9.8 KiB
Python
"""PLAN 365 sub7 — 평면 > 종단 <> 횡단 한 방향 · 묶음 주인 = 페이지 · 캐시 · 고리.
|
|
|
|
못박는 것(365-1)
|
|
- 묶음은 페이지(종단도 한 벌 `sharedProfile`)가 세움 — 종단 보기는 다리만 받음(옵션 안 넘김).
|
|
- 노선 초안 둔 채 다시 열기(새로고침 · B06 바로 열기 · 편집기 없음) = B05 길과 같은 초안 상세
|
|
(측점 · 계획고 · 면적) · 측점을 가게에 다시 냄 · 초안 전 = 저장 상세(열 때 맡김) → 버리면 saved.
|
|
- 초안 동안 측점 갈아끼움은 메모리만 — 세션 상세(저장 노선)를 새 측점 + 옛 설계로 덮지 않음.
|
|
못박는 것(365-2)
|
|
- 초안 동안 B06 고침(세션 칸 · 측점 열쇠) → `pipelineCross` → 묶음 cross 입력 · 카드에 보임.
|
|
- 고리 — 같은 입력 = 0 바퀴 · 세션만 바뀜 = 1 바퀴 · 안 맞으면 3 바퀴 뒤 「계산 불안정」 한 번.
|
|
못박는 것(365-3)
|
|
- B05 오른쪽 횡단 판 = 묶음 그 측점 — 계획고 · 절토 면적 · 표본 = B06 카드 · 서버 길 없음.
|
|
못박는 것(365-4)
|
|
- 저장 노선 ▲▼ 한 번 = 횡단 한 벌 — 묶음 cross 가 다시 셈 창구(상세 설계) 한 번 · 종단도 예약 끔.
|
|
- 중심점 좌우 = 노선 차례는 cross 전에 밀림 → cross 한 번(계획고 맞춤 차례만).
|
|
- B06 다시 그림 = 겹친 신호(갈아끼움 · 묶음 cross · 초안) 한 번 · profile 차례는 안 그림.
|
|
못박는 것(365-5)
|
|
- 들어온 화면 무관 — 설계 칸 = 세션 저장 상세 · 초안 자리 시설 = 차례 짝 · 제원 지문 ·
|
|
초안 계획선 = 저장 종단.
|
|
- 초안 소단 = 지금 구조물 구간 · 쪽 깊이 = 세션 고침.
|
|
|
|
짝: `helper_365_page_pipeline.cjs`(진짜 TS · 서버 창구만 가짜 · 틀 = 346-2 시험) ·
|
|
`helper_365_swap_entry.ts` + `helper_365_swap.mjs`(진짜 상세 가게 · vite ssr).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
ROOT = HERE.parents[1]
|
|
if str(HERE) not in sys.path:
|
|
sys.path.insert(0, str(HERE))
|
|
|
|
import test_346_2_pipeline_cross_page as base # noqa: E402
|
|
|
|
from common_util.common_util_surface_sampler import DtmGridSampler, dtm_grid_bytes # noqa: E402
|
|
|
|
pytestmark = base.pytestmark
|
|
|
|
|
|
def _job(folder: Path) -> tuple[Path, Path]:
|
|
npz, nodes = base._fake(folder)
|
|
line = base._line(nodes)
|
|
sampler = DtmGridSampler.from_npz(npz)
|
|
pipes = base._pipes(line)
|
|
saved = base._sections(line, sampler, pipes)
|
|
options = base.resolve_grade_options("trunk", terrain_type="normal", paved=False, stored=None)
|
|
policy = base.design_ground_following_profile(saved["longitudinal"], options)[0]["policy"]
|
|
job = {
|
|
"nodes": nodes,
|
|
"line": line,
|
|
"minRadius": base.MIN_RADIUS_M,
|
|
"policy": policy,
|
|
"types": base._types(),
|
|
"interval": base.INTERVAL_M,
|
|
"pipes": pipes,
|
|
"standard": base.STANDARD_CROSS_SECTION,
|
|
"conversion": base.ONES,
|
|
"detail": base._saved_detail(saved),
|
|
"haulDelayMs": 10,
|
|
}
|
|
path = folder / "job.json"
|
|
path.write_text(json.dumps(job), encoding="utf-8")
|
|
grid = folder / "grid.bin"
|
|
grid.write_bytes(dtm_grid_bytes(npz))
|
|
return path, grid
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def r() -> dict:
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
path, grid = _job(Path(folder))
|
|
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
["node", str(HERE / "helper_365_page_pipeline.cjs"), str(path), str(grid)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=180,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def test_365_1_묶음_주인_페이지():
|
|
shared = (ROOT / "A00_Common/design/design_shared.ts").read_text(encoding="utf-8")
|
|
view = (ROOT / "A00_Common/design/design_view_profile.ts").read_text(encoding="utf-8")
|
|
assert "routeDraftBridge(projectId, profile, {" in shared # 종단도 한 벌과 함께 세움
|
|
assert "draft: routeDraftBridge(activeProjectId, profilePanel)," in view # 다리만 받음
|
|
assert "holdDraft" not in view and "holdDraft" in shared
|
|
|
|
|
|
def test_365_1_초안_둔_채_열기_같은_초안(r):
|
|
b05, b06 = r["b05"], r["b06"]
|
|
assert r["kept"] # 떠나도 세션 초안 남음
|
|
assert b05["rows"] and b06["rows"] == b05["rows"] # 측점 · 계획고 · 면적 같음
|
|
assert b06["outcome"] == "done" and "cross" in b06["stages"]
|
|
assert b06["stations"] == b05["stations"] == [row[0] for row in b05["rows"]] # 측점 다시 냄
|
|
assert b05["holds"] == ["saved"] and b06["holds"] == ["saved"] # 초안 전 = 저장 상세
|
|
|
|
|
|
def test_365_1_초안_버림_저장_정본(r):
|
|
d = r["dropped"]
|
|
assert d["detail"] is None and d["routeDraft"] is None
|
|
assert d["restoreWhy"] == "saved"
|
|
|
|
|
|
def test_365_2_초안_동안_횡단_고침_카드에(r):
|
|
e = r["edit"]
|
|
assert e["before"] != "blasting_rock"
|
|
assert e["input"] == "blasting_rock" and e["after"] == "blasting_rock"
|
|
assert e["runs"] == 1 and e["sources"] == ["cross"]
|
|
|
|
|
|
def test_365_2_고리_바퀴_수(r):
|
|
loop = r["loop"]
|
|
assert loop["same"] == 0 and loop["sameRuns"] == 0 # 같은 입력 = 안 돎
|
|
assert loop["once"] == 1 and loop["onceRuns"] == 1 and loop["onceGround"] == "ripping_rock"
|
|
assert loop["unstable"] == -1 and loop["unstableRuns"] == 3 and loop["alerts"] == 1
|
|
|
|
|
|
def test_365_3_평면_횡단_판_카드와_같음(r):
|
|
p = r["pane"]
|
|
assert p["count"] > 5 and p["same"] == p["count"]
|
|
assert p["elevation"] is not None
|
|
|
|
|
|
def test_365_3_서버_횡단_미리보기_길_없음():
|
|
router = (ROOT / "B05_Profile/B05_Profile_Router_Terrain.py").read_text(encoding="utf-8")
|
|
api = (ROOT / "B05_Profile/B05_Profile_Api_Replan.ts").read_text(encoding="utf-8")
|
|
edit = (ROOT / "B05_Profile/B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
|
assert "cross-preview" not in router and "cross-preview" not in api
|
|
assert "crossPreview.refresh()" not in edit # 묶음 cross 차례가 그림
|
|
|
|
|
|
def test_365_4_저장_노선_계획고_횡단_한_벌(r):
|
|
s = r["saved"]
|
|
assert s["owns"] and s["openCalls"] == 1
|
|
assert s["gradeCalls"] == 1 and s["runs"] == 1 and s["sources"] == ["grade"]
|
|
assert s["fromDetail"] # 묶음 cross = 상세 설계 그대로(엔진 따로 안 셈)
|
|
|
|
|
|
def test_365_4_중심점_좌우_횡단_한_번(r):
|
|
runs = r["shiftRuns"]
|
|
assert [x["outcome"] for x in runs] == ["dropped", "done"], runs
|
|
assert [x["cross"] for x in runs] == [False, True]
|
|
assert runs[1]["sources"] == ["route", "grade"] # 밀린 노선 범위 + 계획고 = 한 차례
|
|
|
|
|
|
def test_365_4_창구로_연_뒤_초안_새로고침과_같음(r):
|
|
s = r["sameAfterReload"]
|
|
assert s["rows"] > 5 and s["marked"] == 0 and s["same"]
|
|
|
|
|
|
def test_365_5_초안_소단_고침(r):
|
|
b = r["berm"]
|
|
assert b["inSpan"] and b["outSpan"] # 소단 = 지금 구조물 구간(초안 측점)
|
|
assert b["depthBefore"] is None and b["depthAfter"] == 2.5 and b["runs"] == 1
|
|
|
|
|
|
def test_365_5_들어온_화면_무관(r):
|
|
v = r["viewFree"]
|
|
g = v["grid20"]
|
|
# 설계 칸 = 세션 저장 상세(화면 상세가 굳힌 선택 false 대신 저장값) · 옛 측구 결과도 실음
|
|
assert g["ditchChoice"] is None and g["ditchEnabled"] is False
|
|
assert v["pipeSpec"] == {
|
|
"culvert": {"type": "pipe", "diameter_m": 0.8}
|
|
} # 초안 자리 관 = 차례 짝
|
|
assert v["specsDiffer"] # 제원이 바뀌면 입력 지문도 바뀜(고리가 다시 셈)
|
|
assert v["firstStored"] == v["savedFirst"] and v["firstShaken"] != v["savedFirst"]
|
|
|
|
|
|
def test_365_4_다시_그림_한_번():
|
|
swap = (ROOT / "B06_Section/B06_Section_UI_Page_Station_Swap.ts").read_text(encoding="utf-8")
|
|
view = (ROOT / "A00_Common/design/design_view_cross.ts").read_text(encoding="utf-8")
|
|
profile = (ROOT / "A00_Common/design/design_profile.ts").read_text(encoding="utf-8")
|
|
assert 'event.stage !== "cross") return;' in swap # profile 차례 중간 상세는 안 그림
|
|
body = view.split("async function refreshDetailForStructures")[1].split("loadSectionDetail")[0]
|
|
assert "setTimeout(next, 0)" in body # 겹친 신호는 seq 가 앞 차례를 버림 = 한 번
|
|
assert "if (!pagePipeline.pipelineOwnsCross(projectId)) scheduleCrossPreview();" in profile
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def swap(tmp_path_factory) -> dict:
|
|
work = tmp_path_factory.mktemp("b365")
|
|
build = subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
[
|
|
"node",
|
|
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
|
"build",
|
|
"--configLoader",
|
|
"runner",
|
|
"--ssr",
|
|
"../resources/tester/helper_365_swap_entry.ts",
|
|
"--outDir",
|
|
str(work),
|
|
"--logLevel",
|
|
"error",
|
|
],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=300,
|
|
)
|
|
assert build.returncode == 0, build.stderr
|
|
bundle = work / "bundle.mjs"
|
|
shutil.copy(work / "helper_365_swap_entry.js", bundle)
|
|
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
["node", str(HERE / "helper_365_swap.mjs"), str(bundle)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=120,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def test_365_1_초안_동안_세션_상세_안_덮음(swap):
|
|
d = swap["draft"]
|
|
assert d["swapped"] and d["cache"] == [0, 20, 30, 40, 60] # 메모리 = 초안 측점
|
|
assert d["session"] == [0, 20, 40, 60] # 세션 = 저장 노선 그대로
|
|
s = swap["saved"]
|
|
assert s["swapped"] and s["session"] == s["cache"] == [0, 20, 50, 60] # 초안 없으면 세션도
|