"""회귀 테스트 — 발견된 버그가 다시 들어오지 못하게 박제. 피드백 #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})"