#!/usr/bin/env python3 """ Credits to Arthur Vasseur for this script. https://github.com/ArthurVasseur/Vkd/blob/main/scripts/cts_report.py """ import sys import re import os import math import xml.etree.ElementTree as ET import pandas as pd from datetime import datetime from collections import Counter PAGE_SIZE = 100 def parse_raw_log(log_text: str): """Extract XML blocks from a raw CTS log file.""" pattern = re.compile( r']*>.*?', re.DOTALL ) matches = pattern.findall(log_text) return matches def parse_xml_file(path: str): """Extract nodes from a pure XML file.""" tree = ET.parse(path) root = tree.getroot() return [ ET.tostring(elem, encoding="unicode") for elem in root.findall(".//TestCaseResult") ] def process_testcases(xml_blocks): """Convert XML test blocks into structured rows.""" rows = [] for block in xml_blocks: elem = ET.fromstring(block) case = elem.attrib.get("CasePath", "unknown") duration = elem.findtext("Number", default="0") result = elem.find("Result").attrib.get("StatusCode", "UNKNOWN") message = elem.findtext("Text", default="") rows.append({ "Test Case": case, "Duration (µs)": int(duration), "Status": result, "Message": message, "RawMessage": message, }) return rows def format_message_html(message: str) -> str: """Format test message for HTML display with proper handling of newlines and tabs.""" if not message: return "" import html import json import textwrap try: formatted = bytes(message, 'utf-8').decode('unicode_escape') except Exception: formatted = message for old, new in [('\\n', '\n'), ('\\t', '\t'), ('\\r', '\r')]: formatted = formatted.replace(old, new) formatted = textwrap.dedent(formatted).strip() try: if formatted.strip().startswith('{') or formatted.strip().startswith('['): parsed = json.loads(formatted) formatted = json.dumps(parsed, indent=2) escaped = html.escape(formatted) return f'
View JSON
{escaped}
' except (json.JSONDecodeError, ValueError): pass if '\n' in formatted or '\t' in formatted or len(message) > 100: escaped = html.escape(formatted) return f'
View details
{escaped}
' else: return html.escape(formatted) def status_to_html(status: str) -> str: cls = { "Pass": "status-Pass", "Fail": "status-Fail", "NotSupported": "status-NotSupported", }.get(status, "") return f'{status}' def calculate_statistics(df): """Calculate test statistics from the dataframe.""" status_counts = Counter(df['Status']) total_tests = len(df) total_duration = df['Duration (µs)'].sum() avg_duration = df['Duration (µs)'].mean() if total_tests > 0 else 0 pass_count = status_counts.get('Pass', 0) fail_count = status_counts.get('Fail', 0) not_supported_count = status_counts.get('NotSupported', 0) other_count = total_tests - (pass_count + fail_count + not_supported_count) pass_rate = (pass_count / total_tests * 100) if total_tests > 0 else 0 return { 'total': total_tests, 'pass': pass_count, 'fail': fail_count, 'not_supported': not_supported_count, 'other': other_count, 'pass_rate': pass_rate, 'total_duration_us': total_duration, 'total_duration_ms': total_duration / 1000, 'total_duration_s': total_duration / 1_000_000, 'avg_duration_us': avg_duration, } def generate_pie_chart_svg(stats): """Generate a simple SVG pie chart for test results.""" total = stats['total'] if total == 0: return "" pass_pct = stats['pass'] / total fail_pct = stats['fail'] / total not_supported_pct = stats['not_supported'] / total other_pct = stats['other'] / total segments = [] cumulative = 0 colors = { 'pass': '#22c55e', 'fail': '#f97373', 'not_supported': '#eab308', 'other': '#64748b' } for name, pct, color in [ ('Pass', pass_pct, colors['pass']), ('Fail', fail_pct, colors['fail']), ('Not Supported', not_supported_pct, colors['not_supported']), ('Other', other_pct, colors['other']) ]: if pct > 0: segments.append({ 'name': name, 'percentage': pct * 100, 'start': cumulative, 'end': cumulative + pct, 'color': color }) cumulative += pct svg_paths = [] radius = 80 cx, cy = 100, 100 for seg in segments: start_angle = seg['start'] * 2 * 3.14159 end_angle = seg['end'] * 2 * 3.14159 x1 = cx + radius * cos_approx(start_angle) y1 = cy + radius * sin_approx(start_angle) x2 = cx + radius * cos_approx(end_angle) y2 = cy + radius * sin_approx(end_angle) large_arc = 1 if (end_angle - start_angle) > 3.14159 else 0 path = f'M {cx} {cy} L {x1} {y1} A {radius} {radius} 0 {large_arc} 1 {x2} {y2} Z' svg_paths.append(f'') return f''' {chr(10).join(svg_paths)} ''' def cos_approx(angle): import math return math.cos(angle) def sin_approx(angle): import math return math.sin(angle) def build_pagination(page_num: int, num_pages: int, base_output: str) -> str: """ base_output: basename used for files, e.g. 'report' -> report_page_1.html """ window = 2 # how many pages before/after the current one to show links = [] # First / Prev if page_num > 1: links.append(f'First') links.append( f'Prev' ) else: links.append('First') links.append('Prev') # Page range around current start_page = max(1, page_num - window) end_page = min(num_pages, page_num + window) # Ellipsis before if start_page > 1: links.append('') for p in range(start_page, end_page + 1): if p == page_num: links.append(f'{p}') else: links.append( f'{p}' ) # Ellipsis after if end_page < num_pages: links.append('') # Next / Last if page_num < num_pages: links.append( f'Next' ) links.append( f'Last' ) else: links.append('Next') links.append('Last') return f""" """ def main(): if len(sys.argv) != 3: print("Usage: cts_report.py ") sys.exit(1) input_path = sys.argv[1] output_path = sys.argv[2] if not os.path.exists(input_path): print(f"Error: input file not found: {input_path}") sys.exit(1) # Detect input format with open(input_path, "r", encoding="utf-8", errors="ignore") as f: content = f.read() if " 1: duration_str = f"{stats['total_duration_s']:.2f}s" else: duration_str = f"{stats['total_duration_ms']:.2f}ms" base_output = os.path.splitext(output_path)[0] # e.g. "report" for page_index in range(num_pages): start = page_index * PAGE_SIZE end = min(start + PAGE_SIZE, total_tests) df_page = df.iloc[start:end] # recreate placeholders & table for this page formatted_messages_page = formatted_messages[start:end] df_page["Message"] = [f"__MSG_PLACEHOLDER_{i}__" for i in range(start, end)] table_html = df_page.to_html( index=False, escape=False, justify="center", border=0, classes="cts-table", table_id="results-table" ) # Replace placeholders for this chunk for i in range(start, end): table_html = table_html.replace( f"__MSG_PLACEHOLDER_{i}__", formatted_messages[i] ) # Page numbering (1-based for humans) page_num = page_index + 1 page_title_suffix = f" – Page {page_num}/{num_pages}" # Simple HTML navigation (pure HTML, no JS) pagination_nav = build_pagination(page_num, num_pages, base_output) page_html = f""" Vulkan CTS Report{page_title_suffix}

