Skip to content

Commit ee624d4

Browse files
baibaichenclaude
andcommitted
Restructure report: record-level stats, cleaner headings, simplified labels
- Switch stats unit from test to record (expression) level - Rename labels: Passed+Offload→Passed, Passed+Fallback→Fallback, Failed+Offload→Failed - Restructure headings: # title, > [!NOTE] callout, ## ANSI Offload, ## Other - Remove Failed+Fallback category - Rename JSON/XML terminology to ANSI Offload/Other suites Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
1 parent 3fde6ba commit ee624d4

1 file changed

Lines changed: 106 additions & 95 deletions

File tree

.github/scripts/analyze-ansi.py

Lines changed: 106 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -205,64 +205,67 @@ def _infer_job_name(xml_path):
205205
# ANALYSIS LAYER
206206
# ===========================================================================
207207

208-
def classify_test(status, has_fallback, has_offload_data, last_record_fallback=None):
209-
"""Unified three-color classification.
208+
def classify_record(offload_status, record_status):
209+
"""Classify a single record (expression-level).
210210
211-
For failed tests, last_record_fallback (bool) determines Offload vs Fallback
212-
because failure always occurs at the last record.
211+
offload_status: "OFFLOAD" or "FALLBACK"
212+
record_status: "PASS" or "FAIL"
213213
"""
214+
is_pass = record_status in ("PASSED", "PASS")
215+
is_fallback = offload_status == "FALLBACK"
216+
if is_fallback:
217+
if is_pass:
218+
return "🔴", "Fallback"
219+
return "🔴", "Failed+Fallback"
220+
if is_pass:
221+
return "🟢", "Passed"
222+
return "🟡", "Failed"
223+
224+
225+
def classify_test_for_xml(status):
226+
"""Classify XML tests (no offload data)."""
214227
is_pass = status in ("PASSED", "PASS")
215228
is_skip = status in ("SKIPPED", "SKIP")
216-
is_fail = status in ("FAILED", "ERROR", "FAIL")
217229
if is_skip:
218230
return "⚪", "Skipped"
219-
if not has_offload_data:
220-
if is_pass:
221-
return "⚪", "Passed (no data)"
222-
return "🟡", "Failed (no data)"
223-
if is_fail:
224-
if last_record_fallback:
225-
return "🔴", "Failed+Fallback"
226-
return "🟡", "Failed+Offload"
227-
if has_fallback:
228-
return "🔴", "Passed+Fallback"
229231
if is_pass:
230-
return "🟢", "Passed+Offload"
231-
return "🟡", "Failed"
232+
return "", "Passed (no data)"
233+
return "🟡", "Failed (no data)"
232234

233235

234236
def analyze_json_tests(suites):
235-
"""Analyze JSON data at test level. Returns summary dict + flat test list."""
236-
tests = []
237+
"""Analyze JSON data at record (expression) level. Returns flat record list."""
238+
records_out = []
237239
for suite_data in suites:
238240
suite_name = suite_data.get("suite", "")
239241
category = suite_data.get("category", "")
240242
for t in suite_data.get("tests", []):
241-
records = t.get("records", [])
242-
has_fallback = any(r.get("offload") == "FALLBACK" for r in records)
243-
has_offload_data = len(records) > 0
244-
last_record_fallback = (records[-1].get("offload") == "FALLBACK") if records else None
245-
color, label = classify_test(
246-
t.get("status", "PASSED"), has_fallback, has_offload_data,
247-
last_record_fallback)
248-
tests.append({
249-
"suite": suite_name,
250-
"test": t["name"],
251-
"status": t.get("status", "PASSED"),
252-
"color": color,
253-
"label": label,
254-
"category": category,
255-
"has_fallback": has_fallback,
256-
"records": records,
257-
})
258-
return tests
243+
test_status = t.get("status", "PASSED")
244+
for rec in t.get("records", []):
245+
offload = rec.get("offload", "")
246+
rec_status = rec.get("status", "PASS")
247+
color, label = classify_record(offload, rec_status)
248+
records_out.append({
249+
"suite": suite_name,
250+
"test": t["name"],
251+
"test_status": test_status,
252+
"status": rec_status,
253+
"color": color,
254+
"label": label,
255+
"category": category,
256+
"offload": offload,
257+
"expression": rec.get("expression", ""),
258+
"failCause": rec.get("failCause", ""),
259+
"meta": rec.get("meta", {}),
260+
})
261+
return records_out
259262

260263

261264
def analyze_xml_tests(xml_results):
262265
"""Analyze surefire XML at test method level."""
263266
tests = []
264267
for t in xml_results:
265-
color, label = classify_test(t["status"], False, False)
268+
color, label = classify_test_for_xml(t["status"])
266269
tests.append({
267270
"suite": t["suite"],
268271
"test": t["test"],
@@ -276,26 +279,22 @@ def analyze_xml_tests(xml_results):
276279
return tests
277280

278281

279-
def build_summary(json_tests, xml_tests):
280-
"""Build unified summary from both data sources."""
282+
def build_summary(json_records, xml_tests):
283+
"""Build unified summary. json_records are at record (expression) level."""
281284
by_color = defaultdict(int)
282285
failures = []
283286
total = 0
284287

285-
for t in json_tests:
288+
for r in json_records:
286289
total += 1
287-
by_color[t["label"]] += 1
288-
if t["status"] in ("FAILED", "ERROR", "FAIL"):
289-
fail_cause = ""
290-
for r in t.get("records", []):
291-
if r.get("failCause"):
292-
fail_cause = r["failCause"]
293-
break
290+
by_color[r["label"]] += 1
291+
if r["status"] in ("FAILED", "ERROR", "FAIL"):
292+
fail_cause = r.get("failCause", "")
294293
failures.append({
295-
"suite": t["suite"],
296-
"test": t["test"],
297-
"color": t["color"],
298-
"label": t["label"],
294+
"suite": r["suite"],
295+
"test": r["test"],
296+
"color": r["color"],
297+
"label": r["label"],
299298
"message": fail_cause,
300299
"source": "json",
301300
})
@@ -314,11 +313,17 @@ def build_summary(json_tests, xml_tests):
314313
"source": "xml",
315314
})
316315

316+
# Count unique tests from JSON for the header
317+
json_test_names = set()
318+
for r in json_records:
319+
json_test_names.add((r["suite"], r["test"]))
320+
317321
return {
318322
"total": total,
319323
"by_color": dict(by_color),
320324
"failures": failures,
321-
"json_test_count": len(json_tests),
325+
"json_record_count": len(json_records),
326+
"json_test_count": len(json_test_names),
322327
"xml_test_count": len(xml_tests),
323328
}
324329

@@ -434,62 +439,68 @@ def _worse(a, b):
434439
# OUTPUT LAYER
435440
# ===========================================================================
436441

437-
def format_summary(summary, json_tests, suites=None):
438-
"""Format test-level summary as markdown."""
439-
lines = ["## ANSI Mode Test Analysis Report (Spark 4.1)\n"]
440-
json_total = summary["json_test_count"]
442+
def format_summary(summary, json_records, suites=None):
443+
"""Format record-level summary as markdown."""
444+
lines = ["# ANSI Mode Test Analysis Report (Spark 4.1)\n"]
445+
lines.append("> [!NOTE]")
446+
lines.append("> Expression-level ANSI mode offload coverage analysis.")
447+
lines.append("> Test config: `spark.sql.ansi.enabled=true`,"
448+
" `spark.gluten.sql.ansiFallback.enabled=false`.")
449+
lines.append("> - **Passed (🟢)**: Velox correctly handles ANSI semantics")
450+
lines.append("> - **Fallback (🔴)**: Expression falls back to Spark execution,"
451+
" needs ANSI support in Velox")
452+
lines.append("> - **Failed (🟡)**: Velox executes but ANSI error behavior"
453+
" differs from Spark, needs exception handling fix\n")
454+
json_test_count = summary["json_test_count"]
455+
json_record_count = summary["json_record_count"]
441456
xml_total = summary["xml_test_count"]
442-
lines.append(f"**JSON tests: {json_total}** | "
443-
f"**XML suites: {xml_total} tests**\n")
457+
lines.append(f"**ANSI Offload suites: {json_test_count} tests, "
458+
f"{json_record_count} records** | "
459+
f"**Other suites: {xml_total} tests**\n")
460+
461+
# --- ANSI Offload section ---
462+
lines.append("## ANSI Offload\n")
444463

445-
# --- Overview: JSON-only three-color stats ---
446-
lines.append("### Overview (JSON Expression Tests)\n")
464+
# --- Overview: record-level stats ---
465+
lines.append("### Overview (ANSI Offload Expression Records)\n")
447466
lines.append("| Classification | Count | % |")
448467
lines.append("|---|---|---|")
449-
json_labels = ["Passed+Offload", "Failed+Offload", "Passed+Fallback",
450-
"Failed+Fallback", "Failed"]
451-
color_map = {"Passed+Offload": "🟢", "Failed+Offload": "🟡",
452-
"Passed+Fallback": "🔴", "Failed+Fallback": "🔴",
453-
"Failed": "🟡"}
468+
json_labels = ["Passed", "Failed", "Fallback"]
469+
color_map = {"Passed": "🟢", "Failed": "🟡",
470+
"Fallback": "🔴"}
454471
for label in json_labels:
455472
count = summary["by_color"].get(label, 0)
456473
if count > 0:
457474
color = color_map.get(label, "")
458-
pct = count * 100 / json_total if json_total else 0
475+
pct = count * 100 / json_record_count if json_record_count else 0
459476
lines.append(f"| {color} {label} | {count} | {pct:.1f}% |")
460477
lines.append("")
461478

462-
# --- Per-Suite Summary (JSON only) ---
479+
# --- Per-Suite Summary (record-level) ---
463480
if suites:
464481
lines.append("### Per-Suite Summary\n")
465-
lines.append("| Suite | 🟢 Passed+Offload | 🟡 Failed+Offload "
466-
"| 🔴 Passed+Fallback | 🔴 Failed+Fallback |")
467-
lines.append("|---|---|---|---|---|")
482+
lines.append("| Suite | 🟢 Passed | 🟡 Failed "
483+
"| 🔴 Fallback |")
484+
lines.append("|---|---|---|---|")
468485
suite_rows = []
469486
for s in suites:
470487
name = s.get("suite", "").split(".")[-1]
471488
cat = s.get("category", "")
472-
tests = s.get("tests", [])
473489
counts = defaultdict(int)
474-
for t in tests:
475-
records = t.get("records", [])
476-
has_fallback = any(r.get("offload") == "FALLBACK"
477-
for r in records)
478-
has_offload_data = len(records) > 0
479-
last_fb = (records[-1].get("offload") == "FALLBACK") if records else None
480-
_, label = classify_test(
481-
t.get("status", "PASSED"), has_fallback, has_offload_data,
482-
last_fb)
483-
counts[label] += 1
490+
for t in s.get("tests", []):
491+
for rec in t.get("records", []):
492+
offload = rec.get("offload", "")
493+
rec_status = rec.get("status", "PASS")
494+
_, label = classify_record(offload, rec_status)
495+
counts[label] += 1
484496
total = sum(counts.values())
485-
po = counts.get("Passed+Offload", 0)
497+
po = counts.get("Passed", 0)
486498
pct = f"{po * 100 / total:.0f}%" if total else "0%"
487499
suite_rows.append((cat, name, po, pct,
488-
counts.get("Failed+Offload", 0),
489-
counts.get("Passed+Fallback", 0),
490-
counts.get("Failed+Fallback", 0)))
491-
for cat, name, po, pct, fo, pfb, ffb in sorted(suite_rows):
492-
lines.append(f"| {name} | {po} ({pct}) | {fo} | {pfb} | {ffb} |")
500+
counts.get("Failed", 0),
501+
counts.get("Fallback", 0)))
502+
for cat, name, po, pct, fo, pfb in sorted(suite_rows):
503+
lines.append(f"| {name} | {po} ({pct}) | {fo} | {pfb} |")
493504
lines.append("")
494505

495506
# --- Failure Cause Analysis (JSON only) ---
@@ -503,7 +514,7 @@ def format_summary(summary, json_tests, suites=None):
503514
cause_counts[cause] += 1
504515

505516
lines.append(f"### Failure Cause Analysis "
506-
f"({len(json_failures)} failures from JSON)\n")
517+
f"({len(json_failures)} failures)\n")
507518
cause_desc = {
508519
"NO_EXCEPTION": "Velox did not throw expected ANSI exception",
509520
"WRONG_EXCEPTION": "Exception wrapped as SparkException",
@@ -536,8 +547,8 @@ def format_summary(summary, json_tests, suites=None):
536547
xml_suite_counts[short] += 1
537548
xml_suite_tests[short].append(f.get("test", ""))
538549
if xml_suite_counts:
539-
lines.append(f"### Failed XML Suites "
540-
f"(not in JSON, {sum(xml_suite_counts.values())} failures)\n")
550+
lines.append(f"## Other "
551+
f"({sum(xml_suite_counts.values())} failures)\n")
541552
lines.append("| Suite | Failures |")
542553
lines.append("|---|---|")
543554
for suite, cnt in sorted(xml_suite_counts.items(),
@@ -709,10 +720,10 @@ def _format_generic_table(lines, matrix, suites, key_label):
709720
lines.append("")
710721

711722

712-
def format_full(summary, json_tests, categories, suites=None,
723+
def format_full(summary, json_records, categories, suites=None,
713724
ai_content=None, ai_model=None):
714725
"""Format full report: summary + matrix in <details> + optional AI analysis."""
715-
parts = [format_summary(summary, json_tests, suites)]
726+
parts = [format_summary(summary, json_records, suites)]
716727
if ai_content:
717728
parts.append("")
718729
parts.append("<details>")
@@ -723,7 +734,7 @@ def format_full(summary, json_tests, categories, suites=None,
723734
return "\n".join(parts)
724735

725736

726-
def format_json_output(summary, json_tests, xml_tests, categories):
737+
def format_json_output(summary, json_records, xml_tests, categories):
727738
"""Format analysis results as JSON."""
728739
output = {
729740
"summary": summary,
@@ -773,7 +784,7 @@ def _build_ai_context(summary, categories):
773784
if k not in ("Passed (no data)", "Skipped")}
774785

775786
output = {
776-
"json_test_count": summary["json_test_count"],
787+
"json_record_count": summary["json_record_count"],
777788
"by_color": json_colors,
778789
"failure_count": len(summary["failures"]),
779790
"failures": compact_failures,

0 commit comments

Comments
 (0)