feat: 가상화 문서들 추가
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""그림 겹침 검사기. 겹친 것을 실제로 잡고 안 겹친 것은 안 잡는지 본다."""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
SCRIPT = os.path.join(ROOT, "scripts", "check-figure-overlap.py")
|
||||
|
||||
HEAD = '<svg xmlns="http://www.w3.org/2000/svg"><rect class="canvas" width="400" height="300"/>'
|
||||
TAIL = "</svg>"
|
||||
|
||||
|
||||
def svg(*rects):
|
||||
return HEAD + "".join(rects) + TAIL
|
||||
|
||||
|
||||
def rect(cls, x, y, w, h):
|
||||
return f'<rect class="{cls}" x="{x}" y="{y}" width="{w}" height="{h}"/>'
|
||||
|
||||
|
||||
def run(body):
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".svg", delete=False,
|
||||
encoding="utf-8") as fh:
|
||||
fh.write(body)
|
||||
path = fh.name
|
||||
try:
|
||||
p = subprocess.run([sys.executable, SCRIPT, "--file", path],
|
||||
capture_output=True, text=True, cwd=ROOT)
|
||||
return p.returncode, p.stdout + p.stderr
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class Overlap(unittest.TestCase):
|
||||
def test_안_겹치면_통과한다(self):
|
||||
code, out = run(svg(rect("group-box", 0, 0, 100, 100),
|
||||
rect("group-box", 150, 0, 100, 100)))
|
||||
self.assertEqual(code, 0, out)
|
||||
self.assertIn("PASS", out)
|
||||
|
||||
def test_맞닿기만_한_것은_겹친_것이_아니다(self):
|
||||
code, out = run(svg(rect("group-box", 0, 0, 100, 100),
|
||||
rect("group-box", 100, 0, 100, 100)))
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
def test_구역_둘이_겹치면_잡는다(self):
|
||||
code, out = run(svg(rect("group-box", 0, 0, 100, 100),
|
||||
rect("group-box", 50, 50, 100, 100)))
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("구역 둘이 겹쳐 그려졌다", out)
|
||||
|
||||
def test_이음_라벨이_상자에_먹히면_잡는다(self):
|
||||
code, out = run(svg(rect("node-shape kind-service", 100, 100, 120, 60),
|
||||
rect("edge-label-bg", 80, 110, 60, 20)))
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("이음 라벨이 상자에 먹혔다", out)
|
||||
|
||||
def test_구역_이름이_서로를_덮으면_잡는다(self):
|
||||
code, out = run(svg(rect("group-label-bg", 10, 10, 120, 22),
|
||||
rect("group-label-bg", 20, 12, 120, 22)))
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("구역 이름이 서로를 덮는다", out)
|
||||
|
||||
def test_상자_안에_든_상자는_설계다(self):
|
||||
"""node-shape 가 group-box 안에 드는 것은 정상이라 잡지 않는다."""
|
||||
code, out = run(svg(rect("group-box", 0, 0, 200, 200),
|
||||
rect("node-shape kind-service", 20, 20, 80, 40)))
|
||||
self.assertEqual(code, 0, out)
|
||||
|
||||
def test_겹친_자리의_이름을_말해_준다(self):
|
||||
body = svg(rect("node-shape kind-service", 100, 100, 120, 60),
|
||||
rect("edge-label-bg", 80, 110, 60, 20)) \
|
||||
.replace(TAIL, '<text class="edge-label" x="82" y="124">토큰</text>' + TAIL)
|
||||
code, out = run(body)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("토큰", out)
|
||||
|
||||
|
||||
class Project(unittest.TestCase):
|
||||
@unittest.skipUnless(os.path.isdir(os.path.join(ROOT, "docs/keycloak/final/assets")),
|
||||
"keycloak 그림이 없다")
|
||||
def test_프로젝트_단위로도_돈다(self):
|
||||
p = subprocess.run([sys.executable, SCRIPT, "keycloak"],
|
||||
capture_output=True, text=True, cwd=ROOT)
|
||||
self.assertIn("FIGURE OVERLAP", p.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,148 @@
|
||||
"""런 원장 검사기. 절차를 안 지킨 원장을 실제로 잡는지 본다.
|
||||
|
||||
이 검사기의 값은 「통과시키는 것」이 아니라 「안 지킨 것을 잡는 것」이라, 시험도 전부
|
||||
위반을 넣어 걸리는지 보는 모양이다.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
SCRIPT = os.path.join(ROOT, "scripts", "verify-pipeline-run.py")
|
||||
LEDGER = os.path.join(ROOT, "runs", "keycloak", "2026-09-07-2215", "run.json")
|
||||
|
||||
|
||||
def run(path, *args):
|
||||
p = subprocess.run([sys.executable, SCRIPT, path, *args],
|
||||
capture_output=True, text=True, cwd=ROOT)
|
||||
return p.returncode, p.stdout + p.stderr
|
||||
|
||||
|
||||
@unittest.skipUnless(os.path.exists(LEDGER), "실증 런 원장이 없다")
|
||||
class LedgerRules(unittest.TestCase):
|
||||
"""실제로 돈 원장을 밑감으로 삼아 한 군데씩 어긴다."""
|
||||
|
||||
def setUp(self):
|
||||
self.base = json.load(open(LEDGER, encoding="utf-8"))
|
||||
self.dir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.dir.cleanup)
|
||||
|
||||
def write(self, mutate):
|
||||
run_ = json.loads(json.dumps(self.base))
|
||||
mutate({s["id"]: s for s in run_["stages"]}, run_)
|
||||
path = os.path.join(self.dir.name, "run.json")
|
||||
json.dump(run_, open(path, "w", encoding="utf-8"), ensure_ascii=False)
|
||||
return path
|
||||
|
||||
def test_실증한_원장은_통과한다(self):
|
||||
code, out = run(LEDGER)
|
||||
self.assertEqual(code, 0, out)
|
||||
self.assertIn("PASS", out)
|
||||
|
||||
def test_영수증이_그_스킬의_문장이_아니면_잡는다(self):
|
||||
path = self.write(lambda st, _: st["S5"].update(
|
||||
skillEcho="이 문장은 그 스킬 어디에도 없다 정말로 없다 한 글자도"))
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("스킬 영수증이 그 스킬의 문장이 아니다", out)
|
||||
|
||||
def test_영수증이_비면_잡는다(self):
|
||||
path = self.write(lambda st, _: st["S6"].update(skillEcho=""))
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("스킬 영수증이 없다", out)
|
||||
|
||||
def test_관문이_실패하면_잡는다(self):
|
||||
def m(st, _):
|
||||
st["S3"]["gates"][0]["exit"] = 1
|
||||
path = self.write(m)
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("관문이 통과하지 못했다", out)
|
||||
|
||||
def test_관문이_빠지면_잡는다(self):
|
||||
path = self.write(lambda st, _: st["S6"].update(gates=[]))
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("관문이 빠졌다", out)
|
||||
|
||||
def test_건너뛸_수_없는_단계를_건너뛰면_잡는다(self):
|
||||
path = self.write(lambda st, _: st["S3"].update(
|
||||
status="SKIPPED", skipReason="그냥"))
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("건너뛸 수 없는 단계를 건너뛰었다", out)
|
||||
|
||||
def test_건너뛴_사유가_없으면_잡는다(self):
|
||||
path = self.write(lambda st, _: st["S4"].update(skipReason=""))
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("건너뛴 사유가 없다", out)
|
||||
|
||||
def test_없는_산출물을_적으면_잡는다(self):
|
||||
path = self.write(lambda st, _: st["S1"].update(
|
||||
outputs=["docs/keycloak/없는파일.md"]))
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("적어 낸 산출물이 디스크에 없다", out)
|
||||
|
||||
def test_다른_스킬을_쓰면_잡는다(self):
|
||||
path = self.write(lambda st, _: st["S7"].update(skill="technical-visualizer"))
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("단계가 다른 스킬을 썼다", out)
|
||||
|
||||
def test_끝나지_않은_단계를_잡는다(self):
|
||||
path = self.write(lambda st, _: st["S5"].update(status="RUNNING"))
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("끝나지 않은 단계가 있다", out)
|
||||
|
||||
def test_단계가_통째로_빠지면_잡는다(self):
|
||||
def m(_, run_):
|
||||
run_["stages"] = [s for s in run_["stages"] if s["id"] != "S6"]
|
||||
path = self.write(m)
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("단계가 원장에 없다", out)
|
||||
|
||||
def test_곁증명의_영수증도_대조한다(self):
|
||||
"""건너뛴 단계라도 곁증명을 냈으면 같은 잣대로 본다."""
|
||||
proof = os.path.join(ROOT, "runs/keycloak/2026-09-07-2215/stage/S4/stage-report.json")
|
||||
if not os.path.exists(proof):
|
||||
self.skipTest("곁증명이 없다")
|
||||
data = json.load(open(proof, encoding="utf-8"))
|
||||
data["skillEcho"] = "그 스킬에 없는 문장을 곁증명에 적었다 정말로 없다"
|
||||
bad = os.path.join(self.dir.name, "stage-report.json")
|
||||
json.dump(data, open(bad, "w", encoding="utf-8"), ensure_ascii=False)
|
||||
rel = os.path.relpath(bad, ROOT)
|
||||
path = self.write(lambda st, _: st["S4"].update(outputs=[rel]))
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("곁증명의 스킬 영수증이 그 스킬의 문장이 아니다", out)
|
||||
|
||||
|
||||
class Init(unittest.TestCase):
|
||||
def test_틀에서_런을_연다(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "run.json")
|
||||
p = subprocess.run(
|
||||
[sys.executable, SCRIPT, "--init", path,
|
||||
"--project", "keycloak", "--record", "docs/keycloak/final/document.md"],
|
||||
capture_output=True, text=True, cwd=ROOT)
|
||||
self.assertEqual(p.returncode, 0, p.stdout + p.stderr)
|
||||
run_ = json.load(open(path, encoding="utf-8"))
|
||||
self.assertEqual([s["id"] for s in run_["stages"]],
|
||||
["S1", "S2", "S3", "S4", "S5", "S6", "S7"])
|
||||
self.assertEqual(run_["project"], "keycloak")
|
||||
# 갓 연 원장은 아직 아무 단계도 안 끝났으므로 통과하면 안 된다
|
||||
code, out = run(path)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("끝나지 않은 단계가 있다", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -123,15 +123,12 @@ class Fixture:
|
||||
self.cite(name)
|
||||
|
||||
def cite(self, name: str) -> None:
|
||||
"""그림을 Studio 자리로 복사하고 기록이 그것을 가리키게 한다 — 실제 작업 순서다."""
|
||||
studio_assets = os.path.join(self.base, "final/assets/tech-log-studio")
|
||||
os.makedirs(studio_assets, exist_ok=True)
|
||||
open(os.path.join(studio_assets, f"{name}.svg"), "w", encoding="utf-8").write("<svg/>")
|
||||
"""기록이 그림을 가리키게 한다. 사본을 만들지 않고 그림이 사는 자리를 그대로 쓴다."""
|
||||
record = os.path.join(self.studio, "session-custody/case/case-session-split.md")
|
||||
open(record, "w", encoding="utf-8").write(RECORD.replace(
|
||||
"status: 게시 전\n",
|
||||
f"status: 게시 전\nassets:\n - key: {name}\n"
|
||||
f" file: ../../../final/assets/tech-log-studio/{name}.svg\n"))
|
||||
f" file: ../../../final/assets/{name}/{name}.svg\n"))
|
||||
|
||||
def __enter__(self):
|
||||
self._saved = (verifier.ROOT, builder.ROOT, layout.ROOT)
|
||||
@@ -396,12 +393,33 @@ class LayoutTest(unittest.TestCase):
|
||||
fx.diagram("hand-drawn", with_source=False)
|
||||
self.assertIn("techviz 정본이 없는 그림", layout.verify("fixture").warns)
|
||||
|
||||
def test_studio_presentation_copies_need_no_source(self):
|
||||
def test_a_figure_that_is_really_a_table_is_counted(self):
|
||||
# 관계선이 없고 항목마다 같은 수의 details 면 표다. 표를 그림으로 그리지 않는다
|
||||
with Fixture() as fx:
|
||||
assets = os.path.join(fx.base, "final/assets/tech-log-studio")
|
||||
os.makedirs(assets)
|
||||
open(os.path.join(assets, "custody.svg"), "w", encoding="utf-8").write("<svg/>")
|
||||
self.assertEqual(layout.verify("fixture").warns, {})
|
||||
fx.diagram("distribution-choice")
|
||||
spec = os.path.join(fx.base, "final/.techviz/distribution-choice/spec.json")
|
||||
with open(spec, "w", encoding="utf-8") as fh:
|
||||
json.dump({"nodes": [{"id": "a", "details": ["형태", "선택"]},
|
||||
{"id": "b", "details": ["형태", "선택"]}],
|
||||
"edges": []}, fh)
|
||||
self.assertIn("표로 되는 그림", layout.verify("fixture").warns)
|
||||
|
||||
def test_a_figure_with_relations_is_not_a_table(self):
|
||||
with Fixture() as fx:
|
||||
fx.diagram("request-path", cited=True)
|
||||
spec = os.path.join(fx.base, "final/.techviz/request-path/spec.json")
|
||||
with open(spec, "w", encoding="utf-8") as fh:
|
||||
json.dump({"nodes": [{"id": "a", "details": ["형태"]},
|
||||
{"id": "b", "details": ["형태"]}],
|
||||
"edges": [{"from": "a", "to": "b"}]}, fh)
|
||||
self.assertNotIn("표로 되는 그림", layout.verify("fixture").warns)
|
||||
|
||||
def test_record_figure_without_a_techviz_source_is_counted(self):
|
||||
# 사본을 가리키면 그 그림은 다시 만들 수 없다. 정본이 둘이 되는 자리다
|
||||
with Fixture() as fx:
|
||||
fx.diagram("hand-drawn", with_source=False, cited=True)
|
||||
self.assertIn("기록이 가리키는 그림에 techviz 정본이 없다",
|
||||
layout.verify("fixture").warns)
|
||||
|
||||
def test_evidence_folder_outside_the_convention_is_an_error(self):
|
||||
with Fixture() as fx:
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"""앵커가 실재하는 절을 가리키는지, 그림 대장이 실물과 맞는지 보는 검사.
|
||||
|
||||
`verify-tech-log-tree.py` 가 오래 못 보던 두 자리다 — 앵커는 SSOT 경로를 포함하는지만
|
||||
봤고, `assetLedger` 는 아무 스크립트도 읽지 않았다.
|
||||
"""
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, os.path.join(ROOT, "scripts"))
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"verify_tech_log_tree", os.path.join(ROOT, "scripts", "verify-tech-log-tree.py"))
|
||||
V = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(V)
|
||||
|
||||
|
||||
class Headings(unittest.TestCase):
|
||||
def test_제목을_슬러그로_바꾼다(self):
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False,
|
||||
encoding="utf-8") as fh:
|
||||
fh.write("# 제목\n\n## 검토한 선택지와 막힌 지점\n\n### AP1 완주: code가 되기까지\n")
|
||||
path = fh.name
|
||||
try:
|
||||
heads = V._headings(path)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
self.assertEqual([h[0] for h in heads], [2, 3])
|
||||
self.assertEqual(heads[0][2], "검토한-선택지와-막힌-지점")
|
||||
self.assertEqual(heads[1][2], "ap1-완주-code가-되기까지")
|
||||
|
||||
def test_없는_파일은_빈_목록이다(self):
|
||||
self.assertEqual(V._headings("/없는/경로.md"), [])
|
||||
|
||||
|
||||
class AnchorBase(unittest.TestCase):
|
||||
SLUGS = ["검토한-선택지와-막힌-지점", "결정이-지켜지는지-확인하는-방법", "ap1-완주"]
|
||||
|
||||
def test_절_제목과_같으면_그것이_바탕이다(self):
|
||||
self.assertEqual(V._anchor_base("검토한-선택지와-막힌-지점", self.SLUGS),
|
||||
"검토한-선택지와-막힌-지점")
|
||||
|
||||
def test_구분자가_붙어도_바탕을_찾는다(self):
|
||||
self.assertEqual(V._anchor_base("검토한-선택지와-막힌-지점-ap1", self.SLUGS),
|
||||
"검토한-선택지와-막힌-지점")
|
||||
|
||||
def test_가장_긴_것을_고른다(self):
|
||||
slugs = ["ap1", "ap1-완주"]
|
||||
self.assertEqual(V._anchor_base("ap1-완주-code", slugs), "ap1-완주")
|
||||
|
||||
def test_어느_절과도_안_맞으면_없다(self):
|
||||
self.assertIsNone(V._anchor_base("§1.1", self.SLUGS))
|
||||
self.assertIsNone(V._anchor_base("a18", self.SLUGS))
|
||||
|
||||
def test_슬러그의_앞부분만_같은_것은_바탕이_아니다(self):
|
||||
"""`검토한-선택지` 로 시작한다고 `검토한-선택지와-막힌-지점` 이 되지는 않는다."""
|
||||
self.assertIsNone(V._anchor_base("검토한-선택", self.SLUGS))
|
||||
|
||||
|
||||
class LedgerNames(unittest.TestCase):
|
||||
def test_이름만_적은_칸(self):
|
||||
self.assertEqual(V._ledger_names(["a", "b"]), {"a", "b"})
|
||||
|
||||
def test_사유를_함께_적은_칸(self):
|
||||
entries = [{"asset": ["a", "b"], "reason": "쓸 자리가 없다"}]
|
||||
self.assertEqual(V._ledger_names(entries), {"a", "b"})
|
||||
|
||||
def test_두_모양이_섞여도_편다(self):
|
||||
self.assertEqual(V._ledger_names(["a", {"asset": ["b"]}]), {"a", "b"})
|
||||
|
||||
def test_비면_빈_집합이다(self):
|
||||
self.assertEqual(V._ledger_names(None), set())
|
||||
|
||||
|
||||
class BareAnchor(unittest.TestCase):
|
||||
def test_그대로인_것은_그대로다(self):
|
||||
self.assertEqual(V._bare_anchor("final/document.md#가-나"), "final/document.md#가-나")
|
||||
|
||||
def test_백틱을_걷는다(self):
|
||||
self.assertEqual(V._bare_anchor("`final/document.md#10-3`"), "final/document.md#10-3")
|
||||
|
||||
def test_뒤에_붙은_절_번호를_걷는다(self):
|
||||
self.assertEqual(V._bare_anchor("`final/document.md#a05` §14.1"),
|
||||
"final/document.md#a05")
|
||||
|
||||
def test_빈_것은_빈_것이다(self):
|
||||
self.assertEqual(V._bare_anchor(" "), "")
|
||||
|
||||
|
||||
class RecordSources(unittest.TestCase):
|
||||
def test_frontmatter_의_source_를_읽는다(self):
|
||||
import tempfile
|
||||
body = ("---\n"
|
||||
"kind: CASE\n"
|
||||
"source:\n"
|
||||
" - final/document.md#가\n"
|
||||
" - final/document.md#나\n"
|
||||
"---\n\n# 제목\n")
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False,
|
||||
encoding="utf-8") as fh:
|
||||
fh.write(body)
|
||||
path = fh.name
|
||||
try:
|
||||
self.assertEqual(V._record_sources(path),
|
||||
["final/document.md#가", "final/document.md#나"])
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
def test_source_가_없으면_빈_목록(self):
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False,
|
||||
encoding="utf-8") as fh:
|
||||
fh.write("---\nkind: CASE\n---\n")
|
||||
path = fh.name
|
||||
try:
|
||||
self.assertEqual(V._record_sources(path), [])
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class RealProjects(unittest.TestCase):
|
||||
"""실물 프로젝트에 돌려 error 가 0 인지 본다. warn 은 편집 판단이라 세지 않는다."""
|
||||
|
||||
def test_keycloak_이_error_0(self):
|
||||
"""이 작업이 증명 대상으로 삼은 프로젝트. 다른 프로젝트의 미해결은 따로 센다."""
|
||||
rep = V.verify("keycloak")
|
||||
self.assertEqual(rep.error_count, 0, dict(rep.errors))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,141 @@
|
||||
"""새 규칙이 **어긴 것을 잡는지** 본다.
|
||||
|
||||
헬퍼 함수가 맞는지는 `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()
|
||||
Reference in New Issue
Block a user