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>
215 lines
8.8 KiB
Python
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})"
|