Files
s-canvas/tests/test_regressions.py
HYUNJUNGLEE e9cc6bfcf4 Phase 0 of expert feedback (#1~#11): infrastructure + design + 1차 fixes
Implementations (즉시 동작):
- #1 crash logging: harness/crash_logger.py (sys.excepthook + threading +
  faulthandler, 회전 파일 logs/scanvas.log). main 진입점 통합.
- #2 smooth curves (1차): gate_3d_builder ogee profile를 arc-length parametric
  CubicSpline로 4× densify (8pt→32pt, 36→132 cells, 60 FPS 안전).
- #3 TIN colormap: matplotlib "terrain"의 파란색 범위 제거 → 짙은갈색→황토→
  모래→능선 LinearSegmentedColormap. 9 사이트 교체. 회귀 테스트 추가.
- #5 uv: pyproject.toml + UV_GUIDE.md. base/[py313]/[dev]/[build] extras + hatchling.
- #6,#7,#8 dev cycle infra: .pre-commit-config.yaml (ruff+secrets+위생),
  .gitea/workflows/ci.yml (Py3.11+3.13 matrix), tests/test_regressions.py
  (18 회귀 테스트, iter=1~7 fix 박제), CONTRIBUTING.md (Red→Green 알고리즘).

Design docs (다음 세션 마이그레이션 청사진):
- #4 UI/UX 전면 수정: UI_REDESIGN_PLAN.md (12 popup→1 inspector, vtkTkRenderWidget
  embedding 게이트, 4 phase × 7 sessions).
- #10 Core/Plugin: ARCHITECTURE_PLAN.md (Core 14 / Plugin 7 구조물 + 2 렌더 + 1 QA,
  STRUCTURE_REGISTRY 확장, manifest 기반 디스커버리).
- #11 perf hotspots: PERFORMANCE_BASELINE.md (19 핫스팟, P1: 타일 직렬DL 5~30s,
  캡처 직렬 4.5~15s, numpy 벡터화 가능 Python loops, 텍스처 4회 반복read).

Behavior preservation: ruff 0 errors, pytest 17 passed/1 skipped(bpy),
import 33/33 OK on Py3.13.13.

Item #2 P2/P3 곡선, #4 UI 마이그레이션, #10 Phase 1 추출, #11 P1 최적화는 차기 세션.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:45:30 +09:00

215 lines
8.8 KiB
Python

