|
4 | 4 | from __future__ import annotations |
5 | 5 |
|
6 | 6 | import argparse |
| 7 | +import json |
| 8 | +from html import escape |
7 | 9 | from pathlib import Path |
8 | 10 |
|
9 | 11 | from run_regression import corner_cases, dot_product_int8 |
10 | 12 |
|
11 | | - |
12 | 13 | ROOT = Path(__file__).resolve().parents[1] |
13 | 14 | OUTPUT = ROOT / "assets" / "rtl-regression.svg" |
14 | 15 | SATURATION_OUTPUT = ROOT / "assets" / "rtl-saturation-evidence.svg" |
| 16 | +CI_RECEIPT = ROOT / "evidence" / "rtl-ci-run-30224621114.json" |
| 17 | +CI_OUTPUT = ROOT / "assets" / "rtl-ci-transcript.svg" |
15 | 18 | SCENARIOS = ((1, 40), (8, 200), (17, 100)) |
16 | 19 |
|
17 | 20 |
|
@@ -220,13 +223,83 @@ def map_y(value: int) -> float: |
220 | 223 | ''' |
221 | 224 |
|
222 | 225 |
|
| 226 | +def render_ci_transcript() -> str: |
| 227 | + receipt = json.loads(CI_RECEIPT.read_text(encoding="utf-8")) |
| 228 | + if receipt.get("schema_version") != "edge-ai-rtl-lab.ci-receipt.v1": |
| 229 | + raise ValueError("unsupported RTL CI receipt schema") |
| 230 | + if receipt.get("conclusion") != "success": |
| 231 | + raise ValueError("RTL CI receipt is not a successful run") |
| 232 | + regressions = receipt.get("regressions") |
| 233 | + synthesis = receipt.get("synthesis") |
| 234 | + if not isinstance(regressions, list) or len(regressions) != 3 or not isinstance(synthesis, dict): |
| 235 | + raise ValueError("RTL CI receipt is missing expected simulator or synthesis records") |
| 236 | + |
| 237 | + expected_commands = [ |
| 238 | + f"python tools/run_regression.py --sim iverilog --vec-len {vec_len} --random-cases {random_cases}" |
| 239 | + for vec_len, random_cases in SCENARIOS |
| 240 | + ] |
| 241 | + transcript: list[tuple[str, str]] = [] |
| 242 | + for row, expected_command in zip(regressions, expected_commands, strict=True): |
| 243 | + if not isinstance(row, dict) or row.get("command") != expected_command: |
| 244 | + raise ValueError("RTL CI regression command does not match the checked-in matrix") |
| 245 | + summary = str(row.get("summary", "")) |
| 246 | + result = str(row.get("result", "")) |
| 247 | + if not summary.startswith("Simulator: iverilog; vectors:") or not result.startswith("PASS:"): |
| 248 | + raise ValueError("RTL CI regression record is malformed") |
| 249 | + transcript.extend((("command", f"$ {expected_command}"), ("output", summary), ("pass", result))) |
| 250 | + |
| 251 | + if synthesis.get("result") != "Found and reported 0 problems.": |
| 252 | + raise ValueError("RTL CI synthesis receipt did not pass its structural check") |
| 253 | + transcript.extend( |
| 254 | + ( |
| 255 | + ("command", "$ yosys ... hierarchy -check; proc; opt; check -assert; stat"), |
| 256 | + ("output", str(synthesis.get("tool"))), |
| 257 | + ("pass", str(synthesis.get("result"))), |
| 258 | + ( |
| 259 | + "muted", |
| 260 | + f"generic RTL cells: {int(synthesis.get('generic_cell_count', -1))} (structural count, not area)", |
| 261 | + ), |
| 262 | + ) |
| 263 | + ) |
| 264 | + |
| 265 | + text_lines = [] |
| 266 | + y = 115 |
| 267 | + colours = {"command": "#d8dee9", "output": "#9fb1c3", "pass": "#7ee2a8", "muted": "#77889a"} |
| 268 | + for kind, value in transcript: |
| 269 | + text_lines.append( |
| 270 | + f'<text x="54" y="{y}" fill="{colours[kind]}" class="terminal">{escape(value)}</text>' |
| 271 | + ) |
| 272 | + y += 36 if kind == "pass" else 29 |
| 273 | + |
| 274 | + run_id = int(receipt["run_id"]) |
| 275 | + commit = escape(str(receipt["commit"])[:7]) |
| 276 | + return f'''<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="650" viewBox="0 0 1280 650" role="img" aria-labelledby="ci-title ci-desc"> |
| 277 | + <title id="ci-title">Successful Edge AI RTL Lab simulator and synthesis transcript</title> |
| 278 | + <desc id="ci-desc">A terminal-style rendering of GitHub Actions run {run_id}: Icarus Verilog passed all deterministic vectors at vector lengths 1, 8, and 17, and Yosys reported no structural problems.</desc> |
| 279 | + <style> |
| 280 | + .chrome {{ font: 600 15px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }} |
| 281 | + .terminal {{ font: 15px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }} |
| 282 | + </style> |
| 283 | + <rect width="1280" height="650" rx="12" fill="#10151c"/> |
| 284 | + <rect width="1280" height="58" rx="12" fill="#202833"/> |
| 285 | + <path d="M0 46H1280V58H0Z" fill="#202833"/> |
| 286 | + <circle cx="25" cy="29" r="7" fill="#ff6b63"/><circle cx="49" cy="29" r="7" fill="#f4bd4f"/><circle cx="73" cy="29" r="7" fill="#62c86f"/> |
| 287 | + <text x="104" y="35" fill="#aebdcc" class="chrome">github/actions · rtl-ci · run {run_id} · {commit}</text> |
| 288 | + {''.join(text_lines)} |
| 289 | + <path d="M32 602H1248" stroke="#2a3440"/> |
| 290 | + <text x="54" y="630" fill="#77889a" class="chrome">captured from the successful public CI log · functional simulation + structural synthesis only</text> |
| 291 | +</svg> |
| 292 | +''' |
| 293 | + |
| 294 | + |
223 | 295 | def main() -> None: |
224 | 296 | parser = argparse.ArgumentParser(description=__doc__) |
225 | 297 | parser.add_argument("--check", action="store_true", help="fail if the committed SVG is stale") |
226 | 298 | args = parser.parse_args() |
227 | 299 | expected_outputs = { |
228 | 300 | OUTPUT: render(), |
229 | 301 | SATURATION_OUTPUT: render_saturation(), |
| 302 | + CI_OUTPUT: render_ci_transcript(), |
230 | 303 | } |
231 | 304 |
|
232 | 305 | if args.check: |
|
0 commit comments