import os
import re
from datetime import datetime
from pathlib import Path
from collections import defaultdict
def extract_test_status(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
status_match = re.search(r'Status: ([🟢🔴🟡])', content)
if status_match:
return status_match.group(1)
match = re.search(r'# Program ([🟢🔴🟡])', content)
if match:
return match.group(1)
lines = content.split('\n')[:10]
for line in lines:
if '🟢' in line:
return '🟢'
elif '🔴' in line:
return '🔴'
elif '🟡' in line:
return '🟡'
raise ValueError(f"No status found in {file_path}")
except Exception as e:
raise RuntimeError(f"Error reading {file_path}: {e}") from e
def generate_html_template():
return '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Summary</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
max-width: 900px;
margin: 40px auto;
padding: 20px;
line-height: 1.6;
background-color: #0d1117;
color: #f0f6fc;
}
h1 {
color: #58a6ff;
border-bottom: 1px solid #30363d;
padding-bottom: 10px;
font-weight: 700;
font-size: 2em;
display: flex;
justify-content: space-between;
align-items: center;
}
.main-title {
flex: 1;
}
.main-summary {
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
color: #8b949e;
font-size: 0.6em;
font-weight: normal;
}
.timestamp {
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
color: #6e7681;
font-size: 0.85em;
font-weight: normal;
margin-top: 10px;
text-align: center;
}
h2 {
color: #f0f6fc;
margin-top: 30px;
}
details {
margin: 10px 0;
border: 1px solid #30363d;
border-radius: 6px;
padding: 10px;
background-color: #161b22;
}
summary {
font-weight: 600;
cursor: pointer;
padding: 5px;
color: #58a6ff;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
summary::before {
content: 'â–¶';
margin-right: 8px;
transition: transform 0.2s ease;
}
details[open] summary::before {
transform: rotate(90deg);
}
summary:hover {
background-color: #21262d;
}
.section-title {
flex: 1;
}
.section-summary {
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.9em;
color: #8b949e;
text-align: right;
}
ul {
margin: 10px 0;
padding-left: 20px;
}
li {
margin: 5px 0;
}
a {
color: #58a6ff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.stats {
background-color: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 15px;
margin-top: 20px;
}
.pass-rate {
font-size: 1.1em;
margin-top: 10px;
}
</style>
</head>
<body>'''
def generate_test_summary():
project_root = Path(__file__).parent.parent
tests_dir = project_root / "tests" / "integration"
if not tests_dir.exists():
print(f"Tests directory not found: {tests_dir}")
return
test_files = list(tests_dir.glob("**/*.md"))
categories = defaultdict(list)
for file_path in test_files:
relative_path = file_path.relative_to(tests_dir)
if relative_path.parent.name == "integration":
category = "root"
else:
category = relative_path.parent.name
test_name = relative_path.stem
status = extract_test_status(file_path)
link_path = str(relative_path)
categories[category].append((test_name, status, link_path))
sorted_categories = sorted(categories.items())
for category, tests in sorted_categories:
tests.sort(key=lambda x: x[0])
total_tests = sum(len(tests) for _, tests in sorted_categories)
passing_tests = sum(1 for _, tests in sorted_categories for _, status, _ in tests if status == '🟢')
failing_tests = sum(1 for _, tests in sorted_categories for _, status, _ in tests if status == '🔴')
no_assert_tests = sum(1 for _, tests in sorted_categories for _, status, _ in tests if status == '🟡')
header_summary_parts = []
if passing_tests > 0:
header_summary_parts.append(f"{passing_tests} 🟢")
if failing_tests > 0:
header_summary_parts.append(f"{failing_tests} 🔴")
if no_assert_tests > 0:
header_summary_parts.append(f"{no_assert_tests} 🟡")
header_summary = " ".join(header_summary_parts)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
html_content = generate_html_template()
html_content += f'''
<h1>
<span class="main-title">Test Summary</span>
<span class="main-summary">{header_summary}</span>
</h1>
<div class="timestamp">{timestamp}</div>
'''
for category, tests in sorted_categories:
if category == "root":
section_title = "Root"
else:
section_title = category.replace('_', ' ').title()
category_passing = sum(1 for _, status, _ in tests if status == '🟢')
category_failing = sum(1 for _, status, _ in tests if status == '🔴')
category_no_assert = sum(1 for _, status, _ in tests if status == '🟡')
summary_parts = []
if category_passing > 0:
summary_parts.append(f"{category_passing} 🟢")
if category_failing > 0:
summary_parts.append(f"{category_failing} 🔴")
if category_no_assert > 0:
summary_parts.append(f"{category_no_assert} 🟡")
summary_text = " ".join(summary_parts)
html_content += f''' <details>
<summary>
<span class="section-title">{section_title}</span>
<span class="section-summary">{summary_text}</span>
</summary>
<ul>
'''
for test_name, status, link_path in tests:
absolute_path = project_root / "tests" / "integration" / link_path
vscode_url = f"vscode://file/{absolute_path}"
html_content += f' <li><a href="{vscode_url}">{test_name}</a>: {status}</li>\n'
html_content += ''' </ul>
</details>
'''
html_content += '''</body>
</html>'''
summary_path = project_root / "test-summary.html"
with open(summary_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"Generated test summary: {summary_path}")
print(f"Total: {total_tests}, Passing: {passing_tests}, Failing: {failing_tests}")
if __name__ == "__main__":
generate_test_summary()