# -*- coding: utf-8 -*- """508-3b · c B07 도면 지문 · 평면도 파생 저장본(임시 폴더 · DB · 실제 마스터 안 씀). - 지문 = 입력이 바뀔 때만 바뀜(같은 내용으로 다시 쓴 종단 = 그대로) - 횡단 측점 하나 바꾸면 그 장만 · 그 장 측점 지문도 그 측점만 - B07 설정 · 도각 · 그 도면 manifest 칸 = 그 범위만 바뀜 - 평면도 저장본 열쇠 = 지문 · 읽기는 안 지움 · 최근 2벌 · 해상도(종이 0.1 mm 솎음 · 1 µm 끊음) ./venv/Scripts/python.exe -m pytest resources/tester/test_508_3_b07_fingerprint.py -q """ from __future__ import annotations import json import os import sys import time from pathlib import Path from types import SimpleNamespace import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B07_DesignDetail import B07_DesignDetail_Router_Support_Fingerprint as fp # noqa: E402 from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Basin import _thinned # noqa: E402 SHEETS = [ {"id": "cross_s00000m", "number": 1, "chainages": [0, 20], "grid": [4, 4]}, {"id": "cross_s00040m", "number": 2, "chainages": [40, 60], "grid": [4, 4]}, ] def _item(drawing_id: str, kind: str) -> SimpleNamespace: return SimpleNamespace(id=drawing_id, kind=kind) ITEMS = [ _item("cover", "cover"), _item("longitudinal_1", "longitudinal"), _item("cross_s00000m", "cross"), _item("cross_s00040m", "cross"), _item("mass_haul", "mass_haul"), ] @pytest.fixture() def project(tmp_path: Path) -> dict: root = tmp_path / "proj" longitudinal = root / "B06_Section" / "longitudinal" / "longitudinal.json" longitudinal.parent.mkdir(parents=True) longitudinal.write_text(json.dumps({"stations": [{"chainage_m": 0}]}), encoding="utf-8") (root / "B07_DesignDetail").mkdir() (root / "B07_DesignDetail" / "drawing_scales.json").write_text("{}", encoding="utf-8") (root / "templates" / "drawing").mkdir(parents=True) (root / "templates" / "drawing" / "00_template_A1.json").write_text("{}", encoding="utf-8") designs = {c: {"cut_slope_ratio": 0.4, "at": c} for c in (0, 20, 40, 60)} sources = { "title": {"공사명": "시험"}, "designs": designs, "detail": {c: {"chainage_m": c, "design": designs[c]} for c in designs}, "overlays": {}, "elevations": {str(c): 100.0 for c in designs}, "sheets": SHEETS, "mass_haul": {"points": [1, 2]}, "cross_inputs": lambda c: {"chainage": c, "design": sources["designs"].get(c)}, } return {"root": root, "longitudinal": longitudinal, "sources": sources} def _prints(project: dict) -> dict: return fp.compute_fingerprints( project["root"], project["longitudinal"], ITEMS, project["sources"] ) def _changed(before: dict, after: dict) -> set[str]: return {key for key in before if before[key]["fingerprint"] != after[key]["fingerprint"]} def test_같은_입력_같은_지문(project): first = _prints(project) assert first == _prints(project) assert all(len(value["fingerprint"]) == 16 for value in first.values()) assert set(first["cross_s00000m"]["stations"]) == {"0", "20"} def test_같은_내용으로_다시_쓴_종단은_그대로(project): before = _prints(project) path: Path = project["longitudinal"] text = path.read_text(encoding="utf-8") time.sleep(0.02) path.write_text(text, encoding="utf-8") # B06 [저장]이 같은 내용으로 다시 씀 os.utime(path, None) assert _changed(before, _prints(project)) == set() path.write_text(json.dumps({"stations": [{"chainage_m": 0}, {"chainage_m": 20}]})) assert _changed(before, _prints(project)) == {"longitudinal_1", "mass_haul"} def test_횡단_측점_하나_바꾸면_그_장만(project): before = _prints(project) sources = project["sources"] sources["designs"] = {**sources["designs"], 40: {"cut_slope_ratio": 0.5, "at": 40}} sources["detail"] = {**sources["detail"], 40: {"chainage_m": 40, "design": {"x": 1}}} after = _prints(project) assert _changed(before, after) == {"cross_s00040m"} stations_before = before["cross_s00040m"]["stations"] stations_after = after["cross_s00040m"]["stations"] assert stations_before["60"] == stations_after["60"] assert stations_before["40"] != stations_after["40"] def test_덧그림_범위만_바뀌어도_그_장(project): before = _prints(project) project["sources"]["overlays"] = {20: (-5.0, 5.0, 90.0, 110.0)} assert _changed(before, _prints(project)) == {"cross_s00000m"} def test_설정_도각_manifest_범위(project): root: Path = project["root"] before = _prints(project) (root / "B07_DesignDetail" / "drawing_scales.json").write_text('{"cross": {"x": 200}}') after = _prints(project) assert _changed(before, after) == {item.id for item in ITEMS} # 설정 = 모든 도면 (root / "templates" / "drawing" / "00_template_A1.json").write_text('{"a": 1}') again = _prints(project) assert _changed(after, again) == {item.id for item in ITEMS} # 도각 = 모든 도면 manifest = {"drawings": {"cover": {"number": 1}}} (root / "B07_DesignDetail" / "manifest.json").write_text(json.dumps(manifest)) last = _prints(project) assert _changed(again, last) == {"cover"} # 그 도면 칸만 · manifest 파일은 설정이 아님 # 그린 결과 폴더(plan_pieces)는 입력 아님 (root / "B07_DesignDetail" / "plan_pieces" / "1").mkdir(parents=True) (root / "B07_DesignDetail" / "plan_pieces" / "1" / "contour.json").write_text("[]") assert _changed(last, _prints(project)) == set() def test_DB_유토만_바뀌면_토적도만(project): before = _prints(project) project["sources"]["mass_haul"] = {"points": [1, 3]} assert _changed(before, _prints(project)) == {"mass_haul"} def test_파일_도장_내용_대_시각(tmp_path, monkeypatch): small = tmp_path / "a.json" small.write_text("1") first = fp.stamp(small) os.utime(small, (1, 1)) assert fp.stamp(small) == first # 작은 파일 = 내용 monkeypatch.setattr(fp, "_CONTENT_MAX", 0) assert fp.stamp(small) == "1000000000:1" # 큰 파일 = 시각:크기 assert fp.stamp(tmp_path / "없음") is None def test_평면도_저장본_열쇠_읽기는_안_지움(tmp_path): root = tmp_path / "proj" assert fp.read_plan_copy(root, "plan_terrain", "k1") is None fp.write_plan_copy(root, "plan_terrain", "k1", b'{"a":1}') fp.write_plan_copy(root, "plan_route", "r1", b'{"r":1}') folder = root / "_derived" / "b07_plan_plan_terrain" assert fp.read_plan_copy(root, "plan_terrain", "k1") == b'{"a":1}' assert fp.read_plan_copy(root, "plan_terrain", "다른") is None # 지문 다름 = 없음 assert (folder / "k1.json").is_file() # 읽기 · 빗나감 모두 안 지움 time.sleep(0.02) fp.write_plan_copy(root, "plan_terrain", "k2", b'{"a":2}') time.sleep(0.02) fp.read_plan_copy(root, "plan_terrain", "k1") # 꺼낸 것 = 최근 time.sleep(0.02) fp.write_plan_copy(root, "plan_terrain", "k3", b'{"a":3}') assert sorted(p.name for p in folder.glob("*.json")) == ["k1.json", "k3.json"] # 최근 2벌 assert (root / "_derived" / "b07_plan_plan_route" / "r1.json").is_file() # 다른 도면 그대로 assert not list(folder.glob("*.tmp")) def test_해상도_종이_솎음(tmp_path): # 종이 mm 직선 위 점 100 개 + 0.05 mm 흔들림 = 양 끝만 · 좌표 1 µm points = [(i * 0.5 + 0.0000123, (0.05 if i % 2 else 0.0)) for i in range(100)] thinned = _thinned(points) assert len(thinned) == 2 assert all(round(v, 3) == v for xy in thinned for v in xy) bend = [(0.0, 0.0), (10.0, 0.5), (20.0, 0.0)] # 0.5 mm 꺾임은 남김 assert len(_thinned(bend)) == 3 def test_평면도_등고선_솎음_적용(): from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import build_plan_kind_drawing scale = 1200 line = [(i * 0.6, 0.00001 * (i % 2)) for i in range(200)] # 실좌표 m — 종이에선 곧은 선 box = (0.0, -10.0, 120.0, 10.0) pieces = {"contour": {"lines": [line]}, "centerline": {"lines": [line]}} drawing = build_plan_kind_drawing("plan_t", "지형", box, pieces, scale) lines = { entity["layerId"]: entity for entity in drawing["entities"] if entity.get("type") == "PolyLine" } contour = next(value for key, value in lines.items() if "contour" in key) center = next(value for key, value in lines.items() if "center" in key) assert len(contour["children"]) == 1 # 배경선 = 솎음 assert len(center["children"]) == 199 # 주제선(중심선)은 그대로 def test_값_꼴_경고_글만_바뀐_저장은_그대로(project): # 옛 판 자료의 첫 B06 [저장] = 4.0 → 4 · 경고 글 꼴만 바뀜(2026-10-22 실측) — 그림은 같음 path: Path = project["longitudinal"] path.write_text(json.dumps({"stations": [{"chainage_m": 0.0}], "warnings": ["a"]})) sources = project["sources"] sources["designs"] = {c: {**d, "width": 4.0} for c, d in sources["designs"].items()} sources["detail"] = {c: {"chainage_m": c, "design": d} for c, d in sources["designs"].items()} before = _prints(project) path.write_text(json.dumps({"stations": [{"chainage_m": 0}], "warnings": ["a", "b"]})) sources["designs"] = {c: {**d, "width": 4} for c, d in sources["designs"].items()} sources["detail"] = { c: {"chainage_m": float(c), "design": d} for c, d in sources["designs"].items() } assert _changed(before, _prints(project)) == set()