feat: 가상화 문서들 추가

This commit is contained in:
DongHyeonka
2026-09-10 08:54:05 +09:00
parent e9f6a93327
commit 43e1aadef0
695 changed files with 153404 additions and 12754 deletions
+91
View File
@@ -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()
+148
View File
@@ -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()
+28 -10
View File
@@ -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:
+133
View File
@@ -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()
+141
View File
@@ -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()