- common_util_derived_store — 열쇠 · 읽기 · 원자 쓰기 · 종류마다 최근 2벌 · 결과 옆 열쇠 · 파일 지문 - B04 전처리 · 도엽 3D 면이 도우미의 file_stamp · key_matches 를 씀 - 유토 배분 — 메모리 다음 _derived/haul/<열쇠>.json(같은 _plan_key) · 재시작 뒤 첫 열기도 셈 없음 · 저장소 못 찾아도 셈 - 시험 test_343_4_derived 11건 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
171 lines
7.3 KiB
Python
171 lines
7.3 KiB
Python
"""343 덧 — 파생 저장본 도우미 · 유토 배분 저장본 · B07 읽기 안 지움 · 묶음 낡음 검사 기억."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from B06_Section import B06_Section_Router_HaulPlan as haul
|
|
from common_util import common_util_derived_store as store
|
|
from common_util import common_util_node_bundle as nb
|
|
|
|
# ── 도우미 ──────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_열쇠_같은_입력_같은_값() -> None:
|
|
assert store.derived_key({"a": 1, "b": 2}, 3) == store.derived_key({"b": 2, "a": 1}, 3)
|
|
assert store.derived_key({"a": 1}) != store.derived_key({"a": 2})
|
|
|
|
|
|
def test_읽기_쓰기_null_도_맞음(tmp_path: Path) -> None:
|
|
assert store.read_derived(tmp_path, "haul", "k") is None
|
|
store.write_derived(tmp_path, "haul", "k", None)
|
|
assert store.read_derived(tmp_path, "haul", "k") == {"key": "k", "value": None}
|
|
assert (tmp_path / "_derived" / "haul" / "k.json").is_file()
|
|
|
|
|
|
def test_종류마다_최근_두_벌_꺼내면_최근(tmp_path: Path) -> None:
|
|
store.write_derived(tmp_path, "haul", "a", 1)
|
|
time.sleep(0.02)
|
|
store.write_derived(tmp_path, "haul", "b", 2)
|
|
store.write_derived(tmp_path, "other", "x", 0)
|
|
time.sleep(0.02)
|
|
assert store.read_derived(tmp_path, "haul", "a")["value"] == 1
|
|
time.sleep(0.02)
|
|
store.write_derived(tmp_path, "haul", "c", 3)
|
|
names = sorted(path.stem for path in (tmp_path / "_derived" / "haul").glob("*.json"))
|
|
assert names == ["a", "c"]
|
|
assert (tmp_path / "_derived" / "other" / "x.json").is_file()
|
|
|
|
|
|
def test_결과_옆_열쇠와_파일_지문(tmp_path: Path) -> None:
|
|
path = tmp_path / "m.key.json"
|
|
assert not store.key_matches(path, {"a": 1})
|
|
path.write_text(json.dumps({"a": 1}), encoding="utf-8")
|
|
assert store.key_matches(path, {"a": 1}) and not store.key_matches(path, {"a": 2})
|
|
assert store.file_stamp(tmp_path / "없음") is None and store.file_stamp(path)
|
|
|
|
|
|
# ── 유토 배분 디스크 저장본 ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def _wire(monkeypatch: pytest.MonkeyPatch, root: Path, outputs: list) -> list:
|
|
calls: list = []
|
|
|
|
async def _none(_project_id): # type: ignore[no-untyped-def]
|
|
return {}
|
|
|
|
async def _stored(_fn, _project_id): # type: ignore[no-untyped-def]
|
|
return "p"
|
|
|
|
def _run(_bundle, _script, payload): # type: ignore[no-untyped-def]
|
|
calls.append(payload)
|
|
return outputs.pop(0)
|
|
|
|
monkeypatch.setattr(haul, "haul_inputs_for", _none)
|
|
monkeypatch.setattr(haul, "conversion_factors_for", _none)
|
|
monkeypatch.setattr(haul, "haul_limits_for", _none)
|
|
monkeypatch.setattr(haul, "_mass_haul_context", lambda *_: {"limits": []})
|
|
monkeypatch.setattr(haul, "run_bundle_json", _run)
|
|
monkeypatch.setattr(haul, "run_with_connection", _stored)
|
|
monkeypatch.setattr(haul, "resolve_stored_project_path", lambda _p: str(root))
|
|
haul._KEPT.clear()
|
|
return calls
|
|
|
|
|
|
def _post(result: dict) -> dict:
|
|
response = asyncio.run(haul.compute_haul_plan(uuid4(), 7, {"result": result}))
|
|
return json.loads(response.body)
|
|
|
|
|
|
def test_유토_배분_재시작_뒤에도_셈_없음(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
calls = _wire(monkeypatch, tmp_path, [{"haul_plan": {"tag": "a"}}, {"haul_plan": {"tag": "b"}}])
|
|
a = {"points": [{"cumulative_volume_m3": 1.0}]}
|
|
assert _post(a)["haul_plan"] == {"tag": "a"} and len(calls) == 1
|
|
key = haul._plan_key(a, {"limits": []}) # 화면 · 서버 메모리와 같은 열쇠
|
|
assert (tmp_path / "_derived" / "haul" / f"{key}.json").is_file()
|
|
haul._KEPT.clear() # 재시작
|
|
assert _post(a)["haul_plan"] == {"tag": "a"} and len(calls) == 1
|
|
b = {"points": [{"cumulative_volume_m3": 2.0}]}
|
|
assert _post(b)["haul_plan"] == {"tag": "b"} and len(calls) == 2
|
|
|
|
|
|
def test_유토_배분_저장소_못_찾아도_셈(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
calls = _wire(monkeypatch, tmp_path, [{"haul_plan": {"tag": "a"}}])
|
|
|
|
async def _missing(_fn, _project_id): # type: ignore[no-untyped-def]
|
|
raise LookupError("없음")
|
|
|
|
monkeypatch.setattr(haul, "run_with_connection", _missing)
|
|
assert _post({"points": [{"cumulative_volume_m3": 1.0}]})["haul_plan"] == {"tag": "a"}
|
|
assert len(calls) == 1 and not (tmp_path / "_derived").exists()
|
|
|
|
|
|
# ── B07 읽기는 측점 파일을 안 지움 ─────────────────────────────────────────────────
|
|
|
|
|
|
def test_B07_읽기는_잔재를_걸러만_읽음(tmp_path: Path) -> None:
|
|
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _cross_files
|
|
|
|
cross_dir = tmp_path / "B05_Profile" / "cross_sections"
|
|
cross_dir.mkdir(parents=True)
|
|
for name in ("cross_00000m.json", "cross_00020m.json", "cross_00040m.json"):
|
|
(cross_dir / name).write_text("{}", encoding="utf-8")
|
|
longitudinal_path = tmp_path / "B05_Profile" / "longitudinal" / "longitudinal.json"
|
|
files = _cross_files(longitudinal_path, {"stations": [{"chainage_m": 0}, {"chainage_m": 20}]})
|
|
assert [path.name for path in files] == ["cross_00000m.json", "cross_00020m.json"]
|
|
assert (cross_dir / "cross_00040m.json").is_file()
|
|
|
|
|
|
# ── 묶음 낡음 검사 기억 ─────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture
|
|
def bundle(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
monkeypatch.setattr(nb, "ROOT", tmp_path)
|
|
monkeypatch.setattr(nb, "_FRESH", {})
|
|
path = tmp_path / "dist" / "bundle.mjs"
|
|
path.parent.mkdir()
|
|
path.write_text("", encoding="utf-8")
|
|
os.utime(path, (1_000_000_000, 1_000_000_000))
|
|
return path
|
|
|
|
|
|
def _counting(monkeypatch: pytest.MonkeyPatch) -> list:
|
|
scans: list = []
|
|
real = nb._sources_newer
|
|
|
|
def counting(built_at: float) -> bool:
|
|
scans.append(built_at)
|
|
return real(built_at)
|
|
|
|
monkeypatch.setattr(nb, "_sources_newer", counting)
|
|
return scans
|
|
|
|
|
|
def test_낡지_않음은_잠시_기억(bundle: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
scans = _counting(monkeypatch)
|
|
assert not nb.bundle_stale(bundle) and not nb.bundle_stale(bundle)
|
|
assert len(scans) == 1
|
|
# 묶음을 새로 지으면(mtime 바뀜) 다시 잼
|
|
os.utime(bundle, (1_000_000_100, 1_000_000_100))
|
|
assert not nb.bundle_stale(bundle) and len(scans) == 2
|
|
# 기억 시간이 지나면 다시 잼
|
|
monkeypatch.setattr(nb, "RECHECK_S", 0.0)
|
|
assert not nb.bundle_stale(bundle) and len(scans) == 3
|
|
|
|
|
|
def test_낡음은_기억하지_않음(bundle: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
source = nb.ROOT / "B05_Profile" / "x.ts"
|
|
source.parent.mkdir()
|
|
source.write_text("", encoding="utf-8")
|
|
scans = _counting(monkeypatch)
|
|
assert nb.bundle_stale(bundle) and nb.bundle_stale(bundle)
|
|
assert len(scans) == 2 and str(bundle) not in nb._FRESH
|