Files
Aislo/resources/tester/test_765_design_sets.py
T
eomsangdonandClaude Opus 5.5 bdaca728fe fix(765-6): 초기값 사본 · 복원에서 예상노선(공유) 뺌 — 수치지형도 벌 [초기화]가 라이다 예상노선을 덮던 것
- 검증_sub5 실측: 칩으로 수치지형도 벌 → [초기화] → 칩 라이다 뒤 예상노선이 수치지형도로 자른 것(643 점 · 2136.2 m)으로 남음 · 라이다로 자른 것 = 415 점 · 1377.4 m
- 예상노선은 지형마다 벌이 있어도 하나(공유 · 추천이 씀) — 초기값 사본에 안 넣고 복원이 안 건드림(옛 사본 안 것도 건너뜀)
- 시험 test_765_design_sets 에 덮지 않음 검사 덧

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:12:24 +09:00

595 lines
25 KiB
Python

# -*- coding: utf-8 -*-
"""765-2 지형마다 설계 한 벌 — 떠 두기 · 꺼내 끼우기 · 맞바꿈(`common_util_design_sets`).
DB = 메모리 sqlite 를 aiomysql 꼴로 감싼 가짜(노선 · 자식 넷 · 단계 · 프로젝트 · 지표면 모델) ·
storage = 시험 안 임시 폴더. 지킴:
① 떠 둔 뒤 꺼내면 값 같음 — 노선 · 자식 · 단계 · 상태 · 벌 몫 파일 · 초기값 · 코리도
② 꺼낼 때 새 노선 id · 자식 다시 잇기 · 코리도 이름 새 id
③ 공유 캐시(비용면 · 예정노선 · 배수 하강장 · 도엽 좌표 · 강우)는 안 옮기고 안 지움
④ 지문 어긋난(묵은) 벌은 버리고 그 자리에서 지음 · 실패면 통째 되돌림
"""
from __future__ import annotations
import asyncio
import json
import re
import sqlite3
import sys
from pathlib import Path
from uuid import uuid4
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import aiomysql # noqa: E402
from common_util import common_util_design_sets as sets # noqa: E402
PID = str(uuid4())
_SCHEMA = """
CREATE TABLE projects (id TEXT PRIMARY KEY, status TEXT, name TEXT);
CREATE TABLE surface_models (id INTEGER PRIMARY KEY, project_id TEXT, status TEXT);
CREATE TABLE project_workflow_stages (
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, stage_no INT, stage_key TEXT,
state TEXT, progress_percent INT, params TEXT, message TEXT, started_at TEXT, completed_at TEXT);
CREATE TABLE routes (
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, surface_model_id INT, status TEXT,
total_length_m REAL, route_data_path TEXT, created_at TEXT);
CREATE TABLE route_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
route_id INT REFERENCES routes(id) ON DELETE CASCADE, chainage_m REAL, sequence_num INT);
CREATE TABLE route_statistics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
route_id INT REFERENCES routes(id) ON DELETE CASCADE, cut_volume_m3 REAL);
CREATE TABLE longitudinal_sections (
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT,
route_id INT REFERENCES routes(id) ON DELETE CASCADE, data TEXT);
CREATE TABLE cross_sections (
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT,
route_id INT REFERENCES routes(id) ON DELETE CASCADE, chainage_m REAL, data TEXT);
"""
class _Cursor:
def __init__(self, db: sqlite3.Connection, dict_rows: bool) -> None:
self.db, self.dict_rows, self.rows, self.lastrowid, self.rowcount = db, dict_rows, [], 0, 0
async def __aenter__(self):
return self
async def __aexit__(self, *_):
return False
@staticmethod
def _sql(sql: str) -> str:
return re.sub(r"%s", "?", sql)
async def execute(self, sql: str, args=()) -> None:
cur = self.db.execute(self._sql(sql), tuple(args or ()))
self.lastrowid, self.rowcount = cur.lastrowid, cur.rowcount
names = [d[0] for d in cur.description or []]
rows = cur.fetchall()
self.rows = [dict(zip(names, r)) for r in rows] if self.dict_rows else rows
async def executemany(self, sql: str, seq) -> None:
self.db.executemany(self._sql(sql), [tuple(a) for a in seq])
async def fetchall(self):
return self.rows
async def fetchone(self):
return self.rows[0] if self.rows else None
class _Conn:
def __init__(self, db: sqlite3.Connection) -> None:
self.db = db
def cursor(self, kind=None):
return _Cursor(self.db, kind is aiomysql.DictCursor)
async def begin(self):
pass
async def commit(self):
self.db.commit()
async def rollback(self):
self.db.rollback()
class _Pool:
def __init__(self, conn: _Conn) -> None:
self.conn = conn
def acquire(self):
conn = self.conn
class _Ctx:
async def __aenter__(self):
return conn
async def __aexit__(self, *_):
return False
return _Ctx()
def _seed_db() -> sqlite3.Connection:
db = sqlite3.connect(":memory:")
db.execute("PRAGMA foreign_keys = ON")
db.executescript(_SCHEMA)
db.execute("INSERT INTO projects VALUES (?, 'WF3_ANALYZING', 'p')", (PID,))
db.executemany(
"INSERT INTO surface_models VALUES (?, ?, ?)",
[(5, PID, "CONFIRMED"), (6, PID, "COMPLETE")],
)
for no in range(7):
db.execute(
"INSERT INTO project_workflow_stages (project_id, stage_no, stage_key, state,"
" progress_percent, params, message) VALUES (?, ?, ?, ?, ?, ?, ?)",
(PID, no, f"S{no}", "IN_PROGRESS", 50, json.dumps({"no": no, "k": "las"}), f"m{no}"),
)
for rid in (10, 11):
db.execute(
"INSERT INTO routes (id, project_id, surface_model_id, status, total_length_m,"
" route_data_path, created_at) VALUES (?, ?, 5, 'CONFIRMED', ?, 'r.geojson', 't')",
(rid, PID, rid * 100.0),
)
for seq in range(3):
db.execute(
"INSERT INTO route_points (route_id, chainage_m, sequence_num) VALUES (?, ?, ?)",
(rid, seq * 20.0, seq),
)
db.execute(
"INSERT INTO cross_sections (project_id, route_id, chainage_m, data)"
" VALUES (?, ?, ?, ?)",
(PID, rid, seq * 20.0, json.dumps({"cut": seq})),
)
db.execute("INSERT INTO route_statistics (route_id, cut_volume_m3) VALUES (?, 7)", (rid,))
db.execute(
"INSERT INTO longitudinal_sections (project_id, route_id, data) VALUES (?, ?, '{}')",
(PID, rid),
)
db.commit()
return db
_SET_FILES = {
"B05_Profile/route/planned_route.csv": "x,y\n1,2\n",
"B05_Profile/route/structures.json": '{"items": [1]}',
"B05_Profile/route/route_main.geojson": "{}",
"B05_Profile/corridor/corridor_0011.json": '{"route": 11}',
"B06_Section/longitudinal/longitudinal.json": '{"z": [1, 2]}',
"B06_Section/sampling.json": '{"surface_model_id": 5}',
"B04_PreProcess/drainage/01_primary_region.geojson": "{}",
"B04_PreProcess/drainage/edits/pipe_points.json": "[1]",
"B04_PreProcess/drainage/manifest.json": "{}",
"initial_snapshot/db.json": '{"routes": []}',
}
_SHARED_FILES = {
"B05_Profile/route/cost_surface_x_implicit.npz": "c",
"B05_Profile/route/expected_route.csv": "e",
"B05_Profile/route/initial_choice.json": "{}",
"B04_PreProcess/drainage/descent_ab_cd.npz": "d",
"B04_PreProcess/drainage/00_sheet_metric.pkl": "s",
"B04_PreProcess/drainage/rainfall_table.json": "r",
"B04_PreProcess/models/dtm_x.npz": "m",
}
def _seed_files(root: Path) -> None:
for relative, text in {**_SET_FILES, **_SHARED_FILES}.items():
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def _tree(root: Path) -> dict[str, str]:
return {
p.relative_to(root).as_posix(): p.read_text(encoding="utf-8")
for p in sorted(root.rglob("*"))
if p.is_file() and SETS_DIR not in p.relative_to(root).parts
}
SETS_DIR = sets.SETS_DIRNAME
FP = {
"terrain": "las",
"source_filter": "classification",
"method": "implicit",
"smooth": False,
"model_id": 5,
"model_digest": "aa",
}
def _rows(db: sqlite3.Connection, sql: str, args=()) -> list[tuple]:
return db.execute(sql, args).fetchall()
def _no_ids(db: sqlite3.Connection) -> dict[str, list]:
"""id 를 뺀 값(자식은 노선 차례로) — 왕복 비교용."""
order = {rid: i for i, (rid,) in enumerate(_rows(db, "SELECT id FROM routes ORDER BY id"))}
out = {
"routes": _rows(
db,
"SELECT project_id, surface_model_id, status, total_length_m, route_data_path,"
" created_at FROM routes ORDER BY id",
),
"stages": _rows(
db,
"SELECT stage_no, state, progress_percent, params, message FROM"
" project_workflow_stages ORDER BY stage_no",
),
"status": _rows(db, "SELECT status FROM projects"),
}
for table, cols in (
("route_points", "chainage_m, sequence_num"),
("route_statistics", "cut_volume_m3"),
("longitudinal_sections", "project_id, data"),
("cross_sections", "project_id, chainage_m, data"),
):
out[table] = sorted(
(order[r[0]], *r[1:])
for r in _rows(db, f"SELECT route_id, {cols} FROM {table} ORDER BY id")
)
return out
def test_벌_몫과_공유를_가름(tmp_path: Path) -> None:
_seed_files(tmp_path)
entries = sets.set_entries(tmp_path)
assert "B06_Section" in entries and "initial_snapshot" in entries
assert "B05_Profile/route/structures.json" in entries
assert "B04_PreProcess/drainage/edits" in entries
joined = " ".join(entries)
for shared in ("cost_surface_", "expected_route", "initial_choice", "descent_", "00_sheet"):
assert shared not in joined
assert "rainfall_" not in joined and "models" not in joined
def test_떠_두고_꺼내면_값_같음_새_id(tmp_path: Path) -> None:
db = _seed_db()
conn = _Conn(db)
_seed_files(tmp_path)
before_db, before_files = _no_ids(db), _tree(tmp_path)
meta = asyncio.run(sets.park_active_set(conn, tmp_path, PID, FP))
assert meta["routes"] == [10, 11] and meta["built"]
assert _rows(db, "SELECT COUNT(*) FROM routes") == [(0,)]
assert _rows(db, "SELECT COUNT(*) FROM cross_sections") == [(0,)]
states = _rows(db, "SELECT stage_no, state, params FROM project_workflow_stages ORDER BY 1")
assert states[:2] == [(0, "IN_PROGRESS", before_db["stages"][0][3])] + [
(1, "IN_PROGRESS", before_db["stages"][1][3])
]
assert all(s[1] == "NOT_STARTED" and s[2] is None for s in states[2:])
left = _tree(tmp_path)
assert set(left) == set(_SHARED_FILES) # 공유만 남음 · 그대로
assert all(left[k] == v for k, v in _SHARED_FILES.items())
bundle = tmp_path / SETS_DIR / "las"
assert (bundle / "db.json").is_file() and (bundle / "initial_snapshot" / "db.json").is_file()
assert (bundle / "files" / "B06_Section" / "sampling.json").is_file()
assert sets.bundle_ready(tmp_path, "las", FP)
route_map = asyncio.run(sets.load_set(conn, tmp_path, PID, "las"))
assert set(route_map) == {10, 11}
assert not set(route_map.values()) & {10, 11} # 새 id
assert _no_ids(db) == before_db # 노선 · 자식 · 단계 · 상태 같음
after = _tree(tmp_path)
moved = before_files.pop("B05_Profile/corridor/corridor_0011.json")
new_corridor = f"B05_Profile/corridor/corridor_{route_map[11]:04d}.json"
assert after.pop(new_corridor) == moved # 코리도 = 새 id 이름
assert after == before_files
assert not bundle.exists() # 꺼낸 묶음은 지움
def test_지문_어긋나면_묵은_벌(tmp_path: Path) -> None:
db = _seed_db()
_seed_files(tmp_path)
asyncio.run(sets.park_active_set(_Conn(db), tmp_path, PID, FP))
assert sets.bundle_ready(tmp_path, "las", FP)
assert sets.bundle_ready(tmp_path, "las", {**FP, "model_id": 99}) # 복제 · 갈음 id 는 안 봄
assert not sets.bundle_ready(tmp_path, "las", {**FP, "model_digest": "bb"})
assert not sets.bundle_ready(tmp_path, "las", {**FP, "source_filter": "csf"})
assert not sets.bundle_ready(tmp_path, "las", None)
assert not sets.bundle_ready(tmp_path, "sheet", {**FP, "terrain": "sheet"})
sets.note_unbuilt(tmp_path, "sheet", "수치지형도 모델 없음")
assert sets.read_meta(tmp_path, "sheet") == {
"terrain": "sheet",
"built": False,
"reason": "수치지형도 모델 없음",
}
assert not sets.bundle_ready(tmp_path, "sheet", {**FP, "terrain": "sheet"})
def _swap_world(monkeypatch, tmp_path: Path, *, sheet_fp: dict, chain):
"""las 활성 · sheet 묶음(지문 = sheet_fp) — 칩으로 sheet."""
import B03_FileInput.B03_FileInput_Service_Chain as chain_mod
import config.config_db as config_db
db = _seed_db()
conn = _Conn(db)
_seed_files(tmp_path)
sheet = {**FP, "terrain": "sheet", "source_filter": "sheet_laplace", "model_id": 6}
# sheet 벌을 미리 떠 둠: 노선 하나(길이 777)
asyncio.run(sets.park_active_set(conn, tmp_path, PID, FP))
db.execute(
"INSERT INTO routes (project_id, surface_model_id, status, total_length_m) VALUES"
" (?, 6, 'CONFIRMED', 777)",
(PID,),
)
db.commit()
(tmp_path / "B06_Section").mkdir(parents=True, exist_ok=True)
(tmp_path / "B06_Section" / "sheet.json").write_text("sheet", encoding="utf-8")
asyncio.run(sets.park_active_set(conn, tmp_path, PID, sheet_fp))
asyncio.run(sets.load_set(conn, tmp_path, PID, "las"))
state = {"design": "las"}
async def params(_c, _pid):
return {
"source_filter": "sheet_laplace" if state["design"] == "sheet" else "classification"
}
async def fingerprint(_c, _pid, _root, terrain):
return sheet if terrain == "sheet" else FP
async def switch(_c, _pid, terrain):
state["design"] = terrain
db.execute(
"UPDATE surface_models SET status = CASE id WHEN 6 THEN 'CONFIRMED' ELSE 'COMPLETE' END"
)
return 6
monkeypatch.setattr(config_db, "get_db_pool", lambda: _Pool(conn))
monkeypatch.setattr(sets, "get_surface_confirmation_params", params)
monkeypatch.setattr(sets, "set_fingerprint", fingerprint)
monkeypatch.setattr(sets, "switch_terrain", switch)
monkeypatch.setattr(chain_mod, "run_auto_design_chain", chain)
return db, state
def test_맞바꿈_묶음_있으면_꺼냄(monkeypatch, tmp_path: Path) -> None:
called = []
async def chain(*a, **k):
called.append(a)
sheet_fp = {**FP, "terrain": "sheet", "source_filter": "sheet_laplace", "model_id": 6}
db, _ = _swap_world(monkeypatch, tmp_path, sheet_fp=sheet_fp, chain=chain)
result = asyncio.run(sets.swap_design_set(PID, tmp_path, "sheet")) # type: ignore[arg-type]
assert result["changed"] and not result["built"] and not called
assert _rows(db, "SELECT total_length_m FROM routes") == [(777.0,)]
assert (tmp_path / "B06_Section" / "sheet.json").read_text(encoding="utf-8") == "sheet"
assert sets.bundle_ready(tmp_path, "las", FP) # 떠난 벌 살림
assert not (tmp_path / SETS_DIR / "sheet").exists()
def test_맞바꿈_묵은_벌은_버리고_지음(monkeypatch, tmp_path: Path) -> None:
called = []
holder: dict = {}
async def chain(pid, model_id, inner=False):
called.append((model_id, inner))
holder["db"].execute(
"INSERT INTO routes (project_id, surface_model_id, status, total_length_m) VALUES"
" (?, 6, 'CONFIRMED', 555)",
(PID,),
)
return {"route_id": 1}
stale = {**FP, "terrain": "sheet", "source_filter": "sheet_laplace", "model_digest": "old"}
db, _ = _swap_world(monkeypatch, tmp_path, sheet_fp=stale, chain=chain)
holder["db"] = db
result = asyncio.run(sets.swap_design_set(PID, tmp_path, "sheet")) # type: ignore[arg-type]
assert result["built"] and called == [(6, True)]
assert _rows(db, "SELECT total_length_m FROM routes") == [(555.0,)] # 묵은 777 버림
assert not (tmp_path / "B06_Section" / "sheet.json").exists()
assert sets.bundle_ready(tmp_path, "las", FP)
def test_맞바꿈_실패면_통째_되돌림(monkeypatch, tmp_path: Path) -> None:
async def chain(*_a, **_k):
return None # 지음 실패
stale = {**FP, "terrain": "sheet", "source_filter": "sheet_laplace", "model_digest": "old"}
db, _ = _swap_world(monkeypatch, tmp_path, sheet_fp=stale, chain=chain)
before_db, before_files = _no_ids(db), _tree(tmp_path)
route_ids = _rows(db, "SELECT id FROM routes ORDER BY id")
sheet_meta = sets.read_meta(tmp_path, "sheet")
models = _rows(db, "SELECT id, status FROM surface_models ORDER BY id")
with pytest.raises(RuntimeError):
asyncio.run(sets.swap_design_set(PID, tmp_path, "sheet")) # type: ignore[arg-type]
assert _no_ids(db) == before_db
assert _rows(db, "SELECT id FROM routes ORDER BY id") == route_ids
assert _rows(db, "SELECT id, status FROM surface_models ORDER BY id") == models
assert _tree(tmp_path) == before_files
assert sets.read_meta(tmp_path, "sheet") == sheet_meta # 묶음도 그대로
assert not (tmp_path / SETS_DIR / "las").exists()
# ── 765-3 전처리 체인 = 두 벌 ─────────────────────────────────────────────
def _chain_world(monkeypatch, tmp_path: Path, *, other, built_other=True):
import B03_FileInput.B03_FileInput_Repository as repo
import B03_FileInput.B03_FileInput_Service_Chain as chain
import B05_Profile.B05_Profile_Router_RouteInitial as router
import common_util.common_util_storage as storage
import config.config_db as config_db
log: list = []
expected = tmp_path / "B05_Profile" / "route" / "expected_route.csv"
async def auto(pid, model_id=None, inner=False):
log.append(("auto", model_id, inner))
expected.parent.mkdir(parents=True, exist_ok=True)
expected.write_text(str(model_id), encoding="utf-8") # 체인이 처음 쓰는 예상노선
return {"route_id": 1} if (built_other or len(log) > 2) else None
async def switch(_c, _pid, terrain):
log.append(("switch", terrain))
return 6 if terrain == "sheet" else 5
async def park(_c, _root, _pid, fingerprint):
log.append(("park", fingerprint["terrain"]))
async def clear(_c, _root, _pid):
log.append("clear")
async def fingerprint(_c, _pid, _root, terrain):
return {**FP, "terrain": terrain}
async def params(_c, _pid):
return {"source_filter": "classification"}
monkeypatch.setattr(config_db, "get_db_pool", lambda: _Pool(_Conn(sqlite3.connect(":memory:"))))
monkeypatch.setattr(repo, "get_project_storage_relative_path", lambda *_a: _done("p"))
monkeypatch.setattr(storage, "resolve_stored_project_path", lambda _p: str(tmp_path))
import common_util.common_util_surface_confirmation as confirmation
monkeypatch.setattr(confirmation, "get_surface_confirmation_params", params)
monkeypatch.setattr(sets, "other_terrain", lambda _p, _r: other)
monkeypatch.setattr(sets, "switch_terrain", switch)
monkeypatch.setattr(sets, "park_active_set", park)
monkeypatch.setattr(sets, "clear_active_set", clear)
monkeypatch.setattr(sets, "set_fingerprint", fingerprint)
monkeypatch.setattr(chain, "run_auto_design_chain", auto)
monkeypatch.setattr(router, "schedule_precompute", lambda pid: log.append("precompute"))
return chain, log, expected
async def _done(value):
return value
def test_전처리_체인_다른_지형_먼저_떠_두고_설계_지형(monkeypatch, tmp_path: Path) -> None:
chain, log, expected = _chain_world(monkeypatch, tmp_path, other="sheet")
(tmp_path / SETS_DIR / "las").mkdir(parents=True) # 옛 묶음 = 버림
summary = asyncio.run(chain.run_preprocess_chain(PID, 5)) # type: ignore[arg-type]
assert summary == {"route_id": 1}
assert log == [
("switch", "sheet"),
("auto", 6, True),
("park", "sheet"),
("switch", "las"),
("auto", 5, True),
"precompute",
]
assert expected.read_text(encoding="utf-8") == "5" # 예상노선 = 설계 지형 벌이 씀
assert not (tmp_path / SETS_DIR / "las").exists()
assert not (tmp_path / "initial_design.lock").exists()
def test_전처리_체인_다른_지형_없으면_한_벌(monkeypatch, tmp_path: Path) -> None:
chain, log, _ = _chain_world(monkeypatch, tmp_path, other=None)
asyncio.run(chain.run_preprocess_chain(PID, 5)) # type: ignore[arg-type]
assert log == [("auto", 5, True), "precompute"]
meta = sets.read_meta(tmp_path, "sheet")
assert meta["built"] is False and "sheet" in meta["reason"]
def test_전처리_체인_다른_벌_실패해도_설계_지형(monkeypatch, tmp_path: Path) -> None:
chain, log, _ = _chain_world(monkeypatch, tmp_path, other="sheet", built_other=False)
summary = asyncio.run(chain.run_preprocess_chain(PID, 5)) # type: ignore[arg-type]
assert summary == {"route_id": 1}
assert log == [
("switch", "sheet"),
("auto", 6, True),
"clear",
("switch", "las"),
("auto", 5, True),
"precompute",
]
assert sets.read_meta(tmp_path, "sheet")["built"] is False
def test_비우기에_묶음_포함(tmp_path: Path) -> None:
from common_util.common_util_project_reset import purge_output_folders
(tmp_path / SETS_DIR / "sheet" / "files").mkdir(parents=True)
(tmp_path / "B04_PreProcess" / "models").mkdir(parents=True)
removed = purge_output_folders(tmp_path, keep_preprocess=True)
assert SETS_DIR in removed and not (tmp_path / SETS_DIR).exists()
assert (tmp_path / "B04_PreProcess" / "models").is_dir()
# ── 765-6 초기값 사본 = 캐시 뺌 · 복제가 묶음 안 지표면 id 고침 ────────────
def test_초기값_사본은_배수_캐시를_빼고_복원은_캐시를_안_지움(tmp_path: Path) -> None:
from common_util import common_util_initial_snapshot as snap
_seed_files(tmp_path)
drainage = tmp_path / "B04_PreProcess" / "drainage"
(drainage / "rainfall_stations").mkdir()
(drainage / "rainfall_stations" / "a.json").write_text("st", encoding="utf-8")
target = tmp_path / "initial_snapshot" / "B04_PreProcess__drainage"
snap._save_tree(drainage, target, snap.DRAINAGE_SHARED_PREFIXES)
saved = sorted(p.name for p in target.iterdir())
assert saved == ["01_primary_region.geojson", "edits", "manifest.json"] # 캐시 없음
# 작업본이 바뀜 · 캐시는 새로 지어짐 · 옛 사본(캐시 듦) 꼴도 섞음
(drainage / "01_primary_region.geojson").write_text("edited", encoding="utf-8")
(drainage / "04_detailed_basins.geojson").write_text("new", encoding="utf-8")
(drainage / "descent_ab_cd.npz").write_text("d2", encoding="utf-8")
(target / "00_sheet_metric.pkl").write_text("old-cache", encoding="utf-8")
snap._restore_tree(target, drainage, snap.DRAINAGE_SHARED_PREFIXES)
live = {p.name: p for p in drainage.iterdir()}
assert live["01_primary_region.geojson"].read_text(encoding="utf-8") == "{}"
assert "04_detailed_basins.geojson" not in live # 초기값에 없던 노선 몫 = 지움
assert live["descent_ab_cd.npz"].read_text(encoding="utf-8") == "d2" # 캐시 그대로
assert live["00_sheet_metric.pkl"].read_text(encoding="utf-8") == "s" # 옛 사본 캐시 안 덮음
assert (live["rainfall_stations"] / "a.json").is_file()
assert (live["edits"] / "pipe_points.json").is_file()
def test_복제가_묶음_안_지표면_id_고침(tmp_path: Path) -> None:
from common_util.common_util_project_copy import _fix_design_sets
db = _seed_db()
_seed_files(tmp_path)
asyncio.run(sets.park_active_set(_Conn(db), tmp_path, PID, FP))
bundle = tmp_path / SETS_DIR / "las"
corridor = bundle / "files" / "B05_Profile" / "corridor" / "corridor_0011.json"
report = _fix_design_sets(tmp_path, {5: 905})
assert sorted(report["las"]) == ["db.json", "meta.json", "sampling"]
dump = json.loads((bundle / "db.json").read_text(encoding="utf-8"))
assert {r["surface_model_id"] for r in dump["routes"]} == {905}
assert [r["id"] for r in dump["routes"]] == [10, 11] # 노선 id 는 꺼낼 때 새로
assert sets.read_meta(tmp_path, "las")["model_id"] == 905
sampling = bundle / "files" / "B06_Section" / "sampling.json"
assert json.loads(sampling.read_text(encoding="utf-8")) == {"surface_model_id": 905}
assert corridor.is_file() # 코리도는 안 건드림
assert sets.bundle_ready(tmp_path, "las", FP) # 지문은 id 를 안 봄
def test_초기값_사본은_비용면_예상노선도_빼고_복원이_안_건드림(tmp_path: Path) -> None:
from common_util import common_util_initial_snapshot as snap
_seed_files(tmp_path)
asyncio.run(_save(snap, tmp_path))
stored = tmp_path / "initial_snapshot" / "B05_Profile__route"
assert not any(p.name.startswith(("cost_surface_", "expected_route")) for p in stored.iterdir())
route = tmp_path / "B05_Profile" / "route"
(route / "cost_surface_x_implicit.npz").write_text("c2", encoding="utf-8")
(route / "plus_stations.json").write_text("[]", encoding="utf-8")
(route / "expected_route.csv").write_text("other-terrain", encoding="utf-8")
snap.restore_snapshot_files(tmp_path)
assert (route / "cost_surface_x_implicit.npz").read_text(encoding="utf-8") == "c2"
# 예상노선 = 공유 — 다른 벌 [초기화]가 덮지 않음
assert (route / "expected_route.csv").read_text(encoding="utf-8") == "other-terrain"
assert not (route / "plus_stations.json").exists() # 초기값에 없던 설계 파일 = 지움
assert (route / "structures.json").read_text(encoding="utf-8") == '{"items": [1]}'
async def _save(snap, root: Path) -> None:
(root / "initial_snapshot").rename(root / "_old_snapshot") # 시드 사본 비켜 둠
db = _seed_db()
await snap.save_initial_snapshot(_Conn(db), root, 11)