236 lines
9.8 KiB
Python
236 lines
9.8 KiB
Python
"""PLAN 511-A 화면 — 자기 포트 Playwright 머리 없는 브라우저 · 서버가 떠 있을 때만 · 검증_sub2.
|
|
|
|
- 판 기본값(both) = B05 계획고 ▲ · 교각점 끌기 동안 /calc 0 · 브라우저 셈 기록만
|
|
- 묶음 하나씩(quantity · carry · fixed_browser) server → 같은 조작 → /calc 요청 · 답마다
|
|
그 페이지 TS 로 다시 셈한 값 = 서버 값 · 그 묶음 id 만 감 → 판 기본값으로(파일 바이트 그대로)
|
|
- 저장 안 함(세션 초안만 · 창 닫으면 버림) · 시험 브라우저 표식 시스템 로그 행만 지움
|
|
- 시간 표 = `tmp/511a_times.json`(묶음마다 브라우저 ms · 왕복 ms · Node ms · 흉내 0 · 30 · 80 ms)
|
|
|
|
./venv/Scripts/python.exe -m pytest resources/tester/test_511_a_calc_sites_browser.py -q -s
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import statistics
|
|
import sys
|
|
import urllib.request
|
|
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))
|
|
|
|
from helper_verify_project import own_ports # noqa: E402
|
|
from test_505_3_cross_card_screen import JS_NODES # noqa: E402
|
|
|
|
API, BASE = own_ports()
|
|
BOARD = ROOT / "resources" / "master_template" / "calc_sites.json"
|
|
PROJECT = "cb15892a-5788-4a26-9316-20a15421f68b" # 검증_sub2(OWNERS.md)
|
|
MARK = "AisloTest-511-A"
|
|
TIMES = ROOT / "tmp" / "511a_times.json"
|
|
GROUP_IDS = {
|
|
"quantity": {"structure.layouts", "cross.berm_spans", "masshaul.series"},
|
|
"carry": {"stations.carry_keyed"},
|
|
"fixed_browser": {"route.handle", "route.station_fit", "stations.swap", "profile.tools"}
|
|
| {"cross.fit", "footprint.plan"},
|
|
}
|
|
|
|
|
|
def _started(base: str) -> str | None:
|
|
try:
|
|
with urllib.request.urlopen(f"{base}/api/health", timeout=3) as res:
|
|
return json.loads(res.read()).get("started_at")
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
_UP = _started(BASE) if BASE else None
|
|
pytestmark = pytest.mark.skipif(
|
|
_UP is None or _UP != _started(API), reason="이 폴더 서버 없음 · vite 가 이 폴더 백엔드로 안 감"
|
|
)
|
|
|
|
JS_GRADE = r"""async () => {
|
|
const url = performance.getEntriesByType('resource').map((e) => e.name)
|
|
.find((n) => /design_pipeline_page\.ts/.test(n));
|
|
const m = await import(url);
|
|
const pid = localStorage.getItem('frd_current_project_id');
|
|
const pipe = m.pagePipeline(pid);
|
|
const cur = pipe.input('alignment') ?? { station_offsets: {}, curve_radii: {} };
|
|
const st = pipe.output('ground').stations;
|
|
const key = st[Math.floor(st.length / 2)].chainageM.toFixed(3);
|
|
m.pipelineGrade(pid, { ...cur, station_offsets: { ...cur.station_offsets,
|
|
[key]: (cur.station_offsets?.[key] ?? 0) + 0.3 } });
|
|
}"""
|
|
|
|
# 잡은 /calc 하나를 이 페이지의 같은 TS 로 다시 셈 → 같은지
|
|
JS_MIRROR = r"""async (calls) => {
|
|
const urls = performance.getEntriesByType('resource').map((e) => e.name);
|
|
const mod = async (re) => {
|
|
const u = urls.find((n) => re.test(n));
|
|
if (!u) throw new Error('모듈 없음 ' + re);
|
|
return import(u);
|
|
};
|
|
const plain = (v) => JSON.parse(JSON.stringify(v, (_k, x) =>
|
|
x instanceof Map ? [...x.entries()] : x instanceof Set ? [...x] : x) ?? 'null');
|
|
const where = {
|
|
'structure.layouts': [/B06_Section_Structure_Layouts\.ts/, 'structureAreaRows'],
|
|
'cross.berm_spans': [/common_util_berm_spans\.ts/, 'bermSpansFromStructures'],
|
|
'masshaul.series': [/common_util_mass_haul\.ts/, 'computeMassHaulSeries'],
|
|
'route.handle': [/B05_Profile_UI_RouteEdit_Curve\.ts/, 'dragHandleTo'],
|
|
'route.station_fit': [/B05_Profile_RouteEdit_StationFit\.ts/, 'fitAfter'],
|
|
'stations.swap': [/B06_Section_Station_Swap\.ts/, 'swapCrossSections'],
|
|
'profile.tools': [/B05_Profile_UI_Profile_Straighten\.ts/, 'straightenBetween'],
|
|
'cross.fit': [/B06_Section_UI_Cross_Fit\.ts/, 'toeOffsets'],
|
|
'footprint.plan': [/B05_Profile_Footprint_Stage\.ts/, 'footprintStage'],
|
|
'stations.carry_keyed': [/B06_Section_Route_Carry\.ts/, 'carryKeyedStates'],
|
|
};
|
|
const out = [];
|
|
for (const { run, inputs, results } of calls) {
|
|
for (const id of run) {
|
|
const fn = (await mod(where[id][0]))[where[id][1]];
|
|
const args = inputs.args?.[id];
|
|
const local = id === 'structure.layouts' ? fn(inputs.detail.cross_sections)
|
|
: id === 'cross.berm_spans' ? fn(inputs.berm.structures)
|
|
: id === 'footprint.plan' ? fn({ out: args[0], range: null, prev: null })
|
|
: id === 'stations.carry_keyed' ? fn((n) => args[0][n] ?? {}, args[1], args[2])
|
|
: fn(...args);
|
|
const theirs = id === 'structure.layouts' ? results[id].areas
|
|
: id === 'cross.berm_spans' ? results[id].spans : results[id];
|
|
out.push({ id, same: JSON.stringify(plain(local)) === JSON.stringify(theirs) });
|
|
}
|
|
}
|
|
return out;
|
|
}"""
|
|
|
|
|
|
def _drop_test_logs() -> None:
|
|
from config.config_db import close_db_pool, init_db_pool
|
|
|
|
async def run():
|
|
pool = await init_db_pool()
|
|
try:
|
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
|
await cursor.execute("DELETE FROM system_audit_logs WHERE user_agent = %s", (MARK,))
|
|
await connection.commit()
|
|
finally:
|
|
await close_db_pool()
|
|
|
|
asyncio.run(run())
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def ctx():
|
|
sync = pytest.importorskip("playwright.sync_api")
|
|
committed = BOARD.read_bytes()
|
|
cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8"))
|
|
try:
|
|
with sync.sync_playwright() as play:
|
|
chrome = play.chromium.launch(channel="msedge", headless=True)
|
|
context = chrome.new_context(viewport={"width": 1600, "height": 1000}, user_agent=MARK)
|
|
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}" # 비밀번호는 안 찍음
|
|
try:
|
|
yield context
|
|
finally:
|
|
for key in GROUP_IDS:
|
|
_put(context, key, "both")
|
|
chrome.close()
|
|
finally:
|
|
BOARD.write_bytes(committed)
|
|
_drop_test_logs()
|
|
|
|
|
|
def _put(context, key: str, value: str) -> None:
|
|
r = context.request.put(f"{BASE}/api/dashboard/admin/calc-sites", data={"sites": {key: value}})
|
|
assert r.ok, r.status
|
|
|
|
|
|
def _session(context, delay: int = 0):
|
|
page = context.new_page()
|
|
page.add_init_script(
|
|
f"localStorage.setItem('frd_current_project_id', '{PROJECT}');"
|
|
" performance.setResourceTimingBufferSize(20000);"
|
|
)
|
|
calls: list[dict] = []
|
|
|
|
def on_response(res):
|
|
if res.url.endswith("/calc") and res.request.method == "POST":
|
|
calls.append({**json.loads(res.request.post_data), "results": res.json()["results"]})
|
|
|
|
page.on("response", on_response)
|
|
page.goto(f"{BASE}/#/b05-profile", wait_until="domcontentloaded")
|
|
page.wait_for_function("() => !!window.__aisloPipeline", timeout=90000)
|
|
page.wait_for_timeout(9000)
|
|
seed = "{ calls: [], local: {}, delayMs: 0 }"
|
|
page.evaluate(f"(d) => {{ (window.__aisloCalcServer ??= {seed}).delayMs = d; }}", delay)
|
|
return page, calls
|
|
|
|
|
|
def _act(page) -> dict:
|
|
page.evaluate(JS_GRADE) # 계획고 ▲ 한 번(가운데 측점 +0.3 m)
|
|
page.wait_for_timeout(3500)
|
|
nodes = page.evaluate(JS_NODES)
|
|
assert len(nodes) >= 3, "지도에서 교각점을 못 찾음"
|
|
node = nodes[len(nodes) // 2]
|
|
page.mouse.move(node["x"], node["y"])
|
|
page.mouse.down()
|
|
page.mouse.move(node["x"] + 14, node["y"] + 9, steps=10) # 교각점 끌기(노선 초안)
|
|
page.mouse.up()
|
|
page.wait_for_timeout(5000)
|
|
return page.evaluate("() => window.__aisloCalcServer")
|
|
|
|
|
|
def _median(values: list[float]) -> float | None:
|
|
values = [v for v in values if v is not None]
|
|
return round(statistics.median(values), 2) if values else None
|
|
|
|
|
|
def test_기본값_서버_부름_0(ctx):
|
|
page, calls = _session(ctx)
|
|
log = _act(page)
|
|
page.close()
|
|
assert calls == [] and log["calls"] == []
|
|
assert {"structure.layouts", "masshaul.series", "footprint.plan"} <= set(log["local"])
|
|
assert {"route.station_fit", "stations.swap", "stations.carry_keyed"} <= set(log["local"])
|
|
TIMES.write_text(json.dumps({"browser": log["local"]}, ensure_ascii=False), encoding="utf-8")
|
|
|
|
|
|
@pytest.mark.parametrize("group", list(GROUP_IDS))
|
|
def test_묶음_서버_요청_값_같음_시간(ctx, group):
|
|
_put(ctx, group, "server")
|
|
table = json.loads(TIMES.read_text(encoding="utf-8")) if TIMES.exists() else {}
|
|
try:
|
|
for delay in (0, 30, 80):
|
|
page, calls = _session(ctx, delay)
|
|
log = _act(page)
|
|
mirror = page.evaluate(JS_MIRROR, calls)
|
|
page.close()
|
|
sent = {id_ for call in calls for id_ in call["run"]}
|
|
assert sent and sent <= GROUP_IDS[group], (group, sent)
|
|
assert mirror and all(row["same"] for row in mirror), (group, mirror)
|
|
assert all(c["same"] is not False for c in log["calls"]), log["calls"]
|
|
rows = {}
|
|
for call in log["calls"]:
|
|
rows.setdefault(call["id"], []).append(call)
|
|
table[f"{group}@{delay}"] = {
|
|
id_: {
|
|
"n": len(group_calls),
|
|
"round_ms": _median([c["roundMs"] for c in group_calls]),
|
|
"node_ms": _median([c["nodeMs"] for c in group_calls]),
|
|
"wall_ms": _median([c["wallMs"] for c in group_calls]),
|
|
"kb": round(_median([c["bytes"] for c in group_calls]) / 1024),
|
|
}
|
|
for id_, group_calls in rows.items()
|
|
}
|
|
print(group, delay, table[f"{group}@{delay}"])
|
|
finally:
|
|
_put(ctx, group, "both")
|
|
TIMES.write_text(json.dumps(table, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
assert json.loads(BOARD.read_text("utf-8"))["sites"][group] == "both"
|