11#!/usr/bin/env python3
2+ """
3+ Build a single markdown report: for each TCK case in JUnit execution order, path + script (from zip)
4+ then pass/fail line(s) from Surefire XML.
5+
6+ Zip entry paths use '/'; display paths in tests use ' » ' (see TckPaths.SEGMENT_SEP).
7+ """
28from __future__ import annotations
39
410import os
814from pathlib import Path
915
1016FULL_REPORT_PATH = Path ("coverage/target/tck-scripts-report.md" )
11- MAX_SUMMARY_CHARS = 120000
1217TCK_ZIP_CANDIDATES = [
1318 Path ("coverage/src/main/resources/v2.1.zip" ),
1419 Path ("vtl/tck/v2.1.zip" ),
1520 Path ("coverage/target/classes/v2.1.zip" ),
1621]
1722SUREFIRE_XML_PATH = Path ("coverage/target/surefire-reports/TEST-fr.insee.vtl.coverage.TCKTest.xml" )
1823
24+ # Matches Java TckPaths.SEGMENT_SEP = " \u00bb "
25+ _DISPLAY_SEP = " \u00bb "
26+
1927
2028def resolve_tck_zip () -> Path | None :
2129 for path in TCK_ZIP_CANDIDATES :
@@ -24,40 +32,104 @@ def resolve_tck_zip() -> Path | None:
2432 return None
2533
2634
27- def parse_test_index (testcase_name : str ) -> int | None :
28- m = re .search (r"\[(\d+)\]\s*$" , testcase_name )
29- if m :
30- return int (m .group (1 ))
31- m = re .search (r"\bTest\s+(\d+)\b" , testcase_name )
35+ def decode_xml_entities (text : str ) -> str :
36+ out = text
37+ for _ in range (4 ):
38+ nxt = (
39+ out .replace (""" , '"' )
40+ .replace (""" , '"' )
41+ .replace ("'" , "'" )
42+ .replace ("'" , "'" )
43+ .replace ("<" , "<" )
44+ .replace ("<" , "<" )
45+ .replace (">" , ">" )
46+ .replace (">" , ">" )
47+ .replace ("&" , "&" )
48+ .replace ("&" , "&" )
49+ .replace (" " , "\n " )
50+ .replace (" " , "\r " )
51+ .replace ("	" , "\t " )
52+ )
53+ if nxt == out :
54+ break
55+ out = nxt
56+ return out
57+
58+
59+ def split_testcase_name (name_attr : str ) -> tuple [int , str ] | None :
60+ """Extract (index, display_path) from Surefire testcase name (before or after prettify)."""
61+ s = decode_xml_entities (name_attr ).strip ()
62+ # Prettified: "Test 1\n\tConditional operators » ..."
63+ if "\n " in s :
64+ lines = [ln .strip () for ln in s .split ("\n " ) if ln .strip ()]
65+ if len (lines ) >= 2 :
66+ m0 = re .match (r"^Test\s+(\d+)\s*$" , lines [0 ])
67+ if m0 :
68+ path_line = lines [1 ].lstrip ("\t " ).strip ()
69+ return int (m0 .group (1 )), path_line
70+ # Original phrased: ... Test 1 — path
71+ m = re .search (r"Test\s+(\d+)\s+—\s+(.+)$" , s )
3272 if m :
33- return int (m .group (1 ))
73+ return int (m .group (1 )), m . group ( 2 ). strip ()
3474 return None
3575
3676
37- def read_execution_results () -> dict [int , dict [str , str ]]:
38- if not SUREFIRE_XML_PATH .exists ():
39- return {}
40- tree = ET .parse (SUREFIRE_XML_PATH )
77+ def display_path_to_zip_key (display_path : str ) -> str :
78+ return display_path .replace (_DISPLAY_SEP , "/" )
79+
80+
81+ def load_scripts_from_zip (zip_path : Path ) -> dict [str , str ]:
82+ scripts : dict [str , str ] = {}
83+ with zipfile .ZipFile (zip_path ) as zf :
84+ for name in zf .namelist ():
85+ if not name .endswith ("transformation.vtl" ):
86+ continue
87+ key = name [: - len ("transformation.vtl" )].rstrip ("/" )
88+ body = zf .read (name ).decode ("utf-8" , errors = "replace" ).replace ("\r " , "" )
89+ scripts [key ] = body
90+ return scripts
91+
92+
93+ def parse_ordered_results (xml_path : Path ) -> list [dict [str , str ]]:
94+ """Surefire testcase document order matches parameterized test order."""
95+ if not xml_path .exists ():
96+ return []
97+ tree = ET .parse (xml_path )
4198 root = tree .getroot ()
42- results : dict [ int , dict [str , str ]] = {}
99+ out : list [ dict [str , str ]] = []
43100 for tc in root .findall (".//testcase" ):
44101 name = tc .attrib .get ("name" , "" )
45- idx = parse_test_index (name )
46- if idx is None :
102+ parsed = split_testcase_name (name )
103+ if parsed is None :
47104 continue
105+ idx , display_path = parsed
48106 failure = tc .find ("failure" )
49107 error = tc .find ("error" )
50108 if failure is not None or error is not None :
51109 node = error if error is not None else failure
52- message = (node .attrib .get ("message" , "" ) if node is not None else "" ).strip ()
53- results [idx ] = {
54- "status" : "FAIL" ,
55- "label" : name ,
56- "error" : message or "Unknown failure" ,
57- }
110+ msg = (node .attrib .get ("message" , "" ) if node is not None else "" ).strip ()
111+ body = (node .text or "" ).strip () if node is not None else ""
112+ detail = msg
113+ if body and msg not in body :
114+ detail = f"{ msg } \n { body } " if msg else body
115+ out .append (
116+ {
117+ "index" : idx ,
118+ "display_path" : display_path ,
119+ "status" : "FAIL" ,
120+ "detail" : detail or "Unknown failure" ,
121+ }
122+ )
58123 else :
59- results [idx ] = {"status" : "PASS" , "label" : name , "error" : "" }
60- return results
124+ out .append (
125+ {
126+ "index" : idx ,
127+ "display_path" : display_path ,
128+ "status" : "PASS" ,
129+ "detail" : name ,
130+ }
131+ )
132+ return out
61133
62134
63135def main () -> None :
@@ -73,55 +145,50 @@ def main() -> None:
73145 + "\n " ,
74146 encoding = "utf-8" ,
75147 )
148+ if summary_path :
149+ with open (summary_path , "a" , encoding = "utf-8" ) as out :
150+ out .write (
151+ "## TCK report\n \n "
152+ "_Zip not found — could not build report._\n "
153+ )
76154 return
77155
78- cases : list [tuple [str , str ]] = []
79- with zipfile .ZipFile (zip_path ) as zf :
80- for name in sorted (zf .namelist ()):
81- if not name .endswith ("transformation.vtl" ):
82- continue
83- script = zf .read (name ).decode ("utf-8" , errors = "replace" ).replace ("\r " , "" )
84- display_path = name [: - len ("transformation.vtl" )].rstrip ("/" )
85- cases .append ((display_path , script ))
86-
87- execution = read_execution_results ()
88-
89- with open (FULL_REPORT_PATH , "w" , encoding = "utf-8" ) as full_out :
90- full_out .write ("# TCK scripts output\n \n " )
91- full_out .write (f"Source zip: `{ zip_path } `\n \n " )
92- full_out .write (f"Total cases: { len (cases )} \n \n " )
93- for i , (display_path , script ) in enumerate (cases , start = 1 ):
94- full_out .write (f"## Test { i } \n \n " )
95- full_out .write (f"{ display_path } \n \n " )
96- full_out .write ("```vtl\n " )
97- full_out .write (script if script else "(empty)" )
98- full_out .write ("\n ```\n \n " )
99- result = execution .get (i )
100- if result is None :
101- full_out .write ("Result: ⏳ Test not executed or result unavailable\n \n " )
102- elif result ["status" ] == "PASS" :
103- full_out .write (f"✅ Test { i } \n " )
104- full_out .write (f"\t { result ['label' ]} \n \n " )
105- else :
106- full_out .write (f"❌ Test { i } \n " )
107- full_out .write (f"\t { result ['label' ]} \n " )
108- full_out .write (f"\t { result ['error' ]} \n \n " )
109-
110- if not summary_path :
111- return
156+ scripts = load_scripts_from_zip (zip_path )
157+ results = parse_ordered_results (SUREFIRE_XML_PATH )
158+
159+ lines : list [str ] = []
160+ lines .append ("# TCK scripts output\n " )
161+ lines .append (f"\n _Source zip:_ `{ zip_path } ` \n " )
162+ lines .append (f"_Cases (from Surefire):_ { len (results )} \n " )
163+
164+ for row in results :
165+ i = row ["index" ]
166+ display_path = row ["display_path" ]
167+ zip_key = display_path_to_zip_key (display_path )
168+ script = scripts .get (zip_key , "(script not found in zip for this path)" )
169+ lines .append (f"\n ## Test { i } \n \n " )
170+ lines .append (f"{ display_path } \n \n " )
171+ lines .append ("```vtl\n " )
172+ lines .append (script if script else "(empty)" )
173+ lines .append ("\n ```\n \n " )
174+ if row ["status" ] == "PASS" :
175+ lines .append (f"✅ Test { i } \n " )
176+ lines .append (f"\t { display_path } \n " )
177+ else :
178+ lines .append (f"❌ Test { i } \n " )
179+ lines .append (f"\t { display_path } \n " )
180+ detail = row ["detail" ]
181+ for ln in detail .split ("\n " ):
182+ lines .append (f"\t { ln } \n " )
112183
113- full_markdown = FULL_REPORT_PATH .read_text (encoding = "utf-8" , errors = "replace" )
114- preview = full_markdown [:MAX_SUMMARY_CHARS ]
115- truncated = len (full_markdown ) > MAX_SUMMARY_CHARS
184+ FULL_REPORT_PATH .write_text ("" .join (lines ), encoding = "utf-8" )
116185
117- with open (summary_path , "a" , encoding = "utf-8" ) as out :
118- out .write ("## TCK scripts output\n \n " )
119- out .write ("Report is exported as artifact `tck-scripts-report`.\n \n " )
120- out .write (preview )
121- if truncated :
186+ if summary_path :
187+ with open (summary_path , "a" , encoding = "utf-8" ) as out :
122188 out .write (
123- "\n \n _... Output truncated in job summary due to GitHub size limits. "
124- "Download artifact `tck-scripts-report` for the full content._\n "
189+ "## TCK report\n \n "
190+ "Unified scripts + results (same order as tests): download artifact "
191+ "**`tck-scripts-report`** (`tck-scripts-report.md`).\n "
125192 )
126193
127194
0 commit comments