Files
Aislo/resources/tester/test_618_precompute_split.py
T
eomsangdonandClaude Opus 5.5 88ec2da193 feat(도구): 전국 미리 계산 두 PC 나눔 — 몫 · PC 별 기록 · CPU 상한 · 저전력 코어 지정 (PLAN 618-1 · 요구 507~509)
- --part A-B/N = 도엽 번호 crc32 % N 몫 · 지형 단계가 0차곡 이웃(목록 밖 · 띠 넘음 포함)까지 몫대로 셈
- 0차곡 = 남의 몫 이웃 지형이 창고에 없으면 미룸 · --defer-wait 초 뒤 다시
- 진행 기록 = _precompute_progress_<컴퓨터 이름>.json · 옛 한 벌은 성공만 읽어 이어 받음 · --status 모든 PC 합침 · 창고 실물 수
- --cpu-cap = 전체 CPU 평균이 넘으면 새 도엽 안 줌 · --e-cores --e-workers = 일꾼 앞선 몇 명만 그 논리 CPU 지정
- 죽은 잠금 치움 = 자기 몫 도엽만 · --root 시험용 창고 자리
- test_612: 지형 단계 이웃 일감 덧 · 기록 파일 이름 바뀜에 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:22:24 +09:00

196 lines
7.5 KiB
Python

