⚠ **뿌리** — `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>
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""직행 업로드(`POST /api/projects/{id}/files`)가 청크 경로와 같은 보호막을 갖는지.
|
|
|
|
왜 있나(2026-09-08) — 창 넷이 DB·저장소를 함께 쓰는데 이 갈래만
|
|
① 분석 중 차단(`is_analysis_running`) ② 같은 이름 옛 행 내리기(`supersede_previous_input_files`)
|
|
가 빠져 있었다. 빠지면 분석 2개가 같은 산출물 경로에서 부딪히고, 같은 파일이 두 줄로
|
|
활성으로 남아 어느 것으로 도는지 순서에 달린다.
|
|
|
|
파일 내용(소스 문자열)에 기대지 않는다 — **함수 객체를 실제로 호출**해 확인한다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
import B03_FileInput.B03_FileInput_Router as router_module
|
|
|
|
|
|
class _Cursor:
|
|
async def __aenter__(self) -> "_Cursor":
|
|
return self
|
|
|
|
async def __aexit__(self, *_: Any) -> None:
|
|
return None
|
|
|
|
async def execute(self, *_: Any, **__: Any) -> None:
|
|
return None
|
|
|
|
async def fetchone(self) -> None:
|
|
return None
|
|
|
|
|
|
class _Connection:
|
|
def cursor(self, *_: Any, **__: Any) -> _Cursor:
|
|
return _Cursor()
|
|
|
|
|
|
class _Acquire:
|
|
async def __aenter__(self) -> _Connection:
|
|
return _Connection()
|
|
|
|
async def __aexit__(self, *_: Any) -> None:
|
|
return None
|
|
|
|
|
|
class _Pool:
|
|
def acquire(self) -> _Acquire:
|
|
return _Acquire()
|
|
|
|
|
|
class _Upload(SimpleNamespace):
|
|
"""UploadFile 흉내 — 라우터가 마지막에 `close()` 를 부른다."""
|
|
|
|
async def close(self) -> None:
|
|
return None
|
|
|
|
|
|
def _upload(filename: str) -> _Upload:
|
|
return _Upload(filename=filename, size=10)
|
|
|
|
|
|
_FULL_SET = ["a.las", "b.tif", "b.tfw", "b.prj", "route.csv"]
|
|
|
|
|
|
def test_분석_중이면_직행_업로드를_409로_막는다(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(router_module, "get_db_pool", lambda: _Pool())
|
|
monkeypatch.setattr(
|
|
router_module, "get_project_storage_relative_path", _async_return("1/3/proj")
|
|
)
|
|
monkeypatch.setattr(router_module, "resolve_stored_project_path", lambda _p: ".")
|
|
monkeypatch.setattr(router_module, "is_analysis_running", _async_return(True))
|
|
|
|
response = asyncio.run(
|
|
router_module.upload_project_files(
|
|
project_id=_PROJECT_ID,
|
|
files=[_upload(name) for name in _FULL_SET],
|
|
las_free=False,
|
|
session={"role": "USER"},
|
|
)
|
|
)
|
|
assert response.status_code == 409
|
|
|
|
|
|
def test_직행_업로드가_같은_이름_옛_행을_내린다() -> None:
|
|
"""`supersede_previous_input_files` 가 이 모듈에 실제로 이어져 있는지."""
|
|
from B03_FileInput.B03_FileInput_Repository import (
|
|
supersede_previous_input_files as repository_function,
|
|
)
|
|
|
|
assert router_module.supersede_previous_input_files is repository_function
|
|
|
|
|
|
def test_세_갈래가_같은_보호막을_쓴다() -> None:
|
|
"""청크·임시배치·직행이 같은 두 함수를 쓰는지 — 한 갈래만 빠지는 사고를 막는다."""
|
|
import B03_FileInput.B03_FileInput_Router_Chunks as chunks
|
|
import B03_FileInput.B03_FileInput_Router_Temp as temp
|
|
|
|
for module in (router_module, chunks, temp):
|
|
assert hasattr(module, "is_analysis_running"), module.__name__
|
|
assert hasattr(module, "supersede_previous_input_files"), module.__name__
|
|
# 확인 단계도 세 갈래가 다 들고 있어야 한다 — 하나만 빠지면 그 갈래가 500 으로 죽는다
|
|
# (2026-09-08 실제로 청크 갈래에서 `NameError` 로 겪었다).
|
|
for module in (router_module, chunks, temp):
|
|
assert hasattr(module, "OutputsWouldBeDiscarded"), module.__name__
|
|
assert hasattr(module, "_confirm_replace_response"), module.__name__
|
|
|
|
|
|
from uuid import UUID # noqa: E402 — 아래 상수에서만 쓴다
|
|
|
|
_PROJECT_ID = UUID("fa76c162-71c7-46e5-a95d-fb3930665a45")
|
|
|
|
|
|
def _async_return(value: Any):
|
|
async def _inner(*_: Any, **__: Any) -> Any:
|
|
return value
|
|
|
|
return _inner
|