"""회귀 테스트 — 발견된 버그가 다시 들어오지 못하게 박제.
피드백 #7 인용:
> "버그가 식별되면 수정 후 재발하지 않도록 하는 알고리즘도 필요"
새 버그 발견 → 여기 테스트 추가 → ruff + pytest CI에서 자동 검증.
"""
from __future__ import annotations
import pytest
# ============================================================================
# iter=1 / iter=2: 비동기 lambda + except/loop 변수 캡처 NameError
# (8 + 6 = 14건. 모든 케이스가 `lambda VAR=VAR:` 형식의 default-arg 캡처로 수정됨.)
# ============================================================================
def test_iter1_lambda_default_arg_pattern():
"""`except as e:` 직후 `app.after(..., lambda: ...{e}...)` 패턴은 NameError를 유발.
수정안 `lambda e=e:` 가 작동하는지 검증.
"""
queued: list = []
def fail_then_queue_unfixed():
try:
raise ValueError("boom")
except Exception as e: # noqa: F841 (의도적 demo — async lambda가 e 캡처)
queued.append(lambda: f"unfixed: {e}") # noqa: F821 (의도적 demo)
def fail_then_queue_fixed():
try:
raise ValueError("boom")
except Exception as e:
queued.append(lambda e=e: f"fixed: {e}") # default-arg capture
fail_then_queue_unfixed()
fail_then_queue_fixed()
# Unfixed: 비동기 시점에 `e`가 사라져 NameError.
with pytest.raises(NameError):
queued[0]()
# Fixed: default-arg가 lambda 인스턴스에 락-인.
assert queued[1]() == "fixed: boom"
def test_iter2_loop_var_capture():
"""B023: 루프 변수가 비동기 lambda에 free-var로 캡처되는 패턴.
`lambda _m=_m:` 형식이 올바르게 락-인하는지 검증.
"""
deferred: list = []
# 미수정 패턴 — 마지막 값으로 모두 통일됨 (PERF401/B023 의도적 demo)
for x in [1, 2, 3]:
deferred.append(lambda: x) # noqa: B023, PERF401 (의도적 demo)
assert all(f() == 3 for f in deferred), "free-var 캡처는 마지막 x=3로 통일"
# 수정 패턴 — 각 lambda가 자기 시점의 x를 락-인
fixed: list = []
for x in [1, 2, 3]:
fixed.append(lambda x=x: x) # noqa: PERF401 (demo 대응)
assert [f() for f in fixed] == [1, 2, 3]
# ============================================================================
# iter=4: B905 zip strict — sliding window에서 strict=True가 깨지는지 검증
# ============================================================================
def test_sliding_window_strict_false():
"""validate_gate_params.py:254에서 `pairwise(el_chain)` 사용. strict=True였다면
N과 N-1 길이 차로 ValueError. pairwise는 그 자체로 sliding-pair semantics.
"""
from itertools import pairwise
el_chain = [("a", 1.0), ("b", 2.0), ("c", 3.0), ("d", 4.0)]
pairs = list(pairwise(el_chain))
assert len(pairs) == len(el_chain) - 1
assert pairs[0][0] == el_chain[0]
assert pairs[-1][1] == el_chain[-1]
# ============================================================================
# iter=6: RUF012 ClassVar — class-level mutable default가 인스턴스 간 공유되는지 검증
# ============================================================================
def test_classvar_singleton_pattern():
"""structure_templates.TemplateRegistry는 ClassVar로 명시된 _instance/_templates를
가진다. 모든 인스턴스가 같은 dict를 공유해야 함.
"""
try:
from structure_templates import TemplateRegistry
except ImportError:
pytest.skip("structure_templates 미설치 (외부 deps 필요)")
r1 = TemplateRegistry()
r2 = TemplateRegistry()
assert r1 is r2 # __new__ 싱글톤 — 동일 인스턴스
assert r1._templates is r2._templates # ClassVar — 동일 dict
def test_gate_parser_struct_layers_classvar():
"""gate_parser.GateParser.STRUCT_LAYERS 가 ClassVar set 인지 (인스턴스 X)."""
try:
from gate_parser import GateParser
except ImportError:
pytest.skip("gate_parser 미설치")
p1 = GateParser()
p2 = GateParser()
assert p1.STRUCT_LAYERS is p2.STRUCT_LAYERS # 같은 set 객체 공유
assert "CS-CONC-Spillway" in p1.STRUCT_LAYERS
# ============================================================================
# iter=6: RUF013 implicit Optional — dxf_geometry._process_entity 시그니처
# ============================================================================
def test_dxf_geometry_inherited_layer_optional_str():
"""dxf_geometry._process_entity의 `inherited_layer` 인자는 명시적 Optional[str]."""
try:
from dxf_geometry import extract_structural_geometry
except ImportError:
pytest.skip("dxf_geometry 미설치")
# 타입 자체는 import만 통과하면 OK (런타임 검증 어려움 — annotation 검사로 충분).
import inspect
src = inspect.getsource(extract_structural_geometry)
# 내부 정의된 _process_entity의 시그니처 텍스트 검색
assert "inherited_layer: str | None = None" in src or \
"inherited_layer: Optional[str]" in src
# ============================================================================
# iter=7: SIM102 collapsible-if — gate_3d_builder_bpy 진입점 보호
# (apply_blender_patch가 import 시 main()이 우발 실행 안 됨)
# ============================================================================
def test_gate_3d_builder_bpy_no_unexpected_main_on_import():
"""gate_3d_builder_bpy 의 진입점 가드는 `__name__ == "__main__"` AND --params 일 때만."""
try:
import sys
original_argv = sys.argv[:]
sys.argv = ["test", "--params", "fake.json"] # --params이 있어도
try:
# import만 시도 — main() 호출되면 예외/크래시
import importlib
import gate_3d_builder_bpy
importlib.reload(gate_3d_builder_bpy)
finally:
sys.argv = original_argv
except ModuleNotFoundError:
pytest.skip("gate_3d_builder_bpy bpy 미설치 (Blender 환경)")
except SystemExit:
pytest.fail("import만으로 main()이 실행되었다 — __name__ 가드 손상")
# ============================================================================
# Phase 1A (이번 세션): crash_logger
# ============================================================================
def test_crash_logger_install_idempotent(tmp_path, monkeypatch):
"""install_crash_handlers는 재호출해도 안전 (no-op)."""
from harness import crash_logger
# 기존 상태 백업
orig_excepthook = crash_logger.sys.excepthook
try:
log_dir1 = crash_logger.install_crash_handlers()
log_dir2 = crash_logger.install_crash_handlers()
assert log_dir1 == log_dir2
assert log_dir1.exists()
finally:
crash_logger.sys.excepthook = orig_excepthook
# ============================================================================
# Phase 1B (이번 세션): TIN colormap 파란색 제거
# ============================================================================
def test_tin_colormap_no_blue():
"""_TIN_EARTH_CMAP은 RGB의 B 채널이 R/G보다 작도록 (즉, 차가운 색이 아님) 보장."""
try:
from scanvas_maker import _TIN_EARTH_CMAP
except ImportError:
pytest.skip("scanvas_maker 미설치 (GUI deps)")
import numpy as np
# 5단계 샘플링 후 각 RGB가 earth-tone인지 확인
samples = _TIN_EARTH_CMAP(np.linspace(0, 1, 5))[:, :3] # (5, 3) RGB
for r, g, b in samples:
# earth tone: B (파랑)가 R, G 모두보다 작아야 함 — 따뜻한 톤
assert b <= r, f"파란색 우세 발견: R={r:.2f} G={g:.2f} B={b:.2f}"
assert b <= g, f"파란색 우세 발견: R={r:.2f} G={g:.2f} B={b:.2f}"
# ============================================================================
# 일반 헬스 — production 모듈 import 가능성
# ============================================================================
@pytest.mark.parametrize("module_name", [
"dxf_geometry", "filename_classifier", "optional_detector",
"polygon_reconstructor", "resource_paths",
"harness.crash_logger", "harness.seed_manager",
"harness.prompt_registry", "harness.logger",
])
def test_module_imports_without_external_deps(module_name):
"""기본 모듈은 외부 패키지 없이도 import 되거나, 명확한 ImportError를 줘야 한다."""
import importlib
try:
importlib.import_module(module_name)
except ImportError as e:
# 외부 deps 부재는 OK — 단 메시지가 도움 되는지
assert any(token in str(e).lower() for token in
("install", "필요", "required", "missing")), \
f"{module_name}: ImportError 메시지가 도움 안 됨 ({e})"