test(507-4): 검증 스크립트 정비 — 계산 자리 화면 시험 안전장치 · 505-3 교각점 요소 자리 · 검증_sub7 시험 12 파일 지금 검증 프로젝트로 · 공용 TS 불러오기 첫 걸음

- 309-2 화면 시험: 5177 이 8004 로 갈 때만 돎(남의 마스터 안 씀) · 실패해도 판 되돌림(finally) · 읽기는 서버 응답으로 판정
- 505-3 첫 시험: 픽셀 (1199,296) 대신 캔버스 교각점 색으로 찾음 · 포트 · 프로젝트는 환경 변수로
- 349 계열 · 346-2 · 356-4: helper_verify_project 가 OWNERS.md 검증_subN 중 가장 오래 안 바뀐 프로젝트를 읽기만 · 없으면 까닭 글로 건너뜀 · 낡은 기대 xfail
- helper_ts_stub_loader 공용 불러오기 + 새 시험 · 헬퍼 3 곳 옮김

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 16:02:51 +09:00
co-authored by Claude Sonnet 5.5
parent de1377e45e
commit 026ca74c52
23 changed files with 311 additions and 156 deletions
@@ -108,6 +108,7 @@ STUBS["B06_Section_UI_Standard_Panel"] = {
// 구조물 초안 얹기(측점 계획 · 형상) — 이 시험 밖 · 아무 일 없음
STUBS["B06_Section_Section_Drafts"] = {
supersedeDraftStations: () => undefined,
withDraftStations: async () => undefined,
withDraftShapes: async () => undefined,
forgetDraftStations: () => undefined,
+3 -39
View File
@@ -7,24 +7,11 @@
* 입력 argv[2] JSON · argv[3] 격자 이진. 결과 = 표준출력 JSON. 짝 = `test_349_0l_undo_draw.py`. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const CATALOG = require("./helper_322_catalog_module.cjs");
const { makeLoader, memoryStorage } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const job = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
function memoryStorage() {
const map = new Map();
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
removeItem: (key) => map.delete(key),
key: (index) => [...map.keys()][index] ?? null,
get length() {
return map.size;
},
};
}
const windowBase = {
sessionStorage: memoryStorage(),
localStorage: memoryStorage(),
@@ -52,31 +39,7 @@ globalThis.fetch = (url, init) =>
});
const STUBS = {};
const cache = new Map();
function loadTs(full) {
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (CATALOG.isCatalogRequest(request)) return CATALOG;
const key = Object.keys(STUBS).find((name) => request.endsWith(name));
if (key) return STUBS[key];
if (request.startsWith("@config/")) return { API_BASE_URL: "http://fake" };
if (request.startsWith("@ui/")) return anyModule();
if (request.endsWith(".css")) return {};
if (request.endsWith(".json")) return require(path.join(path.dirname(full), request));
if (request.startsWith("@util/"))
return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`));
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
const load = (...parts) => loadTs(path.join(ROOT, ...parts));
const { load, loadTs } = makeLoader({ root: ROOT, stubs: STUBS, ui: () => anyModule() });
const sampler = load("common_util", "common_util_surface_sampler.ts");
const bytes = fs.readFileSync(process.argv[3]);
@@ -141,6 +104,7 @@ STUBS["B06_Section_UI_Standard_Panel"] = {
};
// 구조물 초안 얹기(측점 계획 · 형상) — 이 시험 밖 · 아무 일 없음
STUBS["B06_Section_Section_Drafts"] = {
supersedeDraftStations: () => undefined,
withDraftStations: async () => undefined,
withDraftShapes: async () => undefined,
forgetDraftStations: () => undefined,
+3 -39
View File
@@ -7,24 +7,11 @@
* 입력 argv[2] JSON · argv[3] 격자 이진. 결과 = 표준출력 JSON. 짝 = `test_349_0o_redo_same.py`. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const CATALOG = require("./helper_322_catalog_module.cjs");
const { makeLoader, memoryStorage } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const job = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
function memoryStorage() {
const map = new Map();
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
removeItem: (key) => map.delete(key),
key: (index) => [...map.keys()][index] ?? null,
get length() {
return map.size;
},
};
}
const windowBase = {
sessionStorage: memoryStorage(),
localStorage: memoryStorage(),
@@ -52,31 +39,7 @@ globalThis.fetch = (url, init) =>
});
const STUBS = {};
const cache = new Map();
function loadTs(full) {
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (CATALOG.isCatalogRequest(request)) return CATALOG;
const key = Object.keys(STUBS).find((name) => request.endsWith(name));
if (key) return STUBS[key];
if (request.startsWith("@config/")) return { API_BASE_URL: "http://fake" };
if (request.startsWith("@ui/")) return anyModule();
if (request.endsWith(".css")) return {};
if (request.endsWith(".json")) return require(path.join(path.dirname(full), request));
if (request.startsWith("@util/"))
return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`));
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
const load = (...parts) => loadTs(path.join(ROOT, ...parts));
const { load, loadTs } = makeLoader({ root: ROOT, stubs: STUBS, ui: () => anyModule() });
const sampler = load("common_util", "common_util_surface_sampler.ts");
const bytes = fs.readFileSync(process.argv[3]);
@@ -141,6 +104,7 @@ STUBS["B06_Section_UI_Standard_Panel"] = {
};
// 구조물 초안 얹기(측점 계획 · 형상) — 이 시험 밖 · 아무 일 없음
STUBS["B06_Section_Section_Drafts"] = {
supersedeDraftStations: () => undefined,
withDraftStations: async () => undefined,
withDraftShapes: async () => undefined,
forgetDraftStations: () => undefined,
@@ -140,6 +140,7 @@ STUBS["B06_Section_UI_Standard_Panel"] = {
};
// 구조물 초안 얹기(측점 계획 · 형상) — 이 시험 밖 · 아무 일 없음
STUBS["B06_Section_Section_Drafts"] = {
supersedeDraftStations: () => undefined,
withDraftStations: async () => undefined,
withDraftShapes: async () => undefined,
forgetDraftStations: () => undefined,
+3 -39
View File
@@ -4,24 +4,11 @@
* 입력 argv[2] JSON · argv[3] 격자 이진. 결과 = 표준출력 JSON. 짝 = `test_349_0s_draw_work.py`. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const CATALOG = require("./helper_322_catalog_module.cjs");
const { makeLoader, memoryStorage } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const job = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
function memoryStorage() {
const map = new Map();
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
removeItem: (key) => map.delete(key),
key: (index) => [...map.keys()][index] ?? null,
get length() {
return map.size;
},
};
}
const windowBase = {
sessionStorage: memoryStorage(),
localStorage: memoryStorage(),
@@ -49,31 +36,7 @@ globalThis.fetch = (url, init) =>
});
const STUBS = {};
const cache = new Map();
function loadTs(full) {
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (CATALOG.isCatalogRequest(request)) return CATALOG;
const key = Object.keys(STUBS).find((name) => request.endsWith(name));
if (key) return STUBS[key];
if (request.startsWith("@config/")) return { API_BASE_URL: "http://fake" };
if (request.startsWith("@ui/")) return anyModule();
if (request.endsWith(".css")) return {};
if (request.endsWith(".json")) return require(path.join(path.dirname(full), request));
if (request.startsWith("@util/"))
return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`));
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
const load = (...parts) => loadTs(path.join(ROOT, ...parts));
const { load, loadTs } = makeLoader({ root: ROOT, stubs: STUBS, ui: () => anyModule() });
const sampler = load("common_util", "common_util_surface_sampler.ts");
const bytes = fs.readFileSync(process.argv[3]);
@@ -138,6 +101,7 @@ STUBS["B06_Section_UI_Standard_Panel"] = {
};
// 구조물 초안 얹기(측점 계획 · 형상) — 이 시험 밖 · 아무 일 없음
STUBS["B06_Section_Section_Drafts"] = {
supersedeDraftStations: () => undefined,
withDraftStations: async () => undefined,
withDraftShapes: async () => undefined,
forgetDraftStations: () => undefined,
@@ -141,6 +141,7 @@ STUBS["B06_Section_UI_Standard_Panel"] = {
};
// 구조물 초안 얹기(측점 계획 · 형상) — 이 시험 밖 · 아무 일 없음
STUBS["B06_Section_Section_Drafts"] = {
supersedeDraftStations: () => undefined,
withDraftStations: async () => undefined,
withDraftShapes: async () => undefined,
forgetDraftStations: () => undefined,
@@ -0,0 +1,29 @@
/* 507-4 공용 불러오기 본보기 — 진짜 `design_calc_sites.ts` 를 공용 로더로 불러 판 읽기를 가짜 fetch 로 시험.
* node helper_507_4_loader_smoke.cjs (짝 = `test_507_4_ts_stub_loader.py`) */
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const calls = [];
globalThis.fetch = async (url) => {
calls.push(url);
return {
ok: true,
json: async () => ({
version: 1,
sites: { ground_sample: "server", corridor_mesh: "both" },
groups: [],
}),
};
};
const { load } = makeLoader({ root: ROOT });
const sites = load("A00_Common", "design", "design_calc_sites.ts");
(async () => {
const out = {
ground: await sites.calcOnServer("ground_sample"),
corridor: await sites.calcOnServer("corridor_mesh"),
};
out.calls = calls;
console.log(JSON.stringify(out));
})();
@@ -0,0 +1,59 @@
/* 진짜 TS 를 CommonJS 로 트랜스파일해 부르는 공용 불러오기 + 가짜 모듈 끼워 넣기(507-4 · 새 시험부터 이걸 씀).
* 옛 헬퍼 198 벌이 저마다 베낀 `loadTs`(+ `STUBS`) 한 벌 — 진짜 모듈이 새 export · import 를 늘리면 헬퍼마다 낡아
* 깨졌음(482). 여기만 고치면 되게 모음.
*
* const { makeLoader, memoryStorage } = require("./helper_ts_stub_loader.cjs");
* const STUBS = {}; // 끝이 이 이름으로 끝나는 import 를 가짜로(헬퍼가 나중에 채움)
* const { load, loadTs } = makeLoader({ root: ROOT, stubs: STUBS, ui: () => anyModule() });
* const store = load("B06_Section", "B06_Section_Section_Store.ts");
*
* 풀이 순서: 카탈로그 · STUBS · @config(가짜 주소) · @ui(`ui` 쪽 값 · 기본 showToast) · .css · .json · @util · 상대 경로 · node.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const CATALOG = require("./helper_322_catalog_module.cjs");
function memoryStorage() {
const map = new Map();
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
removeItem: (key) => map.delete(key),
key: (index) => [...map.keys()][index] ?? null,
get length() {
return map.size;
},
};
}
function makeLoader({ root, stubs = {}, ui = () => ({ showToast: () => undefined }) }) {
const cache = new Map();
function loadTs(full) {
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (CATALOG.isCatalogRequest(request)) return CATALOG;
const key = Object.keys(stubs).find((name) => request.endsWith(name));
if (key) return stubs[key];
if (request.startsWith("@config/")) return { API_BASE_URL: "http://fake" };
if (request.startsWith("@ui/")) return ui();
if (request.endsWith(".css")) return {};
if (request.endsWith(".json")) return require(path.join(path.dirname(full), request));
if (request.startsWith("@util/"))
return loadTs(path.join(root, "common_util", `${request.slice(6)}.ts`));
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
return { loadTs, load: (...parts) => loadTs(path.join(root, ...parts)) };
}
module.exports = { makeLoader, memoryStorage };
+64
View File
@@ -0,0 +1,64 @@
"""검증 프로젝트 찾기 — 읽기만 하는 시험이 필요한 파일을 가진 storage 프로젝트 하나를 고름(507-4).
옛 시험은 지워진 검증_sub7 의 uuid 를 박아 늘 건너뜀 → 지금 storage 에 있는 프로젝트 가운데
OWNERS.md 의 검증_subN 가운데 필요한 파일이 다 있고 가장 오래 안 바뀐 것을 씀(쓰는 중인 프로젝트 피함). `AISLO_TEST_PROJECT=<uuid>` 로 고정 가능. 없으면
`NONE` 경로(존재하지 않음) 를 돌려줘 시험이 `SKIP_REASON` 글로 건너뜀. 쓰는 시험 금지(읽기 전용).
"""
from __future__ import annotations
import os
import re
import pytest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
NONE = ROOT / "storage" / "_no_verify_project"
SKIP_REASON = "검증 프로젝트 없음 — storage/*/*/ 에 필요한 파일(격자 · 종단 · 노선 · 관)을 다 가진 프로젝트가 없음"
#: B05 노선 · B06 상세 · 관 · 격자를 쓰는 349 계열 시험이 읽는 파일
ROUTE_SET = (
"B04_PreProcess/models/dtm_classification_smooth.npz",
"B06_Section/longitudinal/longitudinal.json",
"B05_Profile/route/planned_route_curves.json",
"B05_Profile/route/planned_route.csv",
"B05_Profile/route/planned_route_nodes.csv",
"B04_PreProcess/drainage/edits/pipe_points.json",
)
def _verify_ids() -> set[str]:
"""OWNERS.md 표의 검증_subN 프로젝트 uuid — 사용자 프로젝트(엄상준_검증 등)는 시험이 안 열음."""
text = (ROOT / "OWNERS.md").read_text(encoding="utf-8")
return set(re.findall(r"`검증_sub\d+`\s*\|\s*`([0-9a-f-]{36})`", text))
def _touched(project: Path, rels: tuple[str, ...]) -> float:
"""가장 늦게 바뀐 때 — 다른 창이 쓰는 중인 프로젝트(최근)는 뒤로."""
sections = project / "B06_Section" / "cross_sections"
stamps = [(project / rel).stat().st_mtime for rel in rels] + [sections.stat().st_mtime]
return max(stamps + [f.stat().st_mtime for f in sections.glob("*.json")])
def find_project(*rels: str) -> Path:
pinned = os.environ.get("AISLO_TEST_PROJECT")
candidates = (
list((ROOT / "storage").glob(f"*/*/{pinned}"))
if pinned
else [p for p in (ROOT / "storage").glob("*/*/*") if p.name in _verify_ids()]
)
found = [
project
for project in candidates
if all((project / rel).is_file() for rel in rels)
and (project / "B06_Section" / "cross_sections").is_dir()
]
return min(found, key=lambda project: _touched(project, rels), default=NONE)
#: 검증 프로젝트로 처음 돌려 본 때(507-4) 이미 빨강 — 대부분 프로젝트와 무관(여섯 프로젝트 같음) · 옛 기대가 낡았는지 제품 흠인지 가려질 때까지(0q 메아리 = 돌 때마다 흔들림)
DRIFT = pytest.mark.xfail(
strict=False,
reason="507-4: 검증 프로젝트로 돌리니 빨강 · 흔들림 — 기대 낡음 또는 제품 흠(브레인 가림 전)",
)
@@ -18,21 +18,26 @@ import pytest
ROOT = Path(__file__).resolve().parents[2]
BASE = "http://localhost:5177"
API = "http://localhost:8004" # 5177 이 프록시하는 이 폴더 백엔드(OWNERS.md) — 다른 폴더로 가면 그 폴더 마스터를 씀
BOARD = ROOT / "resources" / "master_template" / "calc_sites.json"
PROJECT = "cb15892a-5788-4a26-9316-20a15421f68b" # 검증_sub2(OWNERS.md)
SHOTS = ROOT / "tmp" / "shots_504_2"
CARD = "계산 자리"
def _up() -> bool:
def _started(base: str) -> str | None:
try:
urllib.request.urlopen(BASE, timeout=3)
return True
with urllib.request.urlopen(f"{base}/api/health", timeout=3) as res:
return json.loads(res.read()).get("started_at")
except Exception:
return False
return None
pytestmark = pytest.mark.skipif(not _up(), reason="5177 서버 없음")
_UP = _started(BASE)
# 5177 이 이 폴더 8004 가 아닌 서버(예 8000 = 메인 폴더)로 가면 PUT 이 남의 판을 고침 → 돌리지 않음
pytestmark = pytest.mark.skipif(
_UP is None or _UP != _started(API), reason="5177 서버 없음 · 5177 이 8004 로 안 감"
)
def _cred(who: str) -> dict:
@@ -51,9 +56,11 @@ def browser():
).stdout # fmt: skip
with sync.sync_playwright() as play:
chrome = play.chromium.launch(channel="msedge", headless=True)
yield chrome
chrome.close()
BOARD.write_bytes(committed) # 판 = 커밋한 기본값 그대로
try:
yield chrome
finally: # 실패해도 판 = 커밋한 기본값 그대로
chrome.close()
BOARD.write_bytes(committed)
def _context(browser, who: str):
@@ -131,6 +138,8 @@ def test_시스템_관리자_카드_창_G1_서버_길(browser):
assert "지금 서버 1 줄" in dialog.inner_text()
page.locator(".b01-dashboard__calc-sites .ui-modal__actions button").click()
page.wait_for_selector(".b01-dashboard__calc-sites", state="detached", timeout=30000)
got = ctx.request.get(f"{BASE}/api/dashboard/calc-sites").json()["sites"]
assert got["ground_sample"] == "server", (got, BOARD.read_text("utf-8"))
assert json.loads(BOARD.read_text("utf-8"))["sites"]["ground_sample"] == "server"
logs = ctx.request.get(f"{BASE}/api/dashboard/admin/audit-logs").json()["items"]
assert any(
@@ -31,6 +31,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile import B05_Profile_Engine_Sections_Core as core # noqa: E402
from B05_Profile.B05_Profile_Engine_Grade import resolve_grade_options # noqa: E402
from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402
@@ -50,7 +52,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
REAL_GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
REAL_NODES = PROJECT / "B05_Profile/route/planned_route_nodes.csv"
MIN_RADIUS_M = 12.0
@@ -96,7 +98,7 @@ def _fake(folder: Path) -> tuple[Path, list[list[float]]]:
def _real(_folder: Path) -> tuple[Path, list[list[float]]]:
if not (REAL_GRID.is_file() and REAL_NODES.is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
with REAL_NODES.open(encoding="utf-8") as handle:
return REAL_GRID, [[float(r["x"]), float(r["y"])] for r in csv.DictReader(handle)]
+6 -2
View File
@@ -26,6 +26,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
@@ -35,7 +37,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
ROUTE = PROJECT / "B05_Profile/route"
SECTION = PROJECT / "B06_Section"
GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
@@ -79,7 +81,7 @@ def _structure(sid: str, line: list[list[float]], ratio: float, off: float) -> d
@pytest.fixture(scope="module")
def r() -> dict:
if not (GRID.is_file() and (SECTION / "longitudinal/longitudinal.json").is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
curves = _json(ROUTE / "planned_route_curves.json")
sections = sorted(
(_json(p) for p in (SECTION / "cross_sections").glob("cross_*.json")),
@@ -132,6 +134,7 @@ def test_열기만_하면_가게_안_씀(r):
assert o["followed"] == []
@DRIFT
def test_꼭짓점_끌기_가게에_새_측점_한_번(r):
d = r["drag"]
assert d["runs"] == [["route"]], d["runs"]
@@ -144,6 +147,7 @@ def test_꼭짓점_끌기_가게에_새_측점_한_번(r):
assert d["blocked"] == 0
@DRIFT
def test_되돌리기_초안_전_값(r):
u = r["undo"]
assert u["runs"] == [["route"]], u["runs"]
@@ -23,6 +23,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
@@ -32,7 +34,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
ROUTE = PROJECT / "B05_Profile/route"
SECTION = PROJECT / "B06_Section"
GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
@@ -51,7 +53,7 @@ def _json(path: Path):
@pytest.fixture(scope="module")
def r() -> dict:
if not (GRID.is_file() and (SECTION / "longitudinal/longitudinal.json").is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
curves = _json(ROUTE / "planned_route_curves.json")
sections = sorted(
(_json(p) for p in (SECTION / "cross_sections").glob("cross_*.json")),
+4 -2
View File
@@ -22,6 +22,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
@@ -31,7 +33,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
ROUTE = PROJECT / "B05_Profile/route"
SECTION = PROJECT / "B06_Section"
GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
@@ -50,7 +52,7 @@ def _json(path: Path):
@pytest.fixture(scope="module")
def r() -> dict:
if not (GRID.is_file() and (SECTION / "longitudinal/longitudinal.json").is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
curves = _json(ROUTE / "planned_route_curves.json")
sections = sorted(
(_json(p) for p in (SECTION / "cross_sections").glob("cross_*.json")),
+5 -2
View File
@@ -23,6 +23,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
@@ -32,7 +34,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
ROUTE = PROJECT / "B05_Profile/route"
SECTION = PROJECT / "B06_Section"
GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
@@ -51,7 +53,7 @@ def _json(path: Path):
@pytest.fixture(scope="module")
def r() -> dict:
if not (GRID.is_file() and (SECTION / "longitudinal/longitudinal.json").is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
curves = _json(ROUTE / "planned_route_curves.json")
sections = sorted(
(_json(p) for p in (SECTION / "cross_sections").glob("cross_*.json")),
@@ -128,6 +130,7 @@ def test_끌기_새_측점(r):
assert d["final"] != r["open"]["final"]
@DRIFT
def test_되돌리기_열기_값_다시_셈_0(r):
u, o = r["undo"], r["open"]
assert [(x["sources"], x["restored"], x["why"]) for x in u["runs"]] == [
+7 -2
View File
@@ -25,6 +25,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
@@ -34,7 +36,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
ROUTE = PROJECT / "B05_Profile/route"
SECTION = PROJECT / "B06_Section"
GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
@@ -64,7 +66,7 @@ def _between(sections: list[dict], index: int) -> dict:
@pytest.fixture(scope="module")
def r() -> dict:
if not (GRID.is_file() and (SECTION / "longitudinal/longitudinal.json").is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
curves = _json(ROUTE / "planned_route_curves.json")
sections = sorted(
(_json(p) for p in (SECTION / "cross_sections").glob("cross_*.json")),
@@ -147,6 +149,7 @@ def test_열기(r):
assert o["final"] is not None
@DRIFT
def test_끌기_새_노선(r):
d = r["first"]
assert [x["sources"] for x in d["runs"]] == [["route"]], d["runs"]
@@ -154,6 +157,7 @@ def test_끌기_새_노선(r):
assert d["final"] != r["open"]["final"]
@DRIFT
def test_되돌리기_열기_값_측점_다름은_기록(r):
o = r["open"]
for u in (r["back1"], r["back2"]):
@@ -185,6 +189,7 @@ def test_다시_하기_첫_끌기_값(r):
assert d["stations"] == f["stations"]
@DRIFT
def test_횡단_보기_다녀와도_열기_값(r):
o = r["open"]
for step in (r["cross"], r["back"]):
+5 -2
View File
@@ -24,6 +24,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
@@ -33,7 +35,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
ROUTE = PROJECT / "B05_Profile/route"
SECTION = PROJECT / "B06_Section"
GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
@@ -63,7 +65,7 @@ def _between(sections: list[dict], index: int) -> dict:
def build_job() -> dict:
"""입력 한 벌 — 356-4 시험도 빌림."""
if not (GRID.is_file() and (SECTION / "longitudinal/longitudinal.json").is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
curves = _json(ROUTE / "planned_route_curves.json")
sections = sorted(
(_json(p) for p in (SECTION / "cross_sections").glob("cross_*.json")),
@@ -159,6 +161,7 @@ def test_되돌리기_열기_객체_그대로(r):
assert d["pipeline"] > d["saved"] and isinstance(d["pipelineM"], float)
@DRIFT
def test_메아리_차례_갈아끼움_없음_열기_값(r):
o, e = r["open"], r["echo"]
assert [x["outcome"] for x in e["runs"]], e["runs"] # 묶음이 실제로 돎
+5 -2
View File
@@ -23,6 +23,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
@@ -32,7 +34,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
ROUTE = PROJECT / "B05_Profile/route"
SECTION = PROJECT / "B06_Section"
GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
@@ -62,7 +64,7 @@ def _between(sections: list[dict], index: int) -> dict:
@pytest.fixture(scope="module")
def r() -> dict:
if not (GRID.is_file() and (SECTION / "longitudinal/longitudinal.json").is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
curves = _json(ROUTE / "planned_route_curves.json")
sections = sorted(
(_json(p) for p in (SECTION / "cross_sections").glob("cross_*.json")),
@@ -147,6 +149,7 @@ def test_그리기_몫_기록(r):
assert x["totalMs"] >= x["stagesMs"] * 0.99
@DRIFT
def test_되돌리기_그대로(r):
u = r["undo"]
assert [x["why"] for x in u["runs"]] == ["saved-diff"], u["runs"] # 세월교 하나 더(0o 꼴)
@@ -23,6 +23,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from common_util.common_util_drainage_pipes import ( # noqa: E402
PipePoint,
@@ -37,7 +39,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
ROUTE = PROJECT / "B05_Profile/route"
SECTION = PROJECT / "B06_Section"
GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
@@ -81,7 +83,7 @@ def _structure(sid: str, line: list[list[float]], ratio: float, off: float) -> d
@pytest.fixture(scope="module")
def r() -> dict:
if not (GRID.is_file() and (SECTION / "longitudinal/longitudinal.json").is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
curves = _json(ROUTE / "planned_route_curves.json")
sections = sorted(
(_json(p) for p in (SECTION / "cross_sections").glob("cross_*.json")),
@@ -139,6 +141,7 @@ def _moved(before: list, after: list) -> list[float]:
return [b[1] - a[1] for a, b in zip(before, after)]
@DRIFT
def test_앞쪽_꺾임점_끌기_측점_밀림(r):
d = r["drag"]
assert d["runs"] == [["route"]], d["runs"]
@@ -155,6 +158,7 @@ def test_앞쪽_꺾임점_끌기_측점_밀림(r):
assert len(d["followed"]) == 1
@DRIFT
def test_되돌리기_초안_값(r):
for u in (r["undo"], r["undo2"]):
assert u["runs"] == [["route"]], u["runs"]
+6 -2
View File
@@ -25,6 +25,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from common_util.common_util_drainage_pipes import ( # noqa: E402
parse_pipe_points,
project_pipe_points,
@@ -36,7 +38,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
PIPES = PROJECT / "B04_PreProcess/drainage/edits/pipe_points.json"
LINE = PROJECT / "B05_Profile/route/planned_route.csv"
@@ -52,10 +54,12 @@ def _vertices(line: list[list[float]]) -> list[RouteVertex]:
@pytest.fixture(scope="module")
def case() -> dict:
if not (PIPES.is_file() and LINE.is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
with LINE.open(encoding="utf-8") as handle:
line = [[float(r["x"]), float(r["y"])] for r in csv.DictReader(handle)]
raw = json.loads(PIPES.read_text(encoding="utf-8"))["points"]
if len(raw) < 6: # 시험이 관 여섯 개째(p5)까지 만짐
pytest.skip("검증 프로젝트 관 6 개 미만")
before = [{**pipe, "id": f"p{i}"} for i, pipe in enumerate(raw)]
# 서버 노선 저장 — 앞쪽 꼭짓점을 서쪽으로 크게 옮긴 새 노선에 좌표 투영 + 새 교차 관 하나
moved = copy.deepcopy(line)
+6 -2
View File
@@ -23,6 +23,8 @@ ROOT = HERE.parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
@@ -32,7 +34,7 @@ pytestmark = pytest.mark.skipif(
reason="프론트엔드 의존성 미설치",
)
PROJECT = ROOT / "storage/1/3/6d3b2812-6d5a-4168-a932-40e4ae303d5a" # 검증_sub7
PROJECT = find_project(*ROUTE_SET) # 지금 있는 검증 프로젝트(읽기만)
ROUTE = PROJECT / "B05_Profile/route"
SECTION = PROJECT / "B06_Section"
GRID = PROJECT / "B04_PreProcess/models/dtm_classification_smooth.npz"
@@ -51,7 +53,7 @@ def _json(path: Path):
@pytest.fixture(scope="module")
def r() -> dict:
if not (GRID.is_file() and (SECTION / "longitudinal/longitudinal.json").is_file()):
pytest.skip("검증_sub7 없음")
pytest.skip(SKIP_REASON)
curves = _json(ROUTE / "planned_route_curves.json")
sections = sorted(
(_json(p) for p in (SECTION / "cross_sections").glob("cross_*.json")),
@@ -101,6 +103,7 @@ def _same(a: list[float], b: list[float]) -> bool:
return len(a) == len(b) and all(abs(x - y) < 1e-6 for x, y in zip(a, b))
@DRIFT
def test_열_때_한_번_저장_측점_그대로(r):
o = r["open"]
assert [x["outcome"] for x in o["runs"]] == ["done"], o["runs"]
@@ -118,6 +121,7 @@ def test_꼭짓점_끌기_한_번(r):
assert d["swaps"] == 1 # 새 노선 측점으로 갈아끼움은 한 번(고리 아님)
@DRIFT
def test_되돌리기_한_번_저장값으로(r):
u = r["undo"]
assert [x["sources"] for x in u["runs"]] == [["route"]], u["runs"]
@@ -11,14 +11,16 @@
from __future__ import annotations
import json
import os
import urllib.request
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
BASE = "http://localhost:5179"
PROJECT = "c725d747-50ae-46b0-87ce-4d4e56a53cf3" # 검증_sub4(OWNERS.md)
# 기본 = sub4 환경 · 다른 창은 AISLO_505_3_BASE · AISLO_505_3_PROJECT 로 바꿔 test_0 만 돌림(test_1 · 2 는 sub4 자료)
BASE = os.environ.get("AISLO_505_3_BASE", "http://localhost:5179")
PROJECT = os.environ.get("AISLO_505_3_PROJECT", "c725d747-50ae-46b0-87ce-4d4e56a53cf3") # 검증_sub4
SHOTS = ROOT / "tmp" / "shots_505_3"
PIPE_M = 388 # 검증_sub4 배관 하나(관경 빈칸 · 19+8)
PREFS = "/api/dashboard/me/ui-prefs"
@@ -34,7 +36,7 @@ def _up() -> bool:
return False
pytestmark = pytest.mark.skipif(not _up(), reason="5179 서버 없음")
pytestmark = pytest.mark.skipif(not _up(), reason="서버 없음")
# 측점 m 을 가로 가운데로 · 그 측점선과 원지반선 교점(화면 좌표) · 그 자리 맨 위 요소
JS_SCROLL = r"""(m) => {
@@ -59,6 +61,26 @@ JS_POINT = r"""(m) => {
const s = pt.matrixTransform(poly.getScreenCTM());
return { x: s.x, y: s.y, hit: document.elementFromPoint(s.x, s.y)?.getAttribute('class') };
}"""
# 지도 캔버스에서 교각점 원(--map-route-node 색)을 찾아 화면 좌표로 — 픽셀 고정 자리 대신 요소 · 색으로
JS_NODES = r"""() => {
const canvas = document.querySelector('.b05-routeedit__canvas');
const box = canvas.getBoundingClientRect();
const hex = getComputedStyle(canvas).getPropertyValue('--map-route-node').trim() || '#15803d';
const want = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
const sx = canvas.width / box.width;
const data = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
const cell = 14, cells = new Map();
for (let y = 0; y < canvas.height; y++) for (let x = 0; x < canvas.width; x++) {
const i = (y * canvas.width + x) * 4;
if (Math.abs(data[i] - want[0]) + Math.abs(data[i + 1] - want[1]) + Math.abs(data[i + 2] - want[2]) > 12) continue;
const key = `${Math.floor(x / cell)},${Math.floor(y / cell)}`;
const c = cells.get(key) ?? { n: 0, x: 0, y: 0 };
c.n++; c.x += x; c.y += y; cells.set(key, c);
}
return [...cells.values()].filter((c) => c.n > 60)
.map((c) => ({ x: box.left + c.x / c.n / sx, y: box.top + c.y / c.n / sx }))
.filter((p) => document.elementFromPoint(p.x, p.y) === canvas);
}"""
JS_MODAL = r"""() => {
const r = document.querySelector('.b05-crossmodal');
const card = r.querySelector('.b06-cross-card');
@@ -145,17 +167,29 @@ def test_0_곡선_판_도구줄_횡단판_비킴(page):
page.locator(".b05-routeedit__side-wrap").get_by_text("횡단").first.click()
page.wait_for_timeout(2000)
shown = "() => [...document.querySelectorAll('.b05-routeedit__label')].some((x) => !x.hidden)"
for _ in range(3): # 첫 누름이 앞 고름(구조물) 풂으로 갈 수 있음
page.mouse.click(1199, 296) # 검증_sub4 첫 화면의 14번째 꺾임점
page.wait_for_timeout(1500)
nodes = page.evaluate(JS_NODES)
assert nodes, "지도에서 교각점을 못 찾음"
for node in nodes[1:-1] or nodes: # 양끝(시점 · 종점) 말고 곡선 있는 가운데 점
for _ in range(3): # 첫 누름이 앞 고름(구조물) 풂으로 갈 수 있음
page.mouse.click(node["x"], node["y"])
page.wait_for_timeout(1500)
if page.evaluate(shown):
break
if page.evaluate(shown):
break
page.screenshot(path=str(SHOTS / "curve_pick.png"))
assert page.evaluate(shown)
# 지도를 위 · 오른쪽으로 밀어 꺾임점을 도구줄 · 오른쪽 끝 밑으로
page.mouse.move(800, 650)
# 지도를 밀어 고른 꺾임점을 도구줄 · 오른쪽 끝 밑으로
canvas = page.locator(".b05-routeedit__canvas").bounding_box()
tools = page.evaluate(JS_RECTS)["tools"]
goal = (canvas["x"] + canvas["width"] - 60, tools[3] + 40)
page.mouse.move(*(canvas["x"] + canvas["width"] / 2, canvas["y"] + canvas["height"] * 0.65))
page.mouse.down(button="middle")
page.mouse.move(1050, 380, steps=16)
page.mouse.move(
canvas["x"] + canvas["width"] / 2 + goal[0] - node["x"],
canvas["y"] + canvas["height"] * 0.65 + goal[1] - node["y"],
steps=16,
)
page.mouse.up(button="middle")
page.wait_for_timeout(1200)
rects = page.evaluate(JS_RECTS)
@@ -0,0 +1,29 @@
"""507-4 — 공용 TS 불러오기 `helper_ts_stub_loader.cjs`: 진짜 TS 를 불러 가짜 @config · fetch 로 돎.
./venv/Scripts/python.exe -m pytest resources/tester/test_507_4_ts_stub_loader.py -q
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
HERE = Path(__file__).resolve().parent
TSC = HERE.parents[1] / "config" / "node_modules" / "typescript"
@pytest.mark.skipif(
shutil.which("node") is None or not TSC.is_dir(), reason="node · typescript 없음"
)
def test_공용_로더가_진짜_TS_불러_가짜_주소로_판_읽음():
run = subprocess.run(
["node", str(HERE / "helper_507_4_loader_smoke.cjs")],
capture_output=True, text=True, encoding="utf-8", timeout=60, check=True,
) # fmt: skip
got = json.loads(run.stdout)
assert got["ground"] is True and got["corridor"] is False
assert got["calls"] == ["http://fake/dashboard/calc-sites"] # 판은 한 번만 받아 둠