Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e3903a3
ci: use UC REST API for schema create/drop in Reyden nightly
eric-wang-1990 Jun 2, 2026
3f2b485
ci: skip schema provisioning for Reyden nightly
eric-wang-1990 Jun 2, 2026
6a9501f
ci: use regular warehouse for setup, Reyden only for test execution
eric-wang-1990 Jun 2, 2026
ce48179
ci: make e2e-tests callable, reyden nightly as thin wrapper
eric-wang-1990 Jun 2, 2026
df65d35
ci: fix secret inheritance in Reyden nightly workflow_call
eric-wang-1990 Jun 2, 2026
98c6703
ci: add e2e_only flag to skip unit tests in nightly run
eric-wang-1990 Jun 2, 2026
167e356
test(csharp): skip/adapt E2E tests for Reyden read-only warehouse
eric-wang-1990 Jun 2, 2026
82a9085
fix(csharp): suppress xUnit1024 on new-hiding test overrides
eric-wang-1990 Jun 2, 2026
ef90009
ci: publish E2E nightly results to a GitHub Pages dashboard
eric-wang-1990 Jun 2, 2026
a039bae
ci: classify Reyden nightly E2E failures by root cause (#512)
eric-wang-1990 Jun 5, 2026
aee6441
feat(ci): split Reyden gap signatures + per-error dashboard detail (#…
eric-wang-1990 Jun 5, 2026
a73f24e
Merge branch 'main' into ci/reyden-rest-nightly
eric-wang-1990 Jun 5, 2026
37243c5
Revert E2E test changes on this branch
eric-wang-1990 Jun 5, 2026
2ec64ff
Un-wrap Reyden nightly; restore generic e2e-tests workflow
eric-wang-1990 Jun 5, 2026
3733b47
Merge branch 'main' into ci/reyden-rest-nightly
eric-wang-1990 Jun 5, 2026
6437f0f
ci(dashboard): make truncated per-error test list expandable
eric-wang-1990 Jun 6, 2026
8622bf2
ci(dashboard): bucket known Reyden backend gaps out of "Real issue"
eric-wang-1990 Jun 6, 2026
bfc0d28
ci(dashboard): scope "Real issue" to the 4 tracked Reyden SEA bugs
eric-wang-1990 Jun 6, 2026
a1d47b6
ci(dashboard): reference SC-233423 for the rows_affected=-1 limitation
eric-wang-1990 Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 334 additions & 0 deletions .github/e2e-dashboard/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
<!DOCTYPE html>
<!--
Copyright (c) 2025 ADBC Drivers Contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E2E Nightly Dashboard — Databricks ADBC</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
:root { --bg:#f5f6f8; --card:#fff; --pass:#2e9e5b; --fail:#d83b3b; --skip:#c9a227; --ink:#222; --muted:#6b7280; --line:#e3e6ea; }
* { box-sizing: border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
margin:0; background:var(--bg); color:var(--ink); }
.wrap { max-width:1200px; margin:0 auto; padding:32px 20px 80px; }
header h1 { margin:0 0 4px; font-size:26px; }
header .sub { color:var(--muted); font-size:14px; }
header .sub a { color:#2563eb; text-decoration:none; }
.cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:14px; margin:24px 0; }
.card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:16px 18px; }
.card .label { font-size:12px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); }
.card .value { font-size:28px; font-weight:700; margin-top:4px; }
.card.pass .value { color:var(--pass); } .card.fail .value { color:var(--fail); } .card.skip .value { color:var(--skip); }
.panel { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:20px; margin:18px 0; }
.panel h2 { margin:0 0 14px; font-size:18px; }
table { width:100%; border-collapse:collapse; font-size:14px; }
th, td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--line); vertical-align:top; }
th { font-size:12px; text-transform:uppercase; letter-spacing:.03em; color:var(--muted); }
td.num, th.num { text-align:right; font-variant-numeric:tabular-nums; }
.bar { height:8px; border-radius:4px; background:var(--line); overflow:hidden; display:flex; min-width:90px; }
.bar > span { display:block; height:100%; }
.sig { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:13px; }
.pill { display:inline-block; font-size:12px; font-weight:600; padding:2px 8px; border-radius:99px; }
.pill.green { background:#e6f4ec; color:var(--pass); } .pill.red { background:#fbe7e7; color:var(--fail); }
tr.run { cursor:pointer; } tr.run:hover { background:#fafbfc; }
tr.run .caret { color:var(--muted); }
.detail td { background:#fafbfc; }
.fail-list { margin:0; padding:0; list-style:none; }
.fail-list li { padding:6px 0; border-bottom:1px dashed var(--line); }
.fail-list .tname { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12.5px; word-break:break-word; }
.fail-list .msg { color:var(--muted); font-size:12px; margin-top:2px; white-space:pre-wrap; word-break:break-word; }
.muted { color:var(--muted); } .small { font-size:12px; }
.toggle-tests { color:#2563eb; text-decoration:none; cursor:pointer; }
.toggle-tests:hover { text-decoration:underline; }
.group-h { margin:12px 0 4px; font-size:13px; font-weight:700; }
canvas { max-height:340px; }
.catbadge { display:inline-block; font-size:12px; font-weight:600; padding:2px 8px; border-radius:6px; }
.cat-gap { background:#e8eefc; color:#2949b8; }
.cat-env { background:#fdf3dc; color:#9a7400; }
.cat-real { background:#fbe7e7; color:#d83b3b; }
.legend { list-style:none; margin:10px 0 4px; padding:0; font-size:12px; color:var(--muted); }
.legend li { margin:5px 0; line-height:1.5; }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>E2E Nightly Test Dashboard</h1>
<div class="sub" id="subtitle">Loading…</div>
</header>

<div class="cards" id="cards"></div>

<div class="panel">
<h2>Trend (per nightly run)</h2>
<canvas id="trendChart"></canvas>
</div>

<div class="panel" id="analysisPanel">
<h2>Latest run — failure analysis</h2>
<div id="analysis"></div>
</div>

<div class="panel">
<h2>Run history <span class="muted small">(click a row to expand failures)</span></h2>
<table>
<thead><tr>
<th></th><th>Date (UTC)</th><th>Protocol</th><th class="num">Pass rate</th>
<th class="num">Passed</th><th class="num">Failed</th><th class="num">Skipped</th>
<th>Result</th><th></th>
</tr></thead>
<tbody id="historyBody"></tbody>
</table>
</div>

<footer class="muted small">
Data stored in <code>e2e-nightly/data/</code> on the <code>gh-pages</code> branch · append-only history.
</footer>
</div>

<script>
const DATA = "data/runs.json";
const fmtDate = ts => { if (!ts) return "—"; const d = new Date(ts); return isNaN(d) ? ts : d.toISOString().slice(0,16).replace("T"," "); };
const esc = s => (s||"").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));

// Root-cause categories emitted by parse-trx-to-json.py. A low raw pass-rate is
// dominated by expected Reyden capability gaps; the "Real issue" bucket is the
// small set of confirmed, tracked Reyden SEA bugs. Everything else on this
// Reyden-only nightly is a known limitation.
const CATEGORIES = {
"Reyden capability gap (expected)": { cls:"cat-gap",
blurb:"A known Reyden limitation — unsupported DDL/statement/type (CREATE TABLE/SCHEMA, SHOW COLUMNS, INTERVAL, …), no Thrift endpoint, no CloudFetch, no hive_metastore, rows_affected=-1, write-not-persisted value mismatches. Expected: gate/skip; not a driver bug." },
"Environment / infra": { cls:"cat-env",
blurb:"Missing/misconfigured warehouse, read-only rejection, auth, timeout, or transport error. Not a driver bug — fix the environment or retry." },
"Real issue / to investigate": { cls:"cat-real",
blurb:"Confirmed Reyden SEA backend bugs, each reproduced (Reyden vs DBSQL / raw SEA API) and filed under Epic SC-222102: SHOW SCHEMAS/TABLES IN ALL CATALOGS (SC-233357), bare SET (SC-233354), col_0 aliases (SC-233356), ANSI heterogeneous cast (SC-233355). Any unclassified failure also surfaces here." },
};
const catBadge = name => `<span class="catbadge ${(CATEGORIES[name]||{}).cls||""}">${esc(name)}</span>`;

// Plain-language "what it is" per failure signature (keys must match
// parse-trx-to-json.py's signature_for output).
const SIGNATURE_DESCRIPTIONS = {
"Thrift endpoint unavailable on SEA/Reyden warehouse": "REST-only warehouse has no Thrift endpoint",
"CloudFetch not supported on Reyden (download failed)": "cloud result download not supported",
"Unsupported statement: SHOW COLUMNS": "metadata path not implemented",
"Unsupported CREATE type: SCHEMA": "cannot create schema",
"Unsupported feature: CREATE OR REPLACE TABLE": "cannot create / replace table",
"Unsupported type: INTERVAL": "cannot store / read interval",
"Reyden rows_affected not returned (known backend bug)": "Reyden returns -1 for INSERT/UPDATE affected rows — known backend bug, tracked SC-233423",
"Reyden does not support hive_metastore catalog": "hive_metastore catalog unavailable on Reyden — defaults to main",
"Reyden unsupported feature (other)": "other unsupported DDL / statement / type",
"Reyden unsupported feature (DDL / statement / type)": "unsupported DDL / statement / type",
"Reyden SEA rejects SHOW SCHEMAS/TABLES IN ALL CATALOGS": "SEA API parser rejects IN ALL CATALOGS (accepted in SQL editor) — tracked SC-233357",
"Reyden SEA rejects bare SET statement": "SEA API rejects a bare SET — tracked SC-233354",
"Reyden SEA omits column aliases (returns col_0)": "SEA result schema returns col_0 instead of the column alias — tracked SC-233356",
"Type cast mismatch on round-trip": "ANSI strict-cast rejects heterogeneous ARRAY/MAP — tracked SC-233355",
"Warehouse not found (ENDPOINT_NOT_FOUND / HTTP 404)": "warehouse missing or misconfigured",
"Read-only warehouse rejected write/DDL": "warehouse is read-only",
"Permission denied (403)": "auth / permission issue",
"Timeout": "operation timed out",
"Connection / transport error": "could not connect to the server",
"SQL syntax error": "malformed statement (unclassified) — surfaced for review",
"Object not found": "metadata resolved a missing object",
"Assertion failed (value mismatch)": "known Reyden value mismatch (e.g. write not persisted) — expected limitation",
"DML/DDL rejected": "write / DDL not supported on Reyden — expected limitation",
};
const sigDesc = s => SIGNATURE_DESCRIPTIONS[s] || "";

// Expand/collapse the truncated test list in the per-error table.
function toggleTests(a) {
const more = a.parentNode.querySelector(".more-tests");
if (!more) return false;
const collapsed = more.hasAttribute("hidden");
more.toggleAttribute("hidden");
a.textContent = collapsed ? "show fewer" : `+${a.dataset.n} more`;
return false;
}

// Class.Method, dropping the namespace prefix and the parameter list.
const shortTest = name => {
const base = (name || "").split("(")[0];
return base.split(".").slice(-2).join(".");
};

// Per-error table for the latest run: Category | Error | Count | What it is | Tests.
function sigDetailTable(failures) {
if (!failures || !failures.length) return '<span class="muted">No failure detail recorded.</span>';
const groups = {};
for (const f of failures) {
const cat = f.category || "Uncategorized", sig = f.signature || "Unknown";
const key = cat + "" + sig;
(groups[key] ??= { cat, sig, items: [] }).items.push(f);
}
const rows = Object.values(groups)
.sort((a, b) => a.cat < b.cat ? -1 : a.cat > b.cat ? 1 : b.items.length - a.items.length)
.map(({ cat, sig, items }) => {
const tests = [...new Set(items.map(f => shortTest(f.name)))].sort();
const head = tests.slice(0, 8).map(esc).join("<br>");
const rest = tests.slice(8);
const shown = head + (rest.length
? `<span class="more-tests" hidden><br>${rest.map(esc).join("<br>")}</span>` +
`<br><a href="#" class="toggle-tests" data-n="${rest.length}" onclick="return toggleTests(this)">+${rest.length} more</a>`
: "");
return `<tr>
<td>${catBadge(cat)}</td>
<td class="sig">${esc(sig)}</td>
<td class="num">${items.length}</td>
<td>${esc(sigDesc(sig))}</td>
<td class="small">${shown}</td></tr>`;
}).join("");
return `<table><thead><tr>
<th>Category</th><th>Error</th><th class="num">Count</th><th>What it is</th><th>Tests</th>
</tr></thead><tbody>${rows}</tbody></table>`;
}

function bar(p, f, s) {
const t = p+f+s || 1;
return `<div class="bar" title="${p} passed / ${f} failed / ${s} skipped">
<span style="width:${100*p/t}%;background:var(--pass)"></span>
<span style="width:${100*f/t}%;background:var(--fail)"></span>
<span style="width:${100*s/t}%;background:var(--skip)"></span></div>`;
}

function renderCards(latest) {
const c = document.getElementById("cards");
if (!latest) { c.innerHTML = '<div class="card"><div class="label">No runs yet</div></div>'; return; }
c.innerHTML = `
<div class="card"><div class="label">Pass rate</div><div class="value">${latest.pass_rate ?? 0}%</div></div>
<div class="card"><div class="label">Total</div><div class="value">${latest.total ?? 0}</div></div>
<div class="card pass"><div class="label">Passed</div><div class="value">${latest.passed ?? 0}</div></div>
<div class="card fail"><div class="label">Failed</div><div class="value">${latest.failed ?? 0}</div></div>
<div class="card skip"><div class="label">Skipped</div><div class="value">${latest.skipped ?? 0}</div></div>`;
}

function renderTrend(runs) {
const labels = runs.map(r => fmtDate(r.timestamp));
const ctx = document.getElementById("trendChart");
new Chart(ctx, {
data: {
labels,
datasets: [
{ type:"bar", label:"Passed", data:runs.map(r=>r.passed), backgroundColor:"#2e9e5b", stack:"s" },
{ type:"bar", label:"Failed", data:runs.map(r=>r.failed), backgroundColor:"#d83b3b", stack:"s" },
{ type:"bar", label:"Skipped", data:runs.map(r=>r.skipped), backgroundColor:"#c9a227", stack:"s" },
{ type:"line", label:"Pass rate %", data:runs.map(r=>r.pass_rate), yAxisID:"y1",
borderColor:"#2563eb", backgroundColor:"#2563eb", tension:.25, pointRadius:3 },
]
},
options: {
responsive:true, interaction:{mode:"index",intersect:false},
scales:{ x:{stacked:true}, y:{stacked:true, title:{display:true,text:"tests"}},
y1:{position:"right", min:0, max:100, grid:{drawOnChartArea:false}, title:{display:true,text:"pass %"}} }
}
});
}

async function renderAnalysis(latest) {
const el = document.getElementById("analysis");
if (!latest || !latest.failed) { el.innerHTML = '<p class="muted">No failures in the latest run. 🎉</p>'; return; }
const catEntries = Object.entries(latest.by_category||{});
const catRows = catEntries.map(([k,v]) =>
`<tr><td>${catBadge(k)}</td><td class="num">${v}</td></tr>`).join("");
const legend = catEntries.length
? `<ul class="legend">` + catEntries.map(([k]) =>
`<li>${catBadge(k)} ${esc((CATEGORIES[k]||{}).blurb||"")}</li>`).join("") + `</ul>`
: "";
const catSection = catEntries.length ? `
<div class="group-h">By root-cause category</div>
<table><thead><tr><th>Category</th><th class="num">Count</th></tr></thead><tbody>${catRows}</tbody></table>
${legend}` : "";

el.innerHTML = `
<p class="muted small">Latest run <a href="${esc(latest.html_url)}">#${esc(latest.run_number||latest.run_id)}</a>
(${esc(latest.protocol)}${latest.read_only?", read-only":""}) — ${latest.failed} failures.</p>
${catSection}
<div class="group-h">By error (root cause)</div>
<div id="sigTable" class="muted small">Loading per-error detail…</div>`;

// The per-error table needs the failure list (test names) from the run detail file.
try {
const full = await (await fetch("data/" + latest.detail + "?_=" + Date.now())).json();
document.getElementById("sigTable").innerHTML = sigDetailTable(full.failures || []);
} catch (e) {
document.getElementById("sigTable").innerHTML =
`<span class="muted">Could not load per-error detail: ${esc(String(e))}</span>`;
}
}

async function toggleDetail(tr, run) {
const next = tr.nextElementSibling;
if (next && next.classList.contains("detail")) { next.remove(); tr.querySelector(".caret").textContent="▸"; return; }
tr.querySelector(".caret").textContent="▾";
const det = document.createElement("tr");
det.className = "detail";
det.innerHTML = `<td colspan="9" class="muted small">Loading failures…</td>`;
tr.after(det);
try {
const r = await fetch("data/" + run.detail);
const full = await r.json();
if (!full.failures || !full.failures.length) { det.firstChild.innerHTML = '<span class="muted">No failure detail recorded.</span>'; return; }
let html = "", curCat = null, curSig = null;
for (const f of full.failures) {
if (f.category && f.category !== curCat) { curCat = f.category; curSig = null; html += `<div class="group-h">${catBadge(f.category)}</div>`; }
if (f.signature !== curSig) { curSig = f.signature; html += `<div class="group-h sig">${esc(curSig)}</div>`; }
html += `<ul class="fail-list"><li><div class="tname">${esc(f.name)}</div>` +
(f.message ? `<div class="msg">${esc(f.message)}</div>` : "") + `</li></ul>`;
}
det.firstChild.innerHTML = html;
} catch (e) {
det.firstChild.innerHTML = `<span class="muted">Could not load detail: ${esc(String(e))}</span>`;
}
}

function renderHistory(runs) {
const body = document.getElementById("historyBody");
body.innerHTML = "";
[...runs].reverse().forEach(run => {
const tr = document.createElement("tr");
tr.className = "run";
const ok = (run.failed||0) === 0;
tr.innerHTML = `
<td class="caret">▸</td>
<td>${fmtDate(run.timestamp)}</td>
<td>${esc(run.protocol)}${run.read_only?' <span class="muted small">(ro)</span>':""}</td>
<td class="num">${run.pass_rate ?? 0}%</td>
<td class="num">${run.passed ?? 0}</td>
<td class="num">${run.failed ?? 0}</td>
<td class="num">${run.skipped ?? 0}</td>
<td>${bar(run.passed||0, run.failed||0, run.skipped||0)}</td>
<td><a href="${esc(run.html_url)}" onclick="event.stopPropagation()" target="_blank">run ↗</a>
<span class="pill ${ok?'green':'red'}">${ok?'pass':'fail'}</span></td>`;
tr.addEventListener("click", () => toggleDetail(tr, run));
body.appendChild(tr);
});
}

(async function () {
let runs = [];
try { runs = await (await fetch(DATA + "?_=" + Date.now())).json(); } catch (e) {}
document.getElementById("subtitle").innerHTML =
runs.length ? `${runs.length} nightly run(s) recorded · last updated ${fmtDate(runs[runs.length-1].timestamp)} UTC`
: "No runs recorded yet — the dashboard populates after the first nightly run.";
const latest = runs[runs.length-1];
renderCards(latest);
if (runs.length) renderTrend(runs);
renderAnalysis(latest);
renderHistory(runs);
})();
</script>
</body>
</html>
Loading
Loading