feat(511-B B-5 follow 줄 엶): 계산 자리 follow(P5 P6 F1 F2) 바꿀 수 있음 · 기본 both 그대로 · 「서버」 = 끌기 도구 꼴(끌기 중 화면 셈 · 잠잠 뒤 창구 /calc stage.follow until follow → 서버 Node route · follow 두 단계 · 답 오면 묶음을 노선부터 다시 돌려 서버 값으로 · 곁 자료 plusAt · followedAt 같이 돌려받아 다시 붙임 · 열쇠 = 노선 초안 + 저장 계획고) · 저장 정본 맥락(노선 계획 · 종류 · 정의 · 관 · 구조물 · 추가 측점 · 측점 간격 · 저장 종단) = 창구가 프로젝트 id 로 모음(B06_Section_Router_Calc_Context · 브라우저 정본 칸 버림 · 정본 쓰기 없음) · 묶음 refresh(입력 그대로 다시 돎) · 계획고 옮김 = 끌기 중 화면이 옮긴 입력 그대로(서버 follow 는 옮김 기준 없음) · 검증_sub3 끌기 네 걸음 뒤 서버 = 화면 0 차(관 · 입력 · 첫 계획선 · 종단 PVI 61 · 왕복 231 ms · Node 5 ms) · Node 두 단계 = 묶음 첫 바퀴 0 차(golden) · 까닭 글 follow · pair_B5 걷음 — 요구 320-4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -319,3 +320,200 @@ def test_화면_서버_노선_화면_셈과_같음(screens, case):
|
||||
assert both["calls"] == []
|
||||
# 저장 노선(초안 없음) = 서버 안 부름 · 초안 = server 판만 서버 노선
|
||||
assert bool(server["calls"]) is (case == "draft")
|
||||
|
||||
|
||||
# ── follow 「서버」 — 창구 맥락 · Node route · follow 두 단계 ─────────────────────────
|
||||
|
||||
|
||||
def test_창구_follow_맥락_브라우저_정본칸_버림(monkeypatch):
|
||||
from B06_Section import B06_Section_Router_Calc_Context as ctx
|
||||
|
||||
async def context(_pid):
|
||||
return {"plan": {"planned": "서버"}, "saved_pipes": ["서버"], "longitudinal": None}
|
||||
|
||||
monkeypatch.setattr(ctx, "follow_context", context)
|
||||
inputs = {
|
||||
"stages": {
|
||||
"until": "follow",
|
||||
"input": {"route": 1},
|
||||
"saved_edits": None,
|
||||
"plan": {"planned": "브라우저"},
|
||||
"saved_pipes": ["브라우저"],
|
||||
"grid": {"path": "C:/x"},
|
||||
"detail": {"x": 1},
|
||||
}
|
||||
}
|
||||
assert asyncio.run(ctx.attach_follow_context("p", inputs)) is None
|
||||
assert inputs["stages"] == {
|
||||
"until": "follow",
|
||||
"input": {"route": 1},
|
||||
"saved_edits": None,
|
||||
"plan": {"planned": "서버"},
|
||||
"saved_pipes": ["서버"],
|
||||
"longitudinal": None,
|
||||
}
|
||||
other = {"stages": {"plan": "브라우저"}} # until 없음 = 그대로(B-1 단계 길)
|
||||
assert asyncio.run(ctx.attach_follow_context("p", other)) is None and other["stages"] == {
|
||||
"plan": "브라우저"
|
||||
}
|
||||
|
||||
|
||||
def _golden_stages() -> dict:
|
||||
from common_util.common_util_surface_sampler import dtm_grid_bytes
|
||||
|
||||
def fixture(name):
|
||||
return json.loads((FIXTURE / name).read_text(encoding="utf-8"))
|
||||
|
||||
plan = fixture("plan.json")
|
||||
nodes = [[n["x"], n["y"]] for n in plan["nodes"]]
|
||||
at = len(nodes) // 2
|
||||
nodes[at] = [nodes[at][0] + 6, nodes[at][1] + 4] # 교각점 하나 옮긴 노선 초안
|
||||
draft = {
|
||||
"nodes": nodes,
|
||||
"curveOn": [n["radius_m"] is not None for n in plan["nodes"]],
|
||||
"curveRadius": [None] * len(nodes),
|
||||
"curveLock": [None] * len(nodes),
|
||||
"curveArc": [None] * len(nodes),
|
||||
"stationSnap": bool(plan.get("station_snap")),
|
||||
"stationSnapStep": plan.get("station_snap_step"),
|
||||
}
|
||||
grid = ROOT / "tmp" / "511b5_grid.bin"
|
||||
if not grid.is_file():
|
||||
grid.write_bytes(
|
||||
dtm_grid_bytes(FIXTURE / "project/B04_PreProcess/models/dtm_classification_smooth.npz")
|
||||
)
|
||||
return {
|
||||
"plan": plan,
|
||||
"detail": fixture("detail.json"),
|
||||
"types": fixture("structure_types.json")["types"],
|
||||
"saved_pipes": [
|
||||
{k: v for k, v in p.items() if k != "lonlat"}
|
||||
for p in fixture("pipes.json")["pipe_points"]
|
||||
],
|
||||
"saved_structures": fixture("structures.json")["structures"],
|
||||
"interval_m": 20,
|
||||
"grid": {"path": str(grid), "design": "las"},
|
||||
"input": {"route": draft, "plusStations": fixture("plus_stations.json")["chainages"]},
|
||||
}
|
||||
|
||||
|
||||
def test_Node_follow_두_단계_묶음_첫_바퀴와_같음():
|
||||
from common_util import common_util_calc_runner as runner
|
||||
|
||||
stages = _golden_stages()
|
||||
full = runner.run_calc_served(["stage.route", "stage.follow"], {"stages": stages})
|
||||
only = runner.run_calc_served(
|
||||
["stage.route", "stage.follow"], {"stages": {**stages, "until": "follow", "grid": None}}
|
||||
)
|
||||
assert full and only and "error" not in full and "error" not in only, (full, only)
|
||||
assert only["results"]["stage.route"] == full["results"]["stage.route"]
|
||||
got = only["results"]["stage.follow"]
|
||||
assert got["patch"] == full["results"]["stage.follow"]
|
||||
assert got["patch"]["pipes"] and got["followed"] # 관 따라감 · 따라가기만 한 자리 곁 자료
|
||||
|
||||
|
||||
# ── 화면 — follow 「서버」 끌기 여러 걸음 뒤 놓음 = 화면 셈(검증_sub3 · Playwright · 저장 안 함) ──
|
||||
|
||||
JS_DRAG = r"""async (steps) => {
|
||||
const urls = performance.getEntriesByType('resource').map((e) => e.name);
|
||||
const mod = async (re) => import(urls.find((n) => re.test(n)));
|
||||
const page = await mod(/design_pipeline_page\.ts/);
|
||||
const pid = localStorage.getItem('frd_current_project_id');
|
||||
const plan = await (await mod(/B05_Profile_Api_Replan\.ts/)).fetchRoutePlan(pid);
|
||||
const flat = (await mod(/B05_Profile_UI_RouteEdit_Edits\.ts/))
|
||||
.flattenServerPlan(plan.nodes, plan.curves ?? [], plan.node_edits);
|
||||
const bridge = page.routeDraftBridge(pid, null);
|
||||
const at = Math.floor(flat.planned.length / 2);
|
||||
for (let k = 1; k <= steps; k += 1) { // 끌기 걸음 — 걸음마다 묶음이 돎(잠잠 안 기다림)
|
||||
const nodes = flat.planned.map(([x, y]) => [x, y]);
|
||||
nodes[at] = [nodes[at][0] + 2 * k, nodes[at][1] + 1.5 * k];
|
||||
bridge.edited({
|
||||
nodes,
|
||||
curveOn: flat.curveOn.map((on) => on !== false),
|
||||
curveRadius: flat.curveRadius.map((r) => r ?? null),
|
||||
curveLock: flat.curveLock ?? nodes.map(() => null),
|
||||
curveArc: flat.curveArc ?? nodes.map(() => null),
|
||||
stationSnap: plan.station_snap ?? false,
|
||||
stationSnapStep: plan.station_snap_step ?? null,
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 60));
|
||||
}
|
||||
await page.pagePipeline(pid).settled();
|
||||
}"""
|
||||
|
||||
JS_FOLLOW = r"""async () => {
|
||||
const urls = performance.getEntriesByType('resource').map((e) => e.name);
|
||||
const page = await import(urls.find((n) => /design_pipeline_page\.ts/.test(n)));
|
||||
const pid = localStorage.getItem('frd_current_project_id');
|
||||
const pipe = page.pagePipeline(pid);
|
||||
await page.reconcilePagePipeline(pid);
|
||||
await pipe.settled();
|
||||
const plain = (v) => JSON.parse(JSON.stringify(v ?? null));
|
||||
const calls = (window.__aisloCalcServer?.calls ?? []).filter((c) => c.id === 'stage.follow');
|
||||
const planline = pipe.output('planline');
|
||||
return {
|
||||
follow: plain(pipe.output('follow')),
|
||||
inputs: plain(['pipes', 'structures', 'plusStations', 'alignment']
|
||||
.map((k) => [k, pipe.input(k)])),
|
||||
base_pvi: plain(planline?.basePvi), pvi: plain(pipe.output('profile')?.pvi),
|
||||
calls: calls.map((c) => ({ round: c.roundMs, node: c.nodeMs, bytes: c.bytes })),
|
||||
runs: (window.__aisloPipeline?.runs ?? []).slice(-1).map((r) => r.outcome),
|
||||
};
|
||||
}"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def drags():
|
||||
from test_511_b1_calc_node_stages import _UP, BASE, _started
|
||||
|
||||
if not BASE or _UP is None or _UP != _started(BASE.replace("5178", "8005")):
|
||||
pytest.skip("이 폴더 서버 없음")
|
||||
sync = pytest.importorskip("playwright.sync_api")
|
||||
cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8"))
|
||||
shots = {}
|
||||
with sync.sync_playwright() as play:
|
||||
chrome = play.chromium.launch(channel="msedge", headless=True)
|
||||
try:
|
||||
for mode in ("both", "server"):
|
||||
context = chrome.new_context(viewport={"width": 1600, "height": 1000})
|
||||
login = {k: cred[k] for k in ("email", "password")}
|
||||
r = context.request.post(
|
||||
f"{BASE}/api/auth/login/request", data=login, timeout=60000
|
||||
)
|
||||
assert r.ok, f"로그인 실패 {r.status}" # 비밀번호는 안 찍음
|
||||
if mode == "server": # 판은 안 고침 — 이 창이 받는 판만 follow = server
|
||||
|
||||
def board(route):
|
||||
body = route.fetch().json()
|
||||
body["sites"]["follow"] = "server"
|
||||
route.fulfill(json=body)
|
||||
|
||||
context.route("**/api/dashboard/calc-sites", board)
|
||||
page = context.new_page()
|
||||
page.add_init_script(
|
||||
f"localStorage.setItem('frd_current_project_id', '{VERIFY}');"
|
||||
" performance.setResourceTimingBufferSize(20000);"
|
||||
)
|
||||
page.goto(f"{BASE}/#/b05-profile", wait_until="domcontentloaded")
|
||||
page.wait_for_function("() => !!window.__aisloPipeline", timeout=90000)
|
||||
page.wait_for_timeout(9000)
|
||||
page.evaluate(JS_DRAG, 4) # 끌기 네 걸음(세션 초안 · 창과 함께 버림)
|
||||
page.wait_for_timeout(4000) # 잠잠 400 ms · 서버 · 다시 돎
|
||||
shots[mode] = page.evaluate(JS_FOLLOW)
|
||||
context.close()
|
||||
finally:
|
||||
chrome.close()
|
||||
(ROOT / "tmp" / "511b5_drags.json").write_text(json.dumps(shots), encoding="utf-8")
|
||||
return shots
|
||||
|
||||
|
||||
def test_화면_follow_서버_끌기_뒤_화면_셈과_같음(drags):
|
||||
both, server = drags["both"], drags["server"]
|
||||
assert both["runs"] == server["runs"] == ["done"]
|
||||
assert both["calls"] == [] and server["calls"] # server 판만 놓은 뒤 서버 follow
|
||||
# 계획고 옮김 = 끌기 중 화면 셈이 입력에 옮겨 둔 값 그대로(서버 follow 는 저장분에서 새로 셈 · 옮김
|
||||
# 기준(앞 노선 · 앞 첫 계획선) 없음 → patch 에 alignment 안 냄) — 입력 · 종단은 같아야 함
|
||||
drop = lambda patch: {k: v for k, v in patch.items() if k != "alignment"} # noqa: E731
|
||||
assert drop(server["follow"]) == drop(both["follow"])
|
||||
for key in ("inputs", "base_pvi", "pvi"):
|
||||
assert server[key] == both[key], key
|
||||
|
||||
Reference in New Issue
Block a user