Skip to content

Commit c9c408d

Browse files
committed
Remove duplication in TCK report
1 parent 256ecf1 commit c9c408d

2 files changed

Lines changed: 137 additions & 66 deletions

File tree

coverage/scripts/render_tck_job_summary.py

Lines changed: 133 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
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+
"""
28
from __future__ import annotations
39

410
import os
@@ -8,14 +14,16 @@
814
from pathlib import Path
915

1016
FULL_REPORT_PATH = Path("coverage/target/tck-scripts-report.md")
11-
MAX_SUMMARY_CHARS = 120000
1217
TCK_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
]
1722
SUREFIRE_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

2028
def 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("&lt;", "<")
44+
.replace("&#60;", "<")
45+
.replace("&gt;", ">")
46+
.replace("&#62;", ">")
47+
.replace("&amp;", "&")
48+
.replace("&#38;", "&")
49+
.replace("&#10;", "\n")
50+
.replace("&#13;", "\r")
51+
.replace("&#9;", "\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

63135
def 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

coverage/src/test/java/fr/insee/vtl/coverage/TCKTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ void tckCase(TckCase c) throws Exception {
5252
}
5353

5454
private static void logCaseOutcome(TckCase c, boolean success) {
55+
// Avoid duplicating hundreds of lines in CI logs; use generated markdown artifact instead.
56+
if (Boolean.parseBoolean(System.getenv().getOrDefault("GITHUB_ACTIONS", "false"))) {
57+
return;
58+
}
5559
System.out.println((success ? "✅" : "❌") + " Test " + c.index());
5660
System.out.println("\tVTL script: " + c.scriptSummary());
5761
System.out.println("\t" + c.displayPath());

0 commit comments

Comments
 (0)