142 lines
6.5 KiB
Python
142 lines
6.5 KiB
Python
"""새 규칙이 **어긴 것을 잡는지** 본다.
|
|
|
|
헬퍼 함수가 맞는지는 `test_tree_anchors.py` 가 본다. 여기서는 프로젝트 사본을 만들어
|
|
한 군데씩 어기고 검사기가 그것을 세는지 본다 — 검사기의 값은 통과시키는 것이 아니라
|
|
안 지킨 것을 잡는 것이다.
|
|
"""
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
SRC = os.path.join(ROOT, "docs", "keycloak")
|
|
|
|
|
|
def _load(name, filename, root):
|
|
"""검사기를 그 저장소 루트를 보도록 다시 읽는다."""
|
|
spec = importlib.util.spec_from_file_location(
|
|
name, os.path.join(ROOT, "scripts", filename))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
sys.path.insert(0, os.path.join(ROOT, "scripts"))
|
|
spec.loader.exec_module(mod)
|
|
mod.ROOT = root
|
|
return mod
|
|
|
|
|
|
@unittest.skipUnless(os.path.isdir(SRC), "keycloak 프로젝트가 없다")
|
|
class Sandbox(unittest.TestCase):
|
|
"""keycloak 사본을 만들어 한 군데씩 어긴다. 원본은 건드리지 않는다."""
|
|
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.tmp.cleanup)
|
|
self.root = self.tmp.name
|
|
os.makedirs(os.path.join(self.root, "docs"))
|
|
shutil.copytree(SRC, os.path.join(self.root, "docs", "keycloak"),
|
|
symlinks=True)
|
|
self.tree_path = os.path.join(
|
|
self.root, "docs/keycloak/tech-log-studio/tech-log-tree.json")
|
|
|
|
def tree(self):
|
|
return json.load(open(self.tree_path, encoding="utf-8"))
|
|
|
|
def save(self, data):
|
|
json.dump(data, open(self.tree_path, "w", encoding="utf-8"),
|
|
ensure_ascii=False, indent=2)
|
|
|
|
def run_tree(self):
|
|
return _load("vt", "verify-tech-log-tree.py", self.root).verify("keycloak")
|
|
|
|
def run_layout(self):
|
|
return _load("vl", "verify-project-layout.py", self.root).verify("keycloak")
|
|
|
|
# ── 사본이 깨끗한지 먼저 ────────────────────────────────────────
|
|
def test_사본은_깨끗하다(self):
|
|
rep = self.run_tree()
|
|
self.assertEqual(rep.error_count, 0, dict(rep.errors))
|
|
|
|
# ── 규칙 1 · 앵커가 실재하는 절을 가리키나 ─────────────────────
|
|
def test_없는_절을_가리키는_앵커를_잡는다(self):
|
|
t = self.tree()
|
|
t["candidates"][0]["sourceRefs"] = ["final/document.md#이런-절은-없다-정말로"]
|
|
self.save(t)
|
|
rep = self.run_tree()
|
|
self.assertIn("SSOT 에 없는 절을 가리키는 앵커", rep.errors)
|
|
|
|
# ── 규칙 2 · 계약과 기록의 source 가 같은가 ────────────────────
|
|
def test_계약과_기록의_source_가_다르면_잡는다(self):
|
|
t = self.tree()
|
|
for kind, items in t["topics"]["oauth-oidc-auth-boundary"]["kinds"].items():
|
|
for node in items:
|
|
if node.get("file") and node.get("source"):
|
|
node["source"] = node["source"][:1]
|
|
break
|
|
else:
|
|
continue
|
|
break
|
|
self.save(t)
|
|
rep = self.run_tree()
|
|
self.assertIn("계약과 기록의 source 가 다르다", rep.errors)
|
|
|
|
# ── 규칙 3 · 범위 안인데 아무 후보도 안 짚은 절 ────────────────
|
|
def test_아무_후보도_안_짚은_절을_센다(self):
|
|
# 전부 비우면 앵커 형식 판별이 꺼진다. 한 h2 를 가리키는 앵커만 걷어낸다
|
|
gone = "선택이-코드와-흐름에-반영되는-방식"
|
|
t = self.tree()
|
|
for c in t["candidates"]:
|
|
c["sourceRefs"] = [r for r in (c.get("sourceRefs") or []) if gone not in r]
|
|
for _, items in t["topics"]["oauth-oidc-auth-boundary"]["kinds"].items():
|
|
for node in items:
|
|
node["source"] = [r for r in (node.get("source") or []) if gone not in r]
|
|
self.save(t)
|
|
rep = self.run_tree()
|
|
self.assertIn("범위 안인데 아무 후보도 가리키지 않는 절", rep.warns)
|
|
|
|
# ── 규칙 4 · assetLedger 가 실물과 맞나 ────────────────────────
|
|
def test_없는_그림을_배정했다고_적으면_잡는다(self):
|
|
t = self.tree()
|
|
t["assetLedger"]["assigned"] = list(t["assetLedger"]["assigned"]) + ["없는-그림"]
|
|
self.save(t)
|
|
rep = self.run_tree()
|
|
self.assertIn("assetLedger 가 없는 그림을 배정했다고 적었다", rep.errors)
|
|
|
|
def test_그림이_대장에_없으면_잡는다(self):
|
|
t = self.tree()
|
|
t["assetLedger"]["assigned"] = list(t["assetLedger"]["assigned"])[1:]
|
|
self.save(t)
|
|
rep = self.run_tree()
|
|
self.assertIn("그림이 assetLedger 에 없다", rep.errors)
|
|
|
|
# ── 규칙 5 · 그림의 근거가 SSOT 인가 ───────────────────────────
|
|
def test_그림의_근거가_SSOT_가_아니면_잡는다(self):
|
|
spec_path = os.path.join(
|
|
self.root, "docs/keycloak/final/.techviz/ap4-edge-trust-architecture/spec.json")
|
|
spec = json.load(open(spec_path, encoding="utf-8"))
|
|
spec["source_context"]["document"] = "case-ap4-identity-header-trust.md"
|
|
json.dump(spec, open(spec_path, "w", encoding="utf-8"), ensure_ascii=False)
|
|
rep = self.run_layout()
|
|
self.assertIn("그림의 근거가 SSOT 가 아니다", rep.errors)
|
|
|
|
# ── 규칙 6 · 그림 안에서 상자와 라벨이 겹치나 ──────────────────
|
|
def test_겹친_그림을_배치_검사가_잡는다(self):
|
|
"""`check-figure-overlap.py` 가 `verify-project-layout.py` 의 관문인지."""
|
|
before = os.path.join(
|
|
ROOT, "runs/keycloak/2026-09-08-0030/stage/S4/before/assets",
|
|
"ap4-edge-trust-architecture/ap4-edge-trust-architecture.svg")
|
|
if not os.path.exists(before):
|
|
self.skipTest("고치기 전 그림 사본이 없다")
|
|
target = os.path.join(
|
|
self.root, "docs/keycloak/final/assets",
|
|
"ap4-edge-trust-architecture/ap4-edge-trust-architecture.svg")
|
|
shutil.copyfile(before, target)
|
|
rep = self.run_layout()
|
|
self.assertIn("그림 안에서 상자와 라벨이 겹친다", rep.errors)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|