"""PLAN 618-1 — 전국 미리 계산 두 PC 나눔(`tools/precompute_terrain.py`) 지킴 시험.
나눔 겹침 0 · 합치면 전체 · 이웃 일감 한 PC 만 · PC 별 기록 이어 받기 · CPU 상한(가짜 값) ·
0차곡 남의 몫 이웃 없으면 미룸 · 지정 코어 인자 풀기 ·
셈 함수는 가짜로 바꿔 서버 · 전국 파일 없이 돎.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B04_PreProcess.B04_PreProcess_Engine_MapSheet import neighbors_3x3 # noqa: E402
from tools import precompute_terrain as tool # noqa: E402
SHEETS = [f"36901{i:03d}" for i in range(1, 101)] + [f"35810{i:03d}" for i in range(1, 101)]
def test_parse_part_and_cpus() -> None:
assert tool.parse_part("") is None
assert tool.parse_part("0-69/100") == (0, 69, 100)
assert tool.parse_part("70-99/100") == (70, 99, 100)
for bad in ("50-10/100", "0-100/100"):
with pytest.raises(ValueError):
tool.parse_part(bad)
assert tool.parse_cpus("12-19") == list(range(12, 20))
assert tool.parse_cpus("1, 3-4") == [1, 3, 4]
assert tool.parse_cpus("") == []
def test_split_no_overlap_and_union_is_all() -> None:
for spans in (("0-69/100", "70-99/100"), ("0-49/100", "50-99/100")):
a, b = (tool.parse_part(s) for s in spans)
mine = {no for no in SHEETS if tool.in_part(no, a)}
other = {no for no in SHEETS if tool.in_part(no, b)}
assert not mine & other and mine | other == set(SHEETS)
ja, jb = set(tool.dem_jobs(SHEETS, a)), set(tool.dem_jobs(SHEETS, b))
assert not ja & jb and ja | jb == set(tool.dem_jobs(SHEETS, None))
big = {no for no in SHEETS if tool.in_part(no, (0, 69, 100))}
assert 0.6 < len(big) / len(SHEETS) < 0.8
assert tool.in_part("36901001@5186", (0, 99, 100))
def test_dem_jobs_cover_neighbors_once() -> None:
jobs = tool.dem_jobs(["36901055"], None)
assert jobs[0] == "36901055" and set(jobs) == set(neighbors_3x3("36901055"))
# 띠 경계(128° · 5186 | 5187) 넘는 이웃 = 가운데 좌표계로 옮겨 담는 일감
west = [no for no in tool.dem_jobs(["36809001"], None) if "@" in no]
assert west and all(j.endswith(f"@{tool.epsg_of('36809001')}") for j in west)
@pytest.fixture()
def fake_store(tmp_path, monkeypatch):
monkeypatch.setattr(tool.store, "TERRAIN_ROOT", tmp_path)
monkeypatch.setattr(tool.store, "_gpkg_stamp", lambda: "stamp-1")
monkeypatch.setattr(tool, "PC_NAME", "LAPTOP")
monkeypatch.setattr(tool, "CPU_SAMPLE_S", 0.0)
log: list = []
def fake_run(step: str, job: str, part=None):
log.append(("run", step, job))
return step, job, {"ok": True, "s": 0.01, "fresh": True, "empty": False}
monkeypatch.setattr(tool, "run_step", fake_run)
return tmp_path, log
def test_progress_per_pc_and_legacy_resume(fake_store, monkeypatch) -> None:
root, log = fake_store
legacy = {"gpkg": "stamp-1", "cell_m": 2.0, "steps": {"dem": {"36901001": {"ok": True}}}}
(root / tool.PROGRESS_NAME).write_text(json.dumps(legacy), encoding="utf-8")
tool.run(["36901001", "36901002"], ["e07"], 1, tool.load_progress(), say=lambda _: None)
assert (root / "_precompute_progress_LAPTOP.json").is_file()
assert json.loads((root / tool.PROGRESS_NAME).read_text(encoding="utf-8")) == legacy # 안 씀
log.clear()
tool.run(["36901001", "36901002"], ["dem"], 1, tool.load_progress(), say=lambda _: None)
assert ("run", "dem", "36901001") not in log # 옛 기록 성공 = 이어 받음
monkeypatch.setattr(tool, "PC_NAME", "DESKTOP")
desk = tool.load_progress()
assert desk["steps"]["e07"] == {} # 다른 PC 기록은 run 에선 안 섞음
desk["steps"]["e07"]["36901003"] = {"ok": True, "s": 1.0, "fresh": True}
tool.save_progress(desk)
merged = tool.load_progress(every_pc=True)["steps"]["e07"]
assert {"36901001", "36901002", "36901003"} <= set(merged)
def test_cpu_cap_holds_new_work(fake_store) -> None:
_, log = fake_store
values = iter([95, 90, 60, 80, 40])
def cpu() -> float:
value = next(values)
log.append(("cpu", value))
return value
tool.run(
["36901001", "36901002"],
["e07"],
1,
tool.load_progress(),
cpu_cap=70,
cpu=cpu,
say=lambda _: None,
)
assert [x[1] if x[0] == "cpu" else x[2] for x in log] == [
95,
90,
60,
"36901001",
80,
40,
"36901002",
]
def test_zero_defers_when_foreign_neighbor_missing(tmp_path, monkeypatch) -> None:
monkeypatch.setattr(tool.store, "TERRAIN_ROOT", tmp_path)
center = "36901055"
part = (0, 49, 100)
foreign = [no for no in neighbors_3x3(center) if not tool.in_part(no, part)]
assert foreign # 고른 가운데는 남의 몫 이웃이 있음
stored: set[str] = set()
monkeypatch.setattr(
tool.store, "_stored_ok", lambda no, e, c: {"k": 1} if no in stored else None
)
ensured: list[str] = []
monkeypatch.setattr(tool.store, "ensure_sheet_terrain", lambda no, e, c: ensured.append(no))
_, _, info = tool.run_step("zero", center, part)
assert info["ok"] is False and info["wait"] and not ensured # 셈 안 함
stored.update(foreign) # 다른 PC 가 셈 · 동기 옴
from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zero
monkeypatch.setattr(zero, "sheet_accumulation", lambda no, e, c: tmp_path / "acc.npz")
_, _, info = tool.run_step("zero", center, part)
assert info["ok"] is True and len(ensured) == 9
def test_deferred_rounds_retry_until_done(fake_store, monkeypatch) -> None:
root, log = fake_store
tries: dict[str, int] = {}
def fake_run(step: str, job: str, part=None):
tries[job] = tries.get(job, 0) + 1
if job == "36901002" and tries[job] < 3:
return step, job, {"ok": False, "wait": True, "error": "이웃 지형 없음"}
return step, job, {"ok": True, "s": 0.01, "fresh": True, "empty": False}
monkeypatch.setattr(tool, "run_step", fake_run)
monkeypatch.setattr(tool.time, "sleep", lambda s: None)
progress = tool.load_progress()
tool.run(["36901001", "36901002"], ["zero"], 1, progress, say=lambda _: None, defer_wait=1)
assert tries == {"36901001": 1, "36901002": 3}
out = tool.summary(["36901001", "36901002"], progress)["zero"]
assert out["ok"] == 2 and out["failed"] == {} and out["waiting"] == 0
def test_clean_dead_only_own_part(tmp_path) -> None:
part = (0, 49, 100)
mine = next(no for no in SHEETS if tool.in_part(no, part))
other = next(no for no in SHEETS if not tool.in_part(no, part))
for no in (mine, other):
(tmp_path / no).mkdir()
(tmp_path / no / ".dem_5187_2m.lock").write_text("999999999 0", encoding="utf-8")
assert tool.clean_dead(tmp_path, part) == 1
assert (tmp_path / other / ".dem_5187_2m.lock").exists()
def test_worker_init_pins_first_e_workers(monkeypatch, tmp_path) -> None:
import multiprocessing
import psutil
pinned: list[list[int]] = []
class FakeProc:
def nice(self, _):
pass
def cpu_affinity(self, cpus):
pinned.append(cpus)
monkeypatch.setattr(psutil, "Process", FakeProc)
monkeypatch.setattr(tool.store, "TERRAIN_ROOT", tool.store.TERRAIN_ROOT) # 끝나면 되돌림
counter = multiprocessing.Value("i", 0)
for _ in range(5):
tool.init_worker(str(tmp_path), counter, [12, 13], 3)
assert pinned == [[12, 13]] * 3 and tool.store.TERRAIN_ROOT == tmp_path