Files
Aislo/resources/tester/test_511_a_calc_sites_browser.py
T

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"