Download app/render.py from hf-internal-testing/integration-failure-triage: direct link, hf CLI and curl.
- Browser
- Download file 15.8 kB
-
https://huggingface.co/spaces/hf-internal-testing/integration-failure-triage/resolve/main/app/render.py
- Command line
-
hf download hf://spaces/hf-internal-testing/integration-failure-triage/app/render.py
-
curl -L -o render.py https://huggingface.co/spaces/hf-internal-testing/integration-failure-triage/resolve/main/app/render.py
15.8 kB
| """Render the triage report to a single static HTML page. | |
| We hand-roll markdown → HTML rather than depending on a `markdown` library | |
| to keep the Docker image lean and the output deterministic. CSS is inlined. | |
| """ | |
| from __future__ import annotations | |
| import datetime | |
| import html | |
| from collections import Counter, defaultdict | |
| HEADER = """<!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>transformers integration-test failure triage</title> | |
| <style> | |
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, | |
| Arial, sans-serif; max-width: 1200px; margin: 24px auto; color: #1f2328; | |
| padding: 0 16px; } | |
| h1 { border-bottom: 1px solid #d0d7de; padding-bottom: 8px; } | |
| h2 { margin-top: 32px; border-bottom: 1px solid #eaeef2; padding-bottom: 4px; } | |
| h3 { margin-top: 24px; } | |
| table { border-collapse: collapse; width: 100%; font-size: 13px; margin: 8px 0 20px; } | |
| th, td { border-bottom: 1px solid #eaeef2; padding: 6px 10px; text-align: left; | |
| vertical-align: top; } | |
| th { background: #f6f8fa; font-weight: 600; } | |
| tr:hover td { background: #f6f8fa; } | |
| code, pre { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; | |
| background: #f6f8fa; padding: 1px 4px; border-radius: 4px; font-size: 12px; } | |
| pre { padding: 8px 12px; overflow-x: auto; } | |
| .pill { display: inline-block; padding: 1px 8px; border-radius: 10px; | |
| font-size: 12px; font-weight: 500; } | |
| .pill-OOM { background: #fff8c5; color: #6e5500; } | |
| .pill-output_mismatch { background: #ddf4ff; color: #054270; } | |
| .pill-cuda_runtime { background: #ffdce0; color: #67050a; } | |
| .pill-load_error { background: #fbecd0; color: #6e3a00; } | |
| .pill-import_or_config { background: #f3e5f5; color: #4a148c; } | |
| .pill-other { background: #eaeef2; color: #57606a; } | |
| .meta { color: #57606a; font-size: 13px; } | |
| details { margin: 4px 0; } | |
| summary { cursor: pointer; } | |
| .num { text-align: right; font-variant-numeric: tabular-nums; } | |
| </style> | |
| </head> | |
| <body> | |
| """ | |
| FOOTER = """ | |
| </body> | |
| </html> | |
| """ | |
| def _pill(tag: str) -> str: | |
| return f'<span class="pill pill-{tag}">{html.escape(tag)}</span>' | |
| def _trace_excerpt(trace: str, max_chars: int = 220) -> str: | |
| """Last non-empty line of trace, trimmed.""" | |
| if not trace: | |
| return "" | |
| for line in reversed(trace.splitlines()): | |
| line = line.strip() | |
| if line: | |
| return (line[: max_chars - 1] + "…") if len(line) > max_chars else line | |
| return "" | |
| def _table(headers: list[str], rows: list[list[str]]) -> str: | |
| if not rows: | |
| return "<p class='meta'><em>(none)</em></p>" | |
| head = "".join(f"<th>{html.escape(h)}</th>" for h in headers) | |
| body = "".join( | |
| "<tr>" + "".join(f"<td>{cell}</td>" for cell in r) + "</tr>" for r in rows | |
| ) | |
| return f"<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>" | |
| def _model_breakdown(failures: list[dict]) -> list[list[str]]: | |
| """Group failures by model, return rows sorted by total desc.""" | |
| by_model: dict[str, list[dict]] = defaultdict(list) | |
| for f in failures: | |
| by_model[f["model"]].append(f) | |
| rows = [] | |
| for model in sorted(by_model, key=lambda m: -len(by_model[m])): | |
| items = by_model[model] | |
| mode_counts = Counter(f["failure_mode"] for f in items) | |
| mode_str = " ".join(f"{_pill(m)} {n}" for m, n in mode_counts.most_common()) | |
| gpu_str = "/".join(sorted({f["gpu"] for f in items})) | |
| rows.append([ | |
| html.escape(model), | |
| f'<span class="num">{len(items)}</span>', | |
| html.escape(gpu_str), | |
| mode_str, | |
| ]) | |
| return rows | |
| def _cluster_block(cluster: dict, idx: int) -> str: | |
| bc = cluster["bad_commit"] | |
| pr = cluster.get("pr_number") | |
| author = cluster.get("author") or "?" | |
| merged_by = cluster.get("merged_by") or "?" | |
| rows = [] | |
| for f in sorted(cluster["failures"], key=lambda f: (f["model"], f["gpu"], f["test"])): | |
| rows.append([ | |
| html.escape(f["model"]), | |
| html.escape(f["gpu"]), | |
| f'<code>{html.escape(f["test"].split("::")[-1])}</code>', | |
| _pill(f["failure_mode"]), | |
| f'<span class="num">{f["days_seen"]}/7</span>', | |
| f'<code>{html.escape(_trace_excerpt(f.get("latest_trace") or f.get("trace") or ""))}</code>', | |
| ]) | |
| pr_link = ( | |
| f'<a href="https://github.com/huggingface/transformers/pull/{pr}">PR #{pr}</a>' | |
| if pr else "<em>no PR</em>" | |
| ) | |
| commit_link = ( | |
| f'<a href="https://github.com/huggingface/transformers/commit/{bc}">' | |
| f'<code>{html.escape(bc[:12])}</code></a>' | |
| ) | |
| excerpt = cluster.get("failure_excerpt") or "" | |
| excerpt_html = ( | |
| f"<details><summary>CI trace from bad commit</summary><pre>{html.escape(excerpt[:2000])}</pre></details>" | |
| if excerpt else "" | |
| ) | |
| return ( | |
| f"<h3>Cluster {idx} · {commit_link} · {pr_link} · " | |
| f"{html.escape(author)} / merged by {html.escape(merged_by)} · " | |
| f"{len(cluster['failures'])} failures</h3>" | |
| + _table( | |
| ["model", "gpu", "test", "mode", "days", "trace excerpt"], | |
| rows, | |
| ) | |
| + excerpt_html | |
| ) | |
| def _examples_for_unpinned(unpinned: list[dict], k_per_mode: int = 5) -> str: | |
| by_mode: dict[str, list[dict]] = defaultdict(list) | |
| for f in unpinned: | |
| by_mode[f["failure_mode"]].append(f) | |
| sections = [] | |
| for mode in ("output_mismatch", "OOM", "load_error", "cuda_runtime", "import_or_config", "other"): | |
| items = by_mode.get(mode, []) | |
| if not items: | |
| continue | |
| # Sort: most-recent first by latest_seen, then model. | |
| items = sorted(items, key=lambda f: (f.get("latest_seen") or "", f["model"]), reverse=True) | |
| sample = items[:k_per_mode] | |
| rows = [] | |
| for f in sample: | |
| rows.append([ | |
| html.escape(f["model"]), | |
| html.escape(f["gpu"]), | |
| f'<code>{html.escape(f["test"].split("::")[-1])}</code>', | |
| f'<span class="num">{f["days_seen"]}/7</span>', | |
| f'<code>{html.escape(_trace_excerpt(f.get("latest_trace") or f.get("trace") or ""))}</code>', | |
| ]) | |
| sections.append( | |
| f"<h3>{_pill(mode)} {len(items)} unpinned failures — sample of {len(sample)}</h3>" | |
| + _table(["model", "gpu", "test", "days", "trace excerpt"], rows) | |
| ) | |
| return "".join(sections) | |
| def _regression_day_block(report: dict) -> str: | |
| """The killer table: how many tests regressed on each historical day, | |
| sorted by size descending. Big buckets = candidate fleet-regressions.""" | |
| buckets = report.get("regression_day_buckets") or {} | |
| if not buckets: | |
| return "" | |
| out = ["<h2>Regression-day clustering (historical first-failure)</h2>"] | |
| out.append( | |
| '<p class="meta">For every persistent failure we walked the daily CI ' | |
| "dataset backwards to find the first day it appeared as failing. The " | |
| "table below groups failures by that day — large buckets are likely " | |
| "<strong>fleet regressions</strong> from a single landed PR. Click a " | |
| "date to see the commits merged in the 24h window before it.</p>" | |
| ) | |
| rows = [] | |
| total = sum(buckets.values()) | |
| for day, n in list(buckets.items())[:25]: | |
| if day == "unknown": | |
| link = day | |
| else: | |
| since = day # before that day's CI run | |
| link = ( | |
| f'<a href="https://github.com/huggingface/transformers/commits/main?' | |
| f'until={day}T23:59:59Z">{day}</a>' | |
| ) | |
| pct = f"{(n / total) * 100:0.1f}%" if total else "—" | |
| rows.append([link, f'<span class="num">{n}</span>', f'<span class="num">{pct}</span>']) | |
| out.append(_table(["first-failure day", "failures", "share"], rows)) | |
| return "".join(out) | |
| def _regression_day_detail_blocks(report: dict, top_k: int = 5) -> str: | |
| """For each of the top-k regression days, show which tests broke (table). | |
| This gives the reader the "what got hit when" inline.""" | |
| buckets = report.get("regression_day_buckets") or {} | |
| if not buckets: | |
| return "" | |
| # All failures, with first_failure_day attached | |
| all_failures = ( | |
| [f for c in report["clusters"].values() for f in c["failures"]] | |
| + report["flaky"] | |
| + report["unpinned"] | |
| ) | |
| by_day: dict[str, list[dict]] = {} | |
| for f in all_failures: | |
| by_day.setdefault(f.get("first_failure_day") or "unknown", []).append(f) | |
| out = ["<h2>Top regression days — failure breakdown</h2>"] | |
| for day, _ in list(buckets.items())[:top_k]: | |
| items = by_day.get(day, []) | |
| if not items: | |
| continue | |
| mode_counts = Counter(f["failure_mode"] for f in items) | |
| models = Counter(f["model"] for f in items) | |
| out.append(f"<h3>{html.escape(day)} — {len(items)} failures</h3>") | |
| out.append('<p class="meta">Failure-mode mix: ' | |
| + " ".join(f"{_pill(m)} {n}" for m, n in mode_counts.most_common()) | |
| + f' · {len(models)} distinct models touched. ' | |
| f'<a href="https://github.com/huggingface/transformers/commits/main?' | |
| f'until={day}T23:59:59Z">commit log around {day}</a></p>') | |
| # Top 12 affected models | |
| rows = [] | |
| for model, n in models.most_common(12): | |
| sample = next(f for f in items if f["model"] == model) | |
| rows.append([ | |
| html.escape(model), | |
| f'<span class="num">{n}</span>', | |
| _pill(sample["failure_mode"]), | |
| f'<code>{html.escape(_trace_excerpt(sample.get("latest_trace") or sample.get("trace") or "", 180))}</code>', | |
| ]) | |
| if len(models) > 12: | |
| rows.append([ | |
| f"<em>… and {len(models) - 12} more models</em>", "", "", "" | |
| ]) | |
| out.append(_table(["model", "failures", "sample mode", "sample trace excerpt"], rows)) | |
| # Full failure list (collapsed). Sort by (failure_mode, model, gpu, test) | |
| # so visually similar failures cluster. | |
| all_rows = [] | |
| for f in sorted( | |
| items, | |
| key=lambda f: ( | |
| f.get("failure_mode") or "", | |
| f.get("model") or "", | |
| f.get("gpu") or "", | |
| f.get("test") or "", | |
| ), | |
| ): | |
| all_rows.append([ | |
| html.escape(f["model"]), | |
| html.escape(f["gpu"]), | |
| f'<code>{html.escape(f["test"].split("::")[-1])}</code>', | |
| _pill(f["failure_mode"]), | |
| f'<span class="num">{f["days_seen"]}/7</span>', | |
| f'<code>{html.escape(_trace_excerpt(f.get("latest_trace") or f.get("trace") or ""))}</code>', | |
| ]) | |
| out.append( | |
| f"<details><summary>Show all {len(items)} failures in this bucket</summary>" | |
| + _table( | |
| ["model", "gpu", "test", "failure_mode", "days_seen", "trace excerpt"], | |
| all_rows, | |
| ) | |
| + "</details>" | |
| ) | |
| return "".join(out) | |
| def render(report: dict, *, generated_at: datetime.datetime, dates_window: list[str]) -> str: | |
| t = report["totals"] | |
| out = [HEADER] | |
| out.append(f"<h1>transformers · integration-test failure triage</h1>") | |
| out.append( | |
| f'<p class="meta">Generated <code>{generated_at.isoformat(timespec="seconds")}Z</code> · ' | |
| f"window <code>{dates_window[0]}</code> → <code>{dates_window[-1]}</code> " | |
| f"({len(dates_window)} daily runs, ≥5/7 intersection)</p>" | |
| ) | |
| # TL;DR | |
| out.append("<h2>TL;DR</h2><ul>") | |
| out.append(f"<li><strong>{t['total']}</strong> persistent integration-test failures (≥5/7 days)</li>") | |
| out.append( | |
| f"<li><strong>{t['in_clusters']}</strong> attributed to <strong>{t['clusters']}</strong> " | |
| f"distinct bad commits (CI bisect)</li>" | |
| ) | |
| out.append(f"<li><strong>{t['flaky']}</strong> tagged flaky by CI</li>") | |
| out.append( | |
| f"<li><strong>{t['unpinned']}</strong> unpinned — CI bisect didn't converge " | |
| "(usually because the regression predates CI's 7-day bisect window)</li>" | |
| ) | |
| buckets = report.get("regression_day_buckets") or {} | |
| if buckets: | |
| top_day, top_n = next(iter(buckets.items())) | |
| out.append( | |
| f"<li>Historical sweep finds <strong>{top_n}</strong> failures " | |
| f"({(top_n / t['total']) * 100:0.0f}% of the total) first appeared on " | |
| f"<code>{html.escape(top_day)}</code> — likely a single fleet-regression PR.</li>" | |
| ) | |
| out.append("</ul>") | |
| out.append(_regression_day_block(report)) | |
| out.append(_regression_day_detail_blocks(report, top_k=5)) | |
| # Failure-mode breakdown for unpinned | |
| if report["unpinned"]: | |
| modes = Counter(f["failure_mode"] for f in report["unpinned"]) | |
| out.append("<h2>Unpinned failure modes</h2>") | |
| out.append(_table( | |
| ["mode", "count"], | |
| [[_pill(m), f'<span class="num">{n}</span>'] for m, n in modes.most_common()], | |
| )) | |
| # Per-model | |
| out.append("<h2>Per-model breakdown (all failures)</h2>") | |
| all_failures = ( | |
| [f for c in report["clusters"].values() for f in c["failures"]] | |
| + report["flaky"] | |
| + report["unpinned"] | |
| ) | |
| out.append(_table( | |
| ["model", "failures", "gpu", "mode mix"], | |
| _model_breakdown(all_failures), | |
| )) | |
| # Clusters | |
| out.append("<h2>Pinned clusters (CI bisect)</h2>") | |
| if report["clusters"]: | |
| for i, (_bc, c) in enumerate(report["clusters"].items(), start=1): | |
| out.append(_cluster_block(c, i)) | |
| else: | |
| out.append("<p class='meta'><em>(none)</em></p>") | |
| # Flaky | |
| out.append("<h2>Flaky (CI flagged)</h2>") | |
| if report["flaky"]: | |
| rows = [] | |
| for f in report["flaky"]: | |
| rows.append([ | |
| html.escape(f["model"]), | |
| html.escape(f["gpu"]), | |
| f'<code>{html.escape(f["test"].split("::")[-1])}</code>', | |
| _pill(f["failure_mode"]), | |
| f'<span class="num">{f["days_seen"]}/7</span>', | |
| ]) | |
| out.append(_table(["model", "gpu", "test", "mode", "days"], rows)) | |
| else: | |
| out.append("<p class='meta'><em>(none)</em></p>") | |
| # Unpinned examples | |
| if report["unpinned"]: | |
| out.append("<h2>Unpinned — samples per mode</h2>") | |
| out.append( | |
| "<p class='meta'>These failures persisted across the window but CI couldn't" | |
| " attribute a bad commit. They likely regressed before the 7-day bisect window." | |
| " Showing the most-recently-seen samples per failure mode.</p>" | |
| ) | |
| out.append(_examples_for_unpinned(report["unpinned"])) | |
| out.append(FOOTER) | |
| return "".join(out) | |
| if __name__ == "__main__": | |
| import sys | |
| from classify import classify | |
| from cluster import cluster_failures | |
| from fetch import fetch_last_n | |
| from filter import per_day_integration_failures, intersect_across_days | |
| cache = sys.argv[1] if len(sys.argv) > 1 else None | |
| out_path = sys.argv[2] if len(sys.argv) > 2 else "/tmp/integration_failures.html" | |
| daily = fetch_last_n(7, cache_dir=cache) | |
| per_day = per_day_integration_failures(daily) | |
| kept = intersect_across_days(per_day, min_days=5) | |
| latest = max(daily.keys()) | |
| nf = daily[latest].get("new_failures") | |
| report = cluster_failures(kept, nf, classify) | |
| html_out = render( | |
| report, | |
| generated_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), | |
| dates_window=sorted(daily.keys()), | |
| ) | |
| with open(out_path, "w") as f: | |
| f.write(html_out) | |
| print(f"wrote {out_path} ({len(html_out)} bytes)") | |