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

1#!/usr/bin/env python3 

2"""Regenerate or structurally verify every committed diagram catalogue.""" 

3 

4from __future__ import annotations 

5 

6import argparse 

7import os 

8import re 

9import subprocess 

10import sys 

11from pathlib import Path 

12 

13ROOT = Path(__file__).resolve().parent.parent 

14sys.path.insert(0, str(ROOT)) 

15 

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 

28 

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] 

51 

52 

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 

71 

72 

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}"] 

86 

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 

102 

103 

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") 

112 

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 ] 

133 

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)) 

147 

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}") 

228 

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}") 

232 

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 ) 

280 

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 

297 

298 

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 

311 

312 

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. 

321 

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 

335 

336 

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) 

352 

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 

361 

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 ) 

384 

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 ) 

390 

391 

392if __name__ == "__main__": 

393 main()