Coverage for diagrams / generate.py: 100.00%
208 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-10-28 16:19 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-10-28 16:19 +0000
1#!/usr/bin/env python3
2"""Regenerate or structurally verify every committed diagram catalogue."""
4from __future__ import annotations
6import argparse
7import os
8import re
9import subprocess
10import sys
11from pathlib import Path
13ROOT = Path(__file__).resolve().parent.parent
14sys.path.insert(0, str(ROOT))
16from diagrams.api_specs.generate import api_contract_issues # noqa: E402
17from diagrams.code_diagrams._renderer import _output_stem_for # noqa: E402
18from diagrams.code_diagrams._source_marker import MARKER_SOURCE_ROOTS, SENTINEL # noqa: E402
19from diagrams.code_diagrams._targets import TARGETS # noqa: E402
20from diagrams.code_diagrams.generate import ( # noqa: E402
21 REGENERATION_HINT,
22 marker_allowed_sources,
23 newest_provenance_stamp,
24 verify_targets_match_provenance_manifest,
25)
26from diagrams.infra_diagrams._catalog import INFRA_DIAGRAM_NAMES # noqa: E402
27from gco.lambda_shared_sources import LAMBDA_SHARED_SOURCE_TARGETS # noqa: E402
29_TIMESTAMP_RE = re.compile(r"Generated at \(UTC\):[^\n]*?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)")
30_SOURCE_COMMIT_RE = re.compile(r"Generated from Git commit:[^\n]*?([0-9a-f]{40})")
31_HTML_SOURCE_COMMIT_RE = re.compile(r'<meta name="gco-source-commit" content="([0-9a-f]{40})">')
32_FLOW_DIGEST_RE = re.compile(r'<meta name="gco-flow-digest" content="([0-9a-f]{16})">')
33_MARKER_BLOCK_RE = re.compile(
34 rf"(?s)# <{re.escape(SENTINEL)}> BEGIN[^\n]*\n(.*?)# <{re.escape(SENTINEL)}> END"
35)
36_MARKER_ENTRY_RE = re.compile(
37 r"# \* ``([^`]+)`` -> ``([^`]+\.html)``\n"
38 r"(?:# \(PNG: ``([^`]+\.png)``\)\n)?"
39)
40_MARKER_BODY_RE = re.compile(
41 r"# Generated at \(UTC\): \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\n"
42 r"# Generated from Git commit: [0-9a-f]{40}\n"
43 r"# Flowchart\(s\) generated from this file:\n"
44 r"(?:# \* ``[^`]+`` -> ``[^`]+\.html``\n"
45 r"# \(PNG: ``[^`]+\.png``\)\n)+"
46 r"# Regenerate with ``SOURCE_DATE_EPOCH=<unix-seconds> "
47 r"GCO_DIAGRAM_SOURCE_COMMIT=<40-char-sha> "
48 r"python diagrams/generate\.py --code-only``\.\n?\Z"
49)
50MarkerPointer = tuple[str, str, str | None]
53def _shared_source_copy_issues(project_root: Path, target_sources: set[str]) -> list[str]:
54 """Require every checked-in shared Lambda copy to equal its target source."""
55 issues: list[str] = []
56 for source, copies in LAMBDA_SHARED_SOURCE_TARGETS.items():
57 if source not in target_sources:
58 continue
59 canonical_path = project_root / source
60 if not canonical_path.is_file():
61 issues.append(f"missing canonical shared source: {source}")
62 continue
63 canonical = canonical_path.read_bytes()
64 for copy in copies:
65 copy_path = project_root / copy
66 if not copy_path.is_file():
67 issues.append(f"missing shared source copy: {copy}")
68 elif copy_path.read_bytes() != canonical:
69 issues.append(f"shared source copy drifted: {copy} != {source}")
70 return issues
73def _marker_pointer_issues(
74 source: str,
75 expected: set[MarkerPointer],
76 source_name: str,
77) -> list[str]:
78 """Compare a source marker's exact generated grammar and pointers."""
79 begin = f"# <{SENTINEL}> BEGIN"
80 end = f"# <{SENTINEL}> END"
81 if source.count(SENTINEL) != 2 or source.count(begin) != 1 or source.count(end) != 1:
82 return [f"source marker delimiter count invalid: {source_name}"]
83 blocks = _MARKER_BLOCK_RE.findall(source)
84 if len(blocks) != 1:
85 return [f"source marker block count invalid: {source_name}"]
87 issues: list[str] = []
88 if _MARKER_BODY_RE.fullmatch(blocks[0]) is None:
89 issues.append(f"source marker grammar invalid: {source_name}")
90 matches = _MARKER_ENTRY_RE.findall(blocks[0])
91 actual: set[MarkerPointer] = {
92 (function, html_path, png_path or None) for function, html_path, png_path in matches
93 }
94 if len(matches) != len(actual):
95 issues.append(f"source marker contains duplicate pointers: {source_name}")
96 if actual != expected:
97 issues.append(
98 f"source marker pointers drifted: {source_name}: "
99 f"missing={sorted(expected - actual)!r}, stale={sorted(actual - expected)!r}"
100 )
101 return issues
104def _code_artifact_contract(project_root: Path) -> list[str]:
105 output_dir = project_root / "diagrams" / "code_diagrams"
106 expected_html: set[Path] = set()
107 expected_png: set[Path] = set()
108 for target in TARGETS:
109 stem = _output_stem_for(target, output_dir=output_dir)
110 expected_html.add(stem.parent / f"{stem.name}.html")
111 expected_png.add(stem.parent / f"{stem.name}.png")
113 actual_html = set(output_dir.rglob("*.html"))
114 actual_png = set(output_dir.rglob("*.png"))
115 issues = [
116 *(
117 f"missing code artifact: {path.relative_to(project_root)}"
118 for path in sorted(expected_html - actual_html)
119 ),
120 *(
121 f"missing code artifact: {path.relative_to(project_root)}"
122 for path in sorted(expected_png - actual_png)
123 ),
124 *(
125 f"orphan code artifact: {path.relative_to(project_root)}"
126 for path in sorted(actual_html - expected_html)
127 ),
128 *(
129 f"orphan code artifact: {path.relative_to(project_root)}"
130 for path in sorted(actual_png - expected_png)
131 ),
132 ]
134 # Freshness is verified against the committed digest manifest, not Git
135 # history: squash merges delete branch commits, so a recorded SHA is
136 # provenance metadata rather than a resolvable object. The manifest also
137 # carries each source's own stamp, which is what lets one PR restamp only
138 # the sources it changed.
139 manifest: dict[str, dict[str, str]] = {}
140 try:
141 manifest = verify_targets_match_provenance_manifest(
142 project_root=project_root,
143 targets=TARGETS,
144 )
145 except RuntimeError as exc:
146 issues.append(str(exc))
148 readme = (output_dir / "README.md").read_text(encoding="utf-8")
149 readme_timestamps = set(_TIMESTAMP_RE.findall(readme))
150 readme_source_commits = set(_SOURCE_COMMIT_RE.findall(readme))
151 if len(readme_timestamps) != 1:
152 issues.append(f"code index timestamp invalid: {sorted(readme_timestamps)}")
153 if len(readme_source_commits) != 1:
154 issues.append(f"code index source commit invalid: {sorted(readme_source_commits)}")
155 if manifest and len(readme_timestamps) == 1 and len(readme_source_commits) == 1:
156 newest_at, newest_commit = newest_provenance_stamp(manifest)
157 if (next(iter(readme_timestamps)), next(iter(readme_source_commits))) != (
158 newest_at,
159 newest_commit,
160 ):
161 issues.append(
162 "diagrams/code_diagrams/README.md: its header stamp must match the "
163 f"newest provenance entry ({newest_at} / {newest_commit}) — rerun the "
164 f"generator to rewrite the index ({REGENERATION_HINT})"
165 )
166 expected_index_links: set[str] = set()
167 checked_sources: set[str] = set()
168 html_source: dict[Path, str] = {}
169 for target in TARGETS:
170 stem = _output_stem_for(target, output_dir=output_dir)
171 for suffix in ("html", "png"):
172 path = stem.parent / f"{stem.name}.{suffix}"
173 if suffix == "html":
174 html_source[path] = target.source
175 relative = path.relative_to(output_dir).as_posix()
176 expected_index_links.add(relative)
177 if f"./{relative}" not in readme:
178 issues.append(f"code index omitted: {relative}")
179 source = (project_root / target.source).read_text(encoding="utf-8")
180 if target.source not in checked_sources:
181 checked_sources.add(target.source)
182 expected_pointers: set[MarkerPointer] = set()
183 for source_target in TARGETS:
184 if source_target.source != target.source:
185 continue
186 source_stem = _output_stem_for(source_target, output_dir=output_dir)
187 html_path = source_stem.parent / f"{source_stem.name}.html"
188 png_path = source_stem.parent / f"{source_stem.name}.png"
189 expected_pointers.add(
190 (
191 source_target.function,
192 html_path.relative_to(project_root).as_posix(),
193 png_path.relative_to(project_root).as_posix(),
194 )
195 )
196 issues.extend(_marker_pointer_issues(source, expected_pointers, target.source))
197 source_timestamps = set(_TIMESTAMP_RE.findall(source))
198 marker_source_commits = set(_SOURCE_COMMIT_RE.findall(source))
199 if len(source_timestamps) != 1:
200 issues.append(
201 f"source marker timestamp invalid: {target.source}: {sorted(source_timestamps)}"
202 )
203 if len(marker_source_commits) != 1:
204 issues.append(
205 f"source marker commit invalid: {target.source}: "
206 f"{sorted(marker_source_commits)}"
207 )
208 # Each source's marker must agree with that source's own recorded
209 # provenance — not with the rest of the catalogue.
210 if (
211 manifest
212 and len(source_timestamps) == 1
213 and len(marker_source_commits) == 1
214 and target.source in manifest
215 ):
216 entry = manifest[target.source]
217 if (next(iter(source_timestamps)), next(iter(marker_source_commits))) != (
218 entry["generated_at"],
219 entry["source_commit"],
220 ):
221 issues.append(
222 f"{target.source}: its marker block's stamp disagrees with the "
223 "provenance recorded for it — regenerate this source's diagrams "
224 f"({REGENERATION_HINT})"
225 )
226 if f"``{target.function}``" not in source:
227 issues.append(f"source marker omitted: {target.source}:{target.function}")
229 indexed_artifacts = set(re.findall(r"\]\(\./([^)]+\.(?:html|png))\)", readme))
230 for relative in sorted(indexed_artifacts - expected_index_links):
231 issues.append(f"orphan code index entry: {relative}")
233 for html in expected_html & actual_html:
234 html_text = html.read_text(encoding="utf-8")
235 html_timestamps = set(_TIMESTAMP_RE.findall(html_text))
236 if len(html_timestamps) != 1:
237 issues.append(
238 f"code artifact timestamp invalid: {html.relative_to(project_root)}: "
239 f"{sorted(html_timestamps)}"
240 )
241 html_source_commits = set(_HTML_SOURCE_COMMIT_RE.findall(html_text))
242 if len(html_source_commits) != 1:
243 issues.append(
244 f"code artifact source commit invalid: {html.relative_to(project_root)}: "
245 f"{sorted(html_source_commits)}"
246 )
247 elif f"<code>{next(iter(html_source_commits))}</code>" not in html_text:
248 issues.append(
249 f"code artifact visible source commit omitted: {html.relative_to(project_root)}"
250 )
251 # An artifact must carry its own source's stamp; sibling artifacts
252 # derived from other sources are free to be a different vintage.
253 owner = html_source.get(html)
254 if (
255 manifest
256 and owner in manifest
257 and len(html_timestamps) == 1
258 and len(html_source_commits) == 1
259 ):
260 entry = manifest[owner]
261 if (next(iter(html_timestamps)), next(iter(html_source_commits))) != (
262 entry["generated_at"],
263 entry["source_commit"],
264 ):
265 issues.append(
266 f"{html.relative_to(project_root)}: this artifact's stamp disagrees "
267 f"with the provenance recorded for {owner} — regenerate that "
268 f"source's diagrams ({REGENERATION_HINT})"
269 )
270 flow_digests = set(_FLOW_DIGEST_RE.findall(html_text))
271 if len(flow_digests) != 1:
272 issues.append(
273 f"code artifact flow digest invalid: {html.relative_to(project_root)}: "
274 f"{sorted(flow_digests)}"
275 )
276 elif f"<code>{next(iter(flow_digests))}</code>" not in html_text:
277 issues.append(
278 f"code artifact visible flow digest omitted: {html.relative_to(project_root)}"
279 )
281 issues.extend(_shared_source_copy_issues(project_root, {target.source for target in TARGETS}))
282 # Shared with the generator's marker pruning so the two can never disagree
283 # about which files may legitimately carry a marker.
284 allowed_marker_sources = marker_allowed_sources(TARGETS)
285 marker_roots = [project_root / root for root in MARKER_SOURCE_ROOTS]
286 for marker_root in marker_roots:
287 paths = [marker_root] if marker_root.is_file() else marker_root.rglob("*.py")
288 for path in paths:
289 if not path.is_file() or "-build" in path.as_posix():
290 continue
291 if SENTINEL not in path.read_text(encoding="utf-8"):
292 continue
293 relative = path.relative_to(project_root).as_posix()
294 if relative not in allowed_marker_sources:
295 issues.append(f"retired source marker: {relative}")
296 return issues
299def _infra_artifact_contract(project_root: Path) -> list[str]:
300 output_dir = project_root / "diagrams" / "infra_diagrams"
301 expected = {output_dir / f"{name}.png" for name in INFRA_DIAGRAM_NAMES}
302 actual = set(output_dir.glob("*.png"))
303 issues = [
304 *(f"missing infrastructure artifact: {path.name}" for path in sorted(expected - actual)),
305 *(f"orphan infrastructure artifact: {path.name}" for path in sorted(actual - expected)),
306 ]
307 issues.extend(
308 f"transient Graphviz sidecar: {path.name}" for path in sorted(output_dir.glob("*.dot"))
309 )
310 return issues
313def check_diagram_contract(
314 project_root: Path = ROOT,
315 *,
316 code: bool = True,
317 infra: bool = True,
318 api: bool = True,
319) -> list[str]:
320 """Return structural catalogue violations without modifying the checkout.
322 The API spec sheets are a deterministic rendering of the committed OpenAPI
323 documents, so their contract is a re-render compared byte for byte
324 (``diagrams.api_specs.generate.api_contract_issues``) rather than a
325 stamp check.
326 """
327 issues: list[str] = []
328 if code:
329 issues.extend(_code_artifact_contract(project_root))
330 if infra:
331 issues.extend(_infra_artifact_contract(project_root))
332 if api:
333 issues.extend(api_contract_issues(project_root))
334 return issues
337def main() -> None:
338 parser = argparse.ArgumentParser(description=__doc__)
339 selection = parser.add_mutually_exclusive_group()
340 selection.add_argument("--code-only", action="store_true")
341 selection.add_argument("--infra-only", action="store_true")
342 selection.add_argument("--api-only", action="store_true")
343 parser.add_argument(
344 "--check",
345 action="store_true",
346 help="Verify artifact/index/marker structure without rendering",
347 )
348 args = parser.parse_args()
349 code = not (args.infra_only or args.api_only)
350 infra = not (args.code_only or args.api_only)
351 api = not (args.code_only or args.infra_only)
353 if args.check:
354 issues = check_diagram_contract(code=code, infra=infra, api=api)
355 if issues:
356 for issue in issues:
357 print(f"ERROR: {issue}", file=sys.stderr)
358 raise SystemExit(1)
359 print("Diagram artifact contract is current")
360 return
362 if code and "SOURCE_DATE_EPOCH" not in os.environ:
363 parser.error("canonical code generation requires integer SOURCE_DATE_EPOCH")
364 if code and "GCO_DIAGRAM_SOURCE_COMMIT" not in os.environ:
365 parser.error("canonical code generation requires 40-character GCO_DIAGRAM_SOURCE_COMMIT")
366 if code:
367 subprocess.run(
368 [sys.executable, "diagrams/code_diagrams/generate.py", "--require-png"],
369 cwd=ROOT,
370 check=True,
371 )
372 if infra:
373 subprocess.run(
374 [sys.executable, "diagrams/infra_diagrams/generate.py", "--stack", "all"],
375 cwd=ROOT,
376 check=True,
377 )
378 if api:
379 subprocess.run(
380 [sys.executable, "diagrams/api_specs/generate.py"],
381 cwd=ROOT,
382 check=True,
383 )
385 issues = check_diagram_contract(code=code, infra=infra, api=api)
386 if issues:
387 raise RuntimeError(
388 "diagram generation completed with structural drift: " + "; ".join(issues)
389 )
392if __name__ == "__main__":
393 main()