feat(763-4): 추천 미리 셈 끊기면 이어 셈 · 화면 진행 n/m · 저장본 없으면 [지금 셈]
- 진행 기록 `B05_Profile/route_recommend_job.json`(저장본 폴더 밖 — 저장본 glob 에 안 섞임) · `note_job` 재사용 · 갈래마다 갱신 · 끝 = running 거짓 - 진행 묻기 = 이 서버 메모리 먼저 · 없으면 기록(10 분 안 = 다른 서버가 셈 · 넘음 = 끊김 `interrupted`) - 추천 요청 409 + 「끊김」 기록 = 이어 셈 예약 한 번(기록을 곧바로 「도는 중」으로 · 뒤 요청은 안 겹침) - 미리 셈 한 갈래 409 = 그 갈래만 건너뜀(옛: 전부 멈춤) - 409 글 = 「추천 저장본 없음 — 지표면이 바뀌어 다시 셈해야 함」(없는 [재계산] 단추 안내 걷음) - 화면(RecPreview) — 셈 중이면 줄 마우스 글 「추천 셈 중 n/m」 · 알림 한 번 · 3 초마다 다시 물어 끝나면 그림 · 아니면 확인창 [지금 셈](그 갈래만 recompute) · 지형 바뀌면 받아 둔 선 비움 - 시험 309_3 · 364_4 · 509_5 · 626_1 을 새 글 · 진행 꼴 · [지금 셈]에 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,8 @@ export interface PrecomputeProgress {
|
||||
done: number;
|
||||
total: number;
|
||||
mode: RouteInitialLogic | null;
|
||||
/** 기록은 「도는 중」인데 서버가 다시 켜져 끊김(763-4) — 추천 요청 409 가 이어 셈을 예약. */
|
||||
interrupted?: boolean;
|
||||
}
|
||||
|
||||
/** 미리 셈 진행 — 못 읽으면 null(진행 표시만 빠짐). */
|
||||
|
||||
@@ -26,8 +26,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# 미리 셈 진행(364-4) — 프로젝트마다 하나만 돎 · 도는 동안 또 오면 끝난 뒤 한 번 더(입력이 바뀜)
|
||||
_RUNNING: dict[str, dict[str, Any]] = {}
|
||||
# 미리 셈 갈래인데 저장본이 없을 때(505-1) — 프로젝트 접근은 셈하지 않음
|
||||
NOT_PREPARED = "추천 저장본이 없습니다 — 전처리를 다시 하면 생깁니다 · [재계산]이면 지금 셉니다."
|
||||
# 미리 셈 갈래인데 저장본이 없을 때(505-1) — 프로젝트 접근은 셈하지 않음 · 화면은 머리말로 가름
|
||||
# (`B05_Profile_UI_RouteEdit_RecPreview.ts` NOT_PREPARED_HEAD · 셈 중이면 진행 · 아니면 [지금 셈])
|
||||
NOT_PREPARED = "추천 저장본 없음 — 지표면이 바뀌어 다시 셈해야 함"
|
||||
# 미리 셈 진행 기록(763-4) — 재시동 뒤 「끊김」을 알게 · 저장본 폴더 밖(저장본 glob 에 안 섞임)
|
||||
JOB_FILE = Path("B05_Profile") / "route_recommend_job.json"
|
||||
# ponytail: 다른 서버(같은 storage)가 도는 중인지 = 기록 나이 · 갈래 하나 최대 ~2 분 · 넘으면 끊김
|
||||
JOB_STALE_S = 600.0
|
||||
|
||||
|
||||
@replan.router.post("/{project_id}/route/initial", response_model=None)
|
||||
@@ -172,6 +177,7 @@ async def compute_initial(
|
||||
)[0],
|
||||
)
|
||||
if found is None:
|
||||
resume_if_interrupted(project_id, project_root) # 끊긴 미리 셈 이어 셈(763-4)
|
||||
return JSONResponse(
|
||||
status_code=409, content={"status": "error", "message": NOT_PREPARED}
|
||||
)
|
||||
@@ -296,6 +302,9 @@ async def precompute_defaults(project_id: UUID) -> None:
|
||||
progress = _RUNNING.get(str(project_id))
|
||||
if progress is not None:
|
||||
progress.update(total=len(terrains) * len(variants) * len(modes))
|
||||
if paths is not None: # 진행 기록 자리 — 끝 기록은 `run_precompute`(763-4)
|
||||
progress["root"] = paths[0]
|
||||
_note(paths, progress)
|
||||
for terrain, (criteria, weights), mode in itertools.product(terrains, variants, modes):
|
||||
if progress is not None:
|
||||
progress["mode"] = mode
|
||||
@@ -308,17 +317,17 @@ async def precompute_defaults(project_id: UUID) -> None:
|
||||
terrain=terrain,
|
||||
)
|
||||
outcome = await compute_initial(project_id, request, precompute=True)
|
||||
if isinstance(outcome, JSONResponse):
|
||||
if isinstance(outcome, JSONResponse): # 그 갈래만 건너뜀(763-4)
|
||||
logger.warning(
|
||||
"추천 미리 셈 멈춤(%s %s %s): project_id=%s",
|
||||
"추천 미리 셈 갈래 건너뜀(%s %s %s): project_id=%s",
|
||||
terrain,
|
||||
mode,
|
||||
outcome.status_code,
|
||||
project_id,
|
||||
)
|
||||
return
|
||||
if progress is not None:
|
||||
progress["done"] += 1
|
||||
_note(paths, progress)
|
||||
logger.info("추천 미리 셈 끝: project_id=%s %.1fs", project_id, time.perf_counter() - started)
|
||||
|
||||
|
||||
@@ -343,8 +352,23 @@ async def precompute_terrains(project_id: UUID, project_root: Path) -> list[Any]
|
||||
return [design, *[source for source in sources if source != design]]
|
||||
|
||||
|
||||
def _note(paths: Any, progress: dict[str, Any], running: bool = True) -> None:
|
||||
"""진행 기록 쓰기(763-4) — 실패해도 셈은 계속."""
|
||||
from common_util.common_util_lidar_upload import note_job
|
||||
|
||||
if paths is not None:
|
||||
note_job(
|
||||
Path(paths[0]) / JOB_FILE,
|
||||
{
|
||||
"running": running,
|
||||
**{k: progress.get(k) for k in ("done", "total", "mode")},
|
||||
"updated": time.time(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def run_precompute(project_id: UUID) -> None:
|
||||
"""미리 셈 한 줄(364-4) — 이미 돌면 「한 번 더」만 적고 돌아감(겹쳐 셈 막음)."""
|
||||
"""미리 셈 한 줄(364-4) — 이미 돌면 「한 번 더」만 적고 돌아감(겹쳐 셈 막음) · 기록(763-4)."""
|
||||
key = str(project_id)
|
||||
if key in _RUNNING:
|
||||
_RUNNING[key]["again"] = True
|
||||
@@ -356,20 +380,62 @@ async def run_precompute(project_id: UUID) -> None:
|
||||
if not _RUNNING[key]["again"]:
|
||||
break
|
||||
finally:
|
||||
_RUNNING.pop(key, None)
|
||||
progress = _RUNNING.pop(key, {})
|
||||
if progress.get("root"):
|
||||
_note((progress["root"],), progress, running=False)
|
||||
|
||||
|
||||
def precompute_progress(project_id: UUID) -> dict[str, Any]:
|
||||
"""모달 진행 표시 — 돌고 있나 · 끝낸 수 / 셀 수 · 지금 갈래."""
|
||||
def precompute_progress(project_id: UUID, project_root: Path | None = None) -> dict[str, Any]:
|
||||
"""진행 표시 — 돌고 있나 · 끝낸 수 / 셀 수 · 지금 갈래 · 끊겼나(763-4).
|
||||
|
||||
이 서버 메모리가 먼저 · 없으면 기록: 「도는 중」이 `JOB_STALE_S` 안 = 다른 서버가 셈 ·
|
||||
넘음 = 끊김."""
|
||||
progress = _RUNNING.get(str(project_id))
|
||||
if progress is None:
|
||||
return {"running": False, "done": 0, "total": 0, "mode": None}
|
||||
return {"running": True, **{k: progress[k] for k in ("done", "total", "mode")}}
|
||||
if progress is not None:
|
||||
return {
|
||||
"running": True,
|
||||
"interrupted": False,
|
||||
**{k: progress[k] for k in ("done", "total", "mode")},
|
||||
}
|
||||
idle = {"running": False, "interrupted": False, "done": 0, "total": 0, "mode": None}
|
||||
if project_root is None:
|
||||
return idle
|
||||
try:
|
||||
saved = json.loads((Path(project_root) / JOB_FILE).read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return idle
|
||||
if not isinstance(saved, dict) or not saved.get("running"):
|
||||
return idle
|
||||
fresh = time.time() - float(saved.get("updated") or 0) < JOB_STALE_S
|
||||
return {
|
||||
**idle,
|
||||
**{k: saved.get(k) for k in ("done", "total", "mode")},
|
||||
"running": fresh,
|
||||
"interrupted": not fresh,
|
||||
}
|
||||
|
||||
|
||||
def resume_if_interrupted(project_id: UUID, project_root: Path) -> bool:
|
||||
"""추천 요청이 409 인데 기록이 「끊김」 — 이 서버가 이어 셈 예약 한 번(돌면 안 겹침 · 763-4).
|
||||
|
||||
이어 셈 = 미리 셈 다시(있는 갈래는 저장본 꺼내기만 · 0.2 s 안팎)."""
|
||||
state = precompute_progress(project_id, project_root)
|
||||
if not state["interrupted"]:
|
||||
return False
|
||||
_note((project_root,), state) # 곧바로 「도는 중」 — 뒤 요청이 또 예약하지 않게
|
||||
schedule_precompute(project_id)
|
||||
logger.info("추천 미리 셈 이어 셈 예약(끊김 기록): project_id=%s", project_id)
|
||||
return True
|
||||
|
||||
|
||||
@replan.router.get("/{project_id}/route/initial/progress", response_model=None)
|
||||
async def initial_progress(project_id: UUID) -> dict[str, Any]:
|
||||
return {"status": "success", **precompute_progress(project_id)}
|
||||
try:
|
||||
paths = await replan._project_paths(project_id)
|
||||
except LookupError:
|
||||
paths = None
|
||||
root = paths[0] if paths is not None else None
|
||||
return {"status": "success", **precompute_progress(project_id, root)}
|
||||
|
||||
|
||||
def schedule_precompute(project_id: UUID) -> None:
|
||||
|
||||
@@ -4,20 +4,32 @@
|
||||
* (영선 · 토공 최소 · 기울기 최소 · 비중 계산) · 여럿 고름(589-12 · 줄마다 색 · 하나도 안 고르면 꺼짐) ·
|
||||
* 선만(위 캔버스 · 노선 아래 그림 · 집기 없음).
|
||||
* 선 = 노선 추천 저장본(`B05_Profile/route_recommend/` · 지금 설계 지형 · 체인이 미리 셈) 그대로 — 다시 셈 · 쓰기 없음 ·
|
||||
* 저장본 없으면(409) 알림 · 끔. 옛 추천 모달 · 좌측 「노선 추천」 칸 · [적용](`as_initial`) 은 걷음(서버 길 · 시험은 남김).
|
||||
* 저장본 없으면(409) — 미리 셈 도는 중 = 진행 n/m(줄 마우스 글 · 알림 한 번) · 끝나면 다시 그림 ·
|
||||
* 아니면 확인창 「지표면이 바뀌어 다시 셈해야 함」 [지금 셈](그 갈래만 `recompute` · 763-4) ·
|
||||
* 지형이 바뀌면 받아 둔 선을 비움. 옛 추천 모달 · 좌측 「노선 추천」 칸 · [적용](`as_initial`) 은 걷음(서버 길 · 시험은 남김).
|
||||
* 부르는 곳 = `_Chrome`(도구줄 `recommend` 칸) · `_RouteEdit`(그리기 `overStations` 앞).
|
||||
* 메뉴 · 선 그리기 = 공용 `A00_Common/map/map_route_preview`(636-2 · 예정노선 가이드 선과 같이 씀).
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { createRoutePreviewMenu, paintRoutePreview } from "../A00_Common/map/map_route_preview";
|
||||
import type { AnalysisTerrain } from "./B05_Profile_Api_Replan";
|
||||
import { fetchRouteInitial, type RouteInitialLogic } from "./B05_Profile_Api_RouteInitial";
|
||||
import {
|
||||
fetchPrecomputeProgress,
|
||||
fetchRouteInitial,
|
||||
type RouteInitialLogic,
|
||||
} from "./B05_Profile_Api_RouteInitial";
|
||||
import { LOGIC_LABEL } from "./B05_Profile_UI_RouteEdit_Initial_Model";
|
||||
|
||||
type Vertex = [number, number];
|
||||
|
||||
/** 서버 `NOT_PREPARED` 머리말(`B05_Profile_Router_RouteInitial.py`) — 저장본 없음 가름(763-4). */
|
||||
const NOT_PREPARED_HEAD = "추천 저장본 없음";
|
||||
/** 미리 셈 진행 묻기 간격(ms). */
|
||||
const POLL_MS = 3000;
|
||||
|
||||
/** 드롭다운 넷(요구 530 · 「예상노선 추적」 = 초기값이라 뺌). */
|
||||
const MODES = [
|
||||
"gradeline",
|
||||
@@ -54,6 +66,8 @@ export function createRecPreview(opts: {
|
||||
const lines = new Map<string, Vertex[]>();
|
||||
const shown = new Map<RouteInitialLogic, Vertex[]>();
|
||||
const turns = new Map<RouteInitialLogic, number>();
|
||||
const waiting = new Set<RouteInitialLogic>();
|
||||
let seenTerrain = opts.terrain();
|
||||
const menu = createRoutePreviewMenu({
|
||||
items: MODES.map((mode, index) => ({
|
||||
value: mode,
|
||||
@@ -63,32 +77,67 @@ export function createRecPreview(opts: {
|
||||
onPick: (mode) => void pick(mode as RouteInitialLogic),
|
||||
});
|
||||
|
||||
async function pick(mode: RouteInitialLogic): Promise<void> {
|
||||
/** 미리 셈이 도는 중이면 진행 n/m 을 보이고 끝난 뒤 다시 고름 — 돌면 true(763-4). */
|
||||
async function waitForPrecompute(mode: RouteInitialLogic, mine: number): Promise<boolean> {
|
||||
const progress = await fetchPrecomputeProgress(opts.projectId);
|
||||
if (!progress?.running || mine !== turns.get(mode)) return false;
|
||||
const note = `추천 셈 중 ${progress.done}/${progress.total}`;
|
||||
menu.setItem(mode, { title: note });
|
||||
if (!waiting.has(mode)) showToast(`${note} — 끝나면 그림`, "info");
|
||||
waiting.add(mode);
|
||||
window.setTimeout(() => {
|
||||
if (mine === turns.get(mode) && menu.values().includes(mode)) void pick(mode);
|
||||
}, POLL_MS);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function pick(mode: RouteInitialLogic, recompute = false): Promise<void> {
|
||||
const mine = (turns.get(mode) ?? 0) + 1;
|
||||
turns.set(mode, mine);
|
||||
shown.delete(mode);
|
||||
opts.redraw();
|
||||
if (!menu.values().includes(mode)) return;
|
||||
const key = `${mode}|${opts.terrain()}`;
|
||||
const terrain = opts.terrain();
|
||||
if (terrain !== seenTerrain) lines.clear(); // 지형 바뀜 = 옛 선 버림(763-4)
|
||||
seenTerrain = terrain;
|
||||
const key = `${mode}|${terrain}`;
|
||||
try {
|
||||
if (!lines.has(key)) {
|
||||
if (!lines.has(key) || recompute) {
|
||||
const body = (await fetchRouteInitial(opts.projectId, {
|
||||
mode,
|
||||
with_profile: true,
|
||||
terrain: opts.terrain(),
|
||||
terrain,
|
||||
...(recompute ? { recompute: true } : {}),
|
||||
})) as { planned?: number[][] };
|
||||
const planned = (body.planned ?? []).map(([x, y]) => [x, y] as Vertex);
|
||||
if (planned.length < 2) throw new Error("추천 선이 비었음");
|
||||
lines.set(key, planned);
|
||||
}
|
||||
if (mine !== turns.get(mode)) return;
|
||||
waiting.delete(mode);
|
||||
menu.setItem(mode, { title: "" });
|
||||
shown.set(mode, lines.get(key)!);
|
||||
opts.redraw();
|
||||
} catch (error) {
|
||||
if (mine !== turns.get(mode)) return;
|
||||
menu.setItem(mode, { checked: false });
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
showToast(`추천 노선 「${LOGIC_LABEL[mode]}」 못 봄 — ${text}`, "info");
|
||||
if (!recompute && (await waitForPrecompute(mode, mine))) return;
|
||||
waiting.delete(mode);
|
||||
menu.setItem(mode, { checked: false, title: "" });
|
||||
const now =
|
||||
text.startsWith(NOT_PREPARED_HEAD) &&
|
||||
(await askConfirm({
|
||||
title: "추천 노선",
|
||||
message: `「${LOGIC_LABEL[mode]}」 ${text}. 지금 셀까요? 몇 분 걸릴 수 있습니다.`,
|
||||
okLabel: "지금 셈",
|
||||
}));
|
||||
if (now) {
|
||||
menu.setItem(mode, { checked: true });
|
||||
showToast(`추천 노선 「${LOGIC_LABEL[mode]}」 셈 중 — 끝나면 그림`, "info");
|
||||
return void pick(mode, true);
|
||||
}
|
||||
if (!text.startsWith(NOT_PREPARED_HEAD))
|
||||
showToast(`추천 노선 「${LOGIC_LABEL[mode]}」 못 봄 — ${text}`, "info");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ def test_화면_기본_갈래_저장본_없으면_409_셈_0(wired) -> None: # n
|
||||
for mode in DEFAULT_MODES:
|
||||
response = _ask(False, mode=mode)
|
||||
assert response.status_code == 409
|
||||
assert "전처리를 다시 하면 생깁니다" in json.loads(response.body)["message"]
|
||||
assert json.loads(response.body)["message"] == router.NOT_PREPARED
|
||||
assert wired.count == 0 and not list(store_dir(wired.root).glob("*.json"))
|
||||
|
||||
|
||||
|
||||
@@ -40,9 +40,16 @@ def test_겹쳐_셈_막음_끝난_뒤_한_번_더(monkeypatch) -> None:
|
||||
|
||||
asyncio.run(main())
|
||||
assert calls == [str(pid), str(pid)]
|
||||
assert seen[0] == {"running": True, "done": 2, "total": 5, "mode": "earthwork"}
|
||||
assert seen[0] == {
|
||||
"running": True,
|
||||
"interrupted": False, # 끊김 기록(763-4)
|
||||
"done": 2,
|
||||
"total": 5,
|
||||
"mode": "earthwork",
|
||||
}
|
||||
assert router.precompute_progress(pid) == {
|
||||
"running": False,
|
||||
"interrupted": False,
|
||||
"done": 0,
|
||||
"total": 0,
|
||||
"mode": None,
|
||||
|
||||
@@ -63,7 +63,7 @@ def test_미리_셈_없는_지형은_안내_재계산은_셈(two) -> None:
|
||||
missing = _ask(False, mode="gentle", terrain="sheet")
|
||||
assert missing.status_code == 409
|
||||
message = json.loads(missing.body)["message"]
|
||||
assert "전처리를 다시 하면" in message and "[재계산]" in message
|
||||
assert message == router.NOT_PREPARED # 화면 = 진행 또는 [지금 셈](763-4)
|
||||
assert two.count == 0
|
||||
assert _ask(False, mode="gentle", terrain="sheet", recompute=True)["stored"] is False
|
||||
assert two.count == 1
|
||||
|
||||
@@ -42,10 +42,11 @@ def test_미리보기_넷_하나만_저장본만() -> None:
|
||||
src = read(B05 / "B05_Profile_UI_RouteEdit_RecPreview.ts")
|
||||
modes = re.search(r"const MODES = \[(.*?)\]", src, re.S).group(1)
|
||||
assert re.findall(r'"(\w+)"', modes) == ["gradeline", "earthwork", "gentle", "weighted"]
|
||||
assert "recompute" not in src and "createRoutePreviewMenu({" in src
|
||||
# 저장본만 · 다시 셈은 저장본 없을 때 확인창 [지금 셈]만(763-4)
|
||||
assert src.count("recompute: true") == 1 and "createRoutePreviewMenu({" in src
|
||||
shared = read(ROOT / "A00_Common" / "map" / "map_route_preview.ts") # 636-2 공용 메뉴 · 그리기
|
||||
assert "createPickMenu({" in shared and "multi: true" in shared and "offLabel" not in shared
|
||||
assert "terrain: opts.terrain()" in src # 지금 설계 지형 저장본(D7)
|
||||
assert "const terrain = opts.terrain();" in src # 지금 설계 지형 저장본(D7)
|
||||
chrome = read(B05 / "B05_Profile_UI_RouteEdit_Chrome.ts")
|
||||
assert "recommend: preview.root" in chrome and "terrain: terrain.root" in chrome
|
||||
menu = read(ROOT / "ui_template" / "ui_template_context_menu.ts")
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""763-4 추천 미리 셈이 끊기면 이어 셈 — 진행 기록 · 한 갈래 409 는 그 갈래만 건너뜀 · 화면 글 짝.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_763_4_precompute_resume.py -q
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
from test_224_3_recommend_store import _ask, wired # noqa: F401 — 픽스처
|
||||
|
||||
from B05_Profile import B05_Profile_Router_RouteInitial as router
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Store import DEFAULT_MODES
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _record(root: Path, running: bool, age_s: float) -> None:
|
||||
path = root / router.JOB_FILE
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
body = {"running": running, "done": 3, "total": 10, "mode": "gentle"}
|
||||
path.write_text(json.dumps({**body, "updated": time.time() - age_s}), encoding="utf-8")
|
||||
|
||||
|
||||
def test_진행_기록_읽기(tmp_path) -> None:
|
||||
pid = uuid4()
|
||||
assert router.precompute_progress(pid, tmp_path)["running"] is False
|
||||
_record(tmp_path, True, 5) # 다른 서버가 도는 중
|
||||
state = router.precompute_progress(pid, tmp_path)
|
||||
assert state["running"] and not state["interrupted"] and state["done"] == 3
|
||||
_record(tmp_path, True, router.JOB_STALE_S + 1) # 재시동으로 끊김
|
||||
state = router.precompute_progress(pid, tmp_path)
|
||||
assert not state["running"] and state["interrupted"]
|
||||
_record(tmp_path, False, 5)
|
||||
assert router.precompute_progress(pid, tmp_path)["interrupted"] is False
|
||||
|
||||
|
||||
def test_끊김_기록_409_이어_셈_한_번(wired, monkeypatch) -> None: # noqa: F811
|
||||
scheduled: list = []
|
||||
monkeypatch.setattr(router, "schedule_precompute", scheduled.append)
|
||||
_record(wired.root, True, router.JOB_STALE_S + 1)
|
||||
for _ in range(3): # 화면이 여러 번 물어도 예약 한 번(기록이 곧바로 「도는 중」)
|
||||
answer = _ask(False, mode="gentle")
|
||||
assert answer.status_code == 409
|
||||
assert json.loads(answer.body)["message"] == router.NOT_PREPARED
|
||||
assert len(scheduled) == 1
|
||||
assert router.precompute_progress(uuid4(), wired.root)["running"] is True
|
||||
|
||||
|
||||
def test_끊김_아니면_예약_없음(wired, monkeypatch) -> None: # noqa: F811
|
||||
scheduled: list = []
|
||||
monkeypatch.setattr(router, "schedule_precompute", scheduled.append)
|
||||
_ask(False, mode="gentle") # 기록 없음
|
||||
_record(wired.root, True, 5) # 다른 서버가 셈 중 — 안 겹침
|
||||
_ask(False, mode="gentle")
|
||||
assert scheduled == []
|
||||
|
||||
|
||||
def test_미리_셈_기록_남기고_끝(wired) -> None: # noqa: F811
|
||||
asyncio.run(router.run_precompute(uuid4()))
|
||||
saved = json.loads((wired.root / router.JOB_FILE).read_text(encoding="utf-8"))
|
||||
assert saved["running"] is False and saved["done"] == saved["total"] == len(DEFAULT_MODES)
|
||||
|
||||
|
||||
def test_한_갈래_409_는_그_갈래만_건너뜀(wired, monkeypatch) -> None: # noqa: F811
|
||||
real = router.compute_initial
|
||||
asked: list[str] = []
|
||||
|
||||
async def flaky(project_id, request, precompute=False):
|
||||
asked.append(request.mode)
|
||||
if request.mode == "gentle":
|
||||
return JSONResponse(status_code=409, content={"message": "x"})
|
||||
return await real(project_id, request, precompute)
|
||||
|
||||
monkeypatch.setattr(router, "compute_initial", flaky)
|
||||
asyncio.run(router.precompute_defaults(uuid4()))
|
||||
assert sorted(asked) == sorted(DEFAULT_MODES)
|
||||
assert wired.count == len(DEFAULT_MODES) - 1
|
||||
|
||||
|
||||
def test_화면_머리말_서버_글과_같음() -> None:
|
||||
preview = (ROOT / "B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
head = preview.split('const NOT_PREPARED_HEAD = "', 1)[1].split('"', 1)[0]
|
||||
assert router.NOT_PREPARED.startswith(head)
|
||||
assert "fetchPrecomputeProgress(" in preview and "recompute: true" in preview
|
||||
assert 'okLabel: "지금 셈"' in preview and "lines.clear()" in preview
|
||||
assert "<button" not in preview # 부품 상자만(확인창 = ui_template_modal)
|
||||
Reference in New Issue
Block a user