"""납품 산출물(dist/)이 소스와 어긋났는지 검사. dist/ 는 gitignore 대상 빌드 출력이라 git 이 드리프트를 알려주지 않는다. 실제로 기능 커밋 4개가 들어간 뒤 번들이 재생성되지 않아, Report/Export/Actions 컨트롤러와 마이그레이션 1건이 빠진 패키지가 납품 직전까지 남아 있었다. 스테일 번들은 '깨진' 상태가 아니라 '내부적으로 정합적인 구버전'이라 SHA256SUMS 도 통과한다 — 그래서 소스와 대조하는 검사가 따로 필요하다. dist/ 가 없으면(클린 체크아웃) 스킵한다. 있으면 소스와 같아야 한다. """ import hashlib from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] DIST = ROOT / "dist" CAFE24_PRIV = DIST / "1_cafe24_upload" / "move_detect_app" PI_SRC = DIST / "2_pi_install" / "src" / "edge" needs_dist = pytest.mark.skipif(not DIST.is_dir(), reason="dist/ not built in this checkout") def _digest(p: Path) -> str: return hashlib.sha256(p.read_bytes()).hexdigest() def _compare_tree(src: Path, packaged: Path, suffix: str) -> tuple[list[str], list[str]]: """(빠진 파일, 내용이 다른 파일) — 소스 기준.""" missing, differing = [], [] for f in sorted(src.rglob(f"*{suffix}")): if "__pycache__" in f.parts: continue rel = f.relative_to(src) target = packaged / rel if not target.is_file(): missing.append(str(rel)) elif _digest(f) != _digest(target): differing.append(str(rel)) return missing, differing @needs_dist @pytest.mark.skipif(not CAFE24_PRIV.is_dir(), reason="cafe24 package not built") def test_cafe24_package_matches_server_source(): missing, differing = _compare_tree(ROOT / "server" / "src", CAFE24_PRIV / "src", ".php") assert not missing, f"번들에 없는 서버 소스(생성기 재실행 필요): {missing}" assert not differing, f"번들과 소스 내용 불일치: {differing}" @needs_dist @pytest.mark.skipif(not CAFE24_PRIV.is_dir(), reason="cafe24 package not built") def test_cafe24_package_ships_every_migration_and_current_schema(): # 마이그레이션이 하나라도 빠지면 기존 DB 업그레이드가 전 요청 500 이 된다. src = sorted(p.name for p in (ROOT / "server" / "migrations").glob("*.sql")) packaged = sorted(p.name for p in (CAFE24_PRIV / "migrations").glob("*.sql")) assert src == packaged, "동봉 마이그레이션이 소스와 다름 — 생성기 재실행 필요" assert _digest(ROOT / "server" / "schema.sql") == _digest(CAFE24_PRIV / "schema.sql") @needs_dist @pytest.mark.skipif(not PI_SRC.is_dir(), reason="pi bundle not built") def test_pi_bundle_matches_edge_source(): missing, differing = _compare_tree(ROOT / "edge", PI_SRC, ".py") assert not missing, f"번들에 없는 엣지 소스(생성기 재실행 필요): {missing}" assert not differing, f"번들과 소스 내용 불일치: {differing}" @needs_dist @pytest.mark.skipif(not (DIST / "2_pi_install" / "MANIFEST.txt").is_file(), reason="pi bundle not built") def test_pi_bundle_records_its_provenance(): # 버전·커밋이 없으면 현장에 올라간 빌드를 나중에 식별할 수 없다 — 스테일 번들이 조용히 # 통과한 구조적 원인. text = (DIST / "2_pi_install" / "MANIFEST.txt").read_text(encoding="utf-8") for field in ("version:", "git_commit:", "built_at:"): assert field in text, f"MANIFEST.txt에 {field} 없음" assert "version: unknown" not in text