Vulkan CTS Report

Summary of test cases, status and timings

{generation_time}
Total: {stats['total']} tests
Passed
{stats['pass']}
{stats['pass_rate']:.1f}% success rate
Failed
{stats['fail']}
{(stats['fail'] / stats['total'] * 100) if stats['total'] > 0 else 0:.1f}% of total
Not Supported
{stats['not_supported']}
{(stats['not_supported'] / stats['total'] * 100) if stats['total'] > 0 else 0:.1f}% of total
Duration
{duration_str}
Avg: {stats['avg_duration_us']:.0f} µs/test
{pie_chart_svg}
{pagination_nav}
{table_html}
""" page_output_path = f"cts_report/{base_output}_page_{page_num}.html" os.makedirs(os.path.dirname(page_output_path), exist_ok=True) with open(page_output_path, "w", encoding="utf-8") as f: f.write(page_html) print(f"[OK] HTML report saved to: {output_path}") print(f"\n--- Test Statistics ---") print(f"Total tests: {stats['total']}") print(f"Passed: {stats['pass']} ({stats['pass_rate']:.1f}%)") print(f"Failed: {stats['fail']} ({(stats['fail'] / stats['total'] * 100) if stats['total'] > 0 else 0:.1f}%)") print(f"Not Supported: {stats['not_supported']} ({(stats['not_supported'] / stats['total'] * 100) if stats['total'] > 0 else 0:.1f}%)") if stats['other'] > 0: print(f"Other: {stats['other']} ({(stats['other'] / stats['total'] * 100) if stats['total'] > 0 else 0:.1f}%)") print(f"Total Duration: {duration_str}") print(f"Average Duration: {stats['avg_duration_us']:.0f} µs/test") if __name__ == "__main__": main()