⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt` 가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는 시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음. - `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침** - `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시) - 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q` 옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0 ⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
"""측점 설계 묶음 저장 — 한 행짜리 함수와 **같은 결과**를 내는지 본다.
|
|
|
|
왜 (2026-09-06) — [저장]이 측점마다 `SELECT`+`UPDATE` 두 왕복을 냈고, 원격 DB 라
|
|
22행이면 670ms 였다. 세 문장으로 묶었는데 값이 달라지면 안 된다.
|
|
DB 는 가짜 커서로 대신한다 — 검사 대상은 **어떤 SQL 을 몇 번 내고, 어떤 JSON 이 되는가**다.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
|
|
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs
|
|
|
|
|
|
class FakeCursor:
|
|
def __init__(self, rows):
|
|
self._rows = rows
|
|
self.calls = []
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *args):
|
|
return False
|
|
|
|
async def execute(self, sql, params=None):
|
|
self.calls.append((" ".join(sql.split()), list(params) if params else []))
|
|
|
|
async def fetchall(self):
|
|
return self._rows
|
|
|
|
|
|
class FakeConnection:
|
|
def __init__(self, rows):
|
|
self.cursor_obj = FakeCursor(rows)
|
|
|
|
def cursor(self):
|
|
return self.cursor_obj
|
|
|
|
|
|
def _rows():
|
|
"""측점 셋 — 하나는 design 이 이미 있고, 하나는 비었고, 하나는 문자열 JSON."""
|
|
return [
|
|
(11, 0.0, {"design": {"ground_type": "soil"}, "summary": "keep"}),
|
|
(12, 20.0, {}),
|
|
(13, 40.0, json.dumps({"design": {"paved": True}})),
|
|
]
|
|
|
|
|
|
def _written(connection):
|
|
"""UPDATE 문에 실린 (id, data) 짝을 돌려준다."""
|
|
for sql, params in connection.cursor_obj.calls:
|
|
if sql.startswith("UPDATE cross_sections SET data = CASE"):
|
|
pairs = {}
|
|
# params = [id, blob, id, blob, …, id, id, …]
|
|
half = len(params) // 3 * 2
|
|
for index in range(0, half, 2):
|
|
pairs[params[index]] = json.loads(params[index + 1])
|
|
return pairs
|
|
return {}
|
|
|
|
|
|
def test_한_문장으로_여러_행을_쓴다():
|
|
connection = FakeConnection(_rows())
|
|
written = asyncio.run(
|
|
merge_cross_section_designs(
|
|
connection,
|
|
route_id=7,
|
|
entries=[(0.0, {"a": 1}), (20.0, {"b": 2}), (40.0, {"c": 3})],
|
|
replace=True,
|
|
)
|
|
)
|
|
assert written == 3
|
|
sqls = [sql for sql, _ in connection.cursor_obj.calls]
|
|
# SELECT 한 번 + UPDATE 한 번 = 왕복 두 번. 행 수와 무관해야 한다.
|
|
assert len(sqls) == 2, sqls
|
|
assert sqls[0].startswith("SELECT id, chainage_m, data")
|
|
assert _written(connection) == {
|
|
11: {"design": {"a": 1}, "summary": "keep"},
|
|
12: {"design": {"b": 2}},
|
|
13: {"design": {"c": 3}},
|
|
}
|
|
|
|
|
|
def test_patch_는_기존_design_을_보존한다():
|
|
connection = FakeConnection(_rows())
|
|
asyncio.run(
|
|
merge_cross_section_designs(
|
|
connection,
|
|
route_id=7,
|
|
entries=[(0.0, {"cut_area_m2": 1.5}), (40.0, {"cut_area_m2": 2.5})],
|
|
replace=False,
|
|
)
|
|
)
|
|
written = _written(connection)
|
|
# replace=False 는 키만 얹는다 — 옛 design 과 형제 키(summary)가 남아야 한다.
|
|
assert written[11] == {
|
|
"design": {"ground_type": "soil", "cut_area_m2": 1.5},
|
|
"summary": "keep",
|
|
}
|
|
assert written[13] == {"design": {"paved": True, "cut_area_m2": 2.5}}
|
|
|
|
|
|
def test_측점_허용오차는_1cm():
|
|
connection = FakeConnection(_rows())
|
|
asyncio.run(
|
|
merge_cross_section_designs(
|
|
connection, route_id=7, entries=[(20.005, {"x": 1})], replace=True
|
|
)
|
|
)
|
|
assert list(_written(connection)) == [12]
|
|
|
|
far = FakeConnection(_rows())
|
|
asyncio.run(
|
|
merge_cross_section_designs(far, route_id=7, entries=[(20.5, {"x": 1})], replace=True)
|
|
)
|
|
# 1cm 밖이면 붙일 행이 없다 — project_id 가 없으므로 아무 것도 안 쓴다.
|
|
assert not [sql for sql, _ in far.cursor_obj.calls if sql.startswith("UPDATE")]
|
|
|
|
|
|
def test_행이_없으면_project_id_로_새로_만든다():
|
|
connection = FakeConnection(_rows())
|
|
written = asyncio.run(
|
|
merge_cross_section_designs(
|
|
connection,
|
|
route_id=7,
|
|
entries=[(999.0, {"x": 1})],
|
|
replace=True,
|
|
project_id="11111111-2222-3333-4444-555555555555",
|
|
)
|
|
)
|
|
assert written == 1
|
|
inserts = [sql for sql, _ in connection.cursor_obj.calls if sql.startswith("INSERT")]
|
|
assert len(inserts) == 1
|
|
|
|
|
|
def test_빈_목록은_왕복을_내지_않는다():
|
|
connection = FakeConnection(_rows())
|
|
assert (
|
|
asyncio.run(merge_cross_section_designs(connection, route_id=7, entries=[], replace=True))
|
|
== 0
|
|
)
|
|
assert connection.cursor_obj.calls == []
|