@@ -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
234236def 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
261264def 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