"""설정 파일 ↔ 엔진 코드 일관성 (GPU 없이). 2026-10-05 전체 재검토에서 나온 '조용히 무시되는 설정' 방지. - configs/sim.json 의 키를 viewer.cpp load_sim_config 가 실제로 읽는가 - configs/render_graph.json 의 참조(파이프라인·실행기·그리기 항목·렌더 대상·셰이더 빌드)가 다 있는가 - bindless 슬롯 상수가 셰이더와 C++ 에서 같은가 - 빌드 목록의 셰이더가 모두 컴파일되는가 (glslc 가 있으면) """ import json import re import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] VIEWER = (ROOT / "src/vkgs/src/viewer.cpp").read_text() CMAKE = (ROOT / "src/vkgs/CMakeLists.txt").read_text() SHADERS = ROOT / "src/vkgs/shaders" def _strip_doc(d): return {k: v for k, v in d.items() if not k.startswith("_")} def _load_sim_config_src() -> str: i = VIEWER.index("void load_sim_config(") return VIEWER[i: VIEWER.index("\n}\n", i)] PY_ONLY = {"drive.start_offset_m"} # 파이썬(nusim render map 출발 프레임)이 읽는 키 def test_sim_json_keys_are_read(): src = _load_sim_config_src() read = set(re.findall(r'"([a-z_0-9]+)"', src)) sim = json.loads((ROOT / "configs/sim.json").read_text()) missing = [] for sec, val in _strip_doc(sim).items(): if sec == "ego": # 파이썬(nusim render view)이 읽는다 continue if sec not in read: missing.append(sec) if isinstance(val, dict) and sec not in ("shader_graphs", "passes"): # 이 둘은 이름이 데이터 for k, v in _strip_doc(val).items(): if k == "presets": continue if k not in read and f"{sec}.{k}" not in PY_ONLY: missing.append(f"{sec}.{k}") if isinstance(v, dict) and k != "defaults": missing += [f"{sec}.{k}.{kk}" for kk in _strip_doc(v) if kk not in read] assert not missing, f"viewer.cpp load_sim_config 가 읽지 않는 sim.json 키: {missing}" def test_weather_preset_fields_and_aug_weather_names(): i = VIEWER.index("void set_weather_field(") fields = set(re.findall(r'k == "([a-z_]+)"', VIEWER[i: VIEWER.index("\n}\n", i)])) sim = json.loads((ROOT / "configs/sim.json").read_text()) names = set() for p in sim["weather"]["presets"]: names.add(p["name"]) bad = set(p["values"]) - fields assert not bad, f"프리셋 {p['name']} 의 모르는 값 {bad} (set_weather_field: {sorted(fields)})" assert set(sim["weather"]["defaults"]) <= fields assert sim["weather"].get("start", "clear") in names variants = json.loads((ROOT / "configs/aug/rig_variants.json").read_text())["variants"] for v in variants: if v.get("weather"): assert v["weather"] in names, f"rig 변형 {v['name']} 의 날씨 {v['weather']} 가 sim.json 프리셋에 없음" def _registered(kind: str) -> set: return set(re.findall(kind + r'\["([a-z_0-9]+)"\]\s*=', VIEWER)) def test_render_graph_references(): g = json.loads((ROOT / "configs/render_graph.json").read_text()) pipes = _strip_doc(g["pipelines"]) res = _strip_doc(g["resources"]) targets = _strip_doc(g.get("targets", {})) pass_kinds, draws = _registered("pass_exec_"), _registered("draw_exec_") built = set(re.findall(r"vkgs_add_shader\(shaders/([^)]+)\)", CMAKE)) graphs = {p.stem + ".comp" for p in (ROOT / "configs/shader_graphs").glob("*.json")} for name, p in pipes.items(): for stage in ("vertex", "fragment", "compute"): if stage in p: assert p[stage] in built | graphs, f"파이프라인 {name}: {p[stage]} 가 CMake 빌드 목록에 없음" if p.get("target", "scene") != "scene": assert p["target"] in targets, f"파이프라인 {name}: 모르는 target {p['target']}" for t, d in targets.items(): for c in d["color"]: assert c in res and res[c].get("type") == "image" and "create" in res[c], f"target {t}: {c} 는 create 이미지여야 함" names = [p["name"] for p in g["passes"]] assert len(names) == len(set(names)) for p in g["passes"]: assert p["kind"] in pass_kinds, f"패스 {p['name']}: 실행기 '{p['kind']}' 가 등록돼 있지 않음 ({sorted(pass_kinds)})" for r, _ in p.get("uses", []): assert r in res, f"패스 {p['name']}: 모르는 리소스 {r}" prm = p.get("params", {}) for key in ("pipeline", "hist", "scan", "scatter", "fallback_pipeline"): if key in prm: assert prm[key] in pipes, f"패스 {p['name']}: {key}={prm[key]} 파이프라인 없음" for d in prm.get("draws", []): assert d in draws, f"패스 {p['name']}: 그리기 항목 '{d}' 실행기 없음" if prm.get("target", "scene") != "scene": assert prm["target"] in targets if p.get("follows"): assert p["follows"] in names if p["kind"] == "radix_sort": assert prm.get("key_bits", 32) == 32 hist = res[prm["histogram"]]["create"]["bytes"] assert hist >= prm.get("groups", 512) * 256 * 4, "sort_hist 가 groups × 256 × 4 보다 작음" def test_bindless_slots_match(): glsl = (SHADERS / "bindless.glsl").read_text() consts = {k: int(v) for k, v in re.findall(r"(k(?:Ssbo|Ubo|Img)\w+) = (\d+)u", glsl)} cpp = {k: int(v) for k, v in re.findall(r"(k(?:Ssbo|Ubo|Img)\w+) = (\d+)", VIEWER)} assert consts, "bindless.glsl 에서 슬롯 상수를 못 찾음" for k, v in consts.items(): assert cpp.get(k) == v, f"{k}: bindless.glsl {v} vs viewer.cpp {cpp.get(k)}" @pytest.mark.skipif(shutil.which("glslc") is None, reason="glslc 없음 (source scripts/local_env.sh)") def test_all_shaders_compile(tmp_path): bad = [] for name in sorted(set(re.findall(r"vkgs_add_shader\(shaders/([^)]+)\)", CMAKE))): r = subprocess.run(["glslc", "--target-env=vulkan1.2", "-O", "-I", str(SHADERS), str(SHADERS / name), "-o", str(tmp_path / (name + ".spv"))], capture_output=True, text=True) if r.returncode: bad.append(f"{name}: {r.stderr.strip()[:300]}") assert not bad, "\n".join(bad)