Files
everything-is-suitable/everything-is-suitable-test/python_e2e/core/reporter.py
T
张翔 08ea5fbe98 feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
2026-03-28 14:37:29 +08:00

303 lines
10 KiB
Python

"""
测试报告生成器模块
提供测试报告生成功能,支持多种报告格式。
"""
import json
import os
from pathlib import Path
from typing import Dict, List, Any, Optional
from datetime import datetime
from dataclasses import dataclass, field, asdict
@dataclass
class TestResult:
"""测试结果数据类"""
name: str
status: str # passed, failed, skipped
duration: float = 0.0
start_time: Optional[str] = None
end_time: Optional[str] = None
error_message: Optional[str] = None
traceback: Optional[str] = None
screenshot: Optional[str] = None
steps: List[Dict[str, Any]] = field(default_factory=list)
@dataclass
class TestSuite:
"""测试套件数据类"""
name: str
tests: List[TestResult] = field(default_factory=list)
@property
def passed_count(self) -> int:
return sum(1 for t in self.tests if t.status == "passed")
@property
def failed_count(self) -> int:
return sum(1 for t in self.tests if t.status == "failed")
@property
def skipped_count(self) -> int:
return sum(1 for t in self.tests if t.status == "skipped")
@property
def total_count(self) -> int:
return len(self.tests)
@property
def pass_rate(self) -> float:
if self.total_count == 0:
return 0.0
return (self.passed_count / self.total_count) * 100
class TestReporter:
"""测试报告生成器"""
def __init__(self, report_dir: str = "reports"):
self.report_dir = Path(report_dir)
self.report_dir.mkdir(parents=True, exist_ok=True)
self.suites: Dict[str, TestSuite] = {}
self.start_time: Optional[datetime] = None
self.end_time: Optional[datetime] = None
def start_report(self) -> None:
"""开始测试报告"""
self.start_time = datetime.now()
print(f"📝 测试报告开始于: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
def end_report(self) -> None:
"""结束测试报告"""
self.end_time = datetime.now()
print(f"📝 测试报告结束于: {self.end_time.strftime('%Y-%m-%d %H:%M:%S')}")
def add_test_result(self, suite_name: str, result: TestResult) -> None:
"""添加测试结果"""
if suite_name not in self.suites:
self.suites[suite_name] = TestSuite(name=suite_name)
self.suites[suite_name].tests.append(result)
def generate_html_report(self, filename: str = "test_report.html") -> str:
"""生成HTML报告"""
filepath = self.report_dir / filename
total_tests = sum(s.total_count for s in self.suites.values())
total_passed = sum(s.passed_count for s in self.suites.values())
total_failed = sum(s.failed_count for s in self.suites.values())
total_skipped = sum(s.skipped_count for s in self.suites.values())
html_content = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E2E测试报告</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
}}
.container {{ max-width: 1200px; margin: 0 auto; }}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}}
.header h1 {{ font-size: 28px; margin-bottom: 10px; }}
.summary {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}}
.summary-card {{
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.summary-card h3 {{ font-size: 14px; color: #666; margin-bottom: 8px; }}
.summary-card .value {{ font-size: 32px; font-weight: bold; }}
.summary-card.passed .value {{ color: #10b981; }}
.summary-card.failed .value {{ color: #ef4444; }}
.summary-card.skipped .value {{ color: #f59e0b; }}
.summary-card.total .value {{ color: #3b82f6; }}
.suite {{
background: white;
border-radius: 8px;
margin-bottom: 20px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.suite-header {{
background: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #e5e7eb;
}}
.suite-header h2 {{ font-size: 18px; color: #374151; }}
.test-list {{ padding: 0; }}
.test-item {{
padding: 15px 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}}
.test-item:last-child {{ border-bottom: none; }}
.test-item.passed {{ border-left: 4px solid #10b981; }}
.test-item.failed {{ border-left: 4px solid #ef4444; }}
.test-item.skipped {{ border-left: 4px solid #f59e0b; }}
.test-name {{ font-weight: 500; color: #374151; }}
.test-status {{
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}}
.test-status.passed {{ background: #d1fae5; color: #065f46; }}
.test-status.failed {{ background: #fee2e2; color: #991b1b; }}
.test-status.skipped {{ background: #fef3c7; color: #92400e; }}
.test-duration {{ color: #6b7280; font-size: 12px; margin-left: 10px; }}
.error-message {{
background: #fee2e2;
color: #991b1b;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
font-size: 12px;
}}
.footer {{
text-align: center;
padding: 20px;
color: #6b7280;
font-size: 12px;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 E2E测试报告</h1>
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
<div class="summary">
<div class="summary-card total">
<h3>总测试数</h3>
<div class="value">{total_tests}</div>
</div>
<div class="summary-card passed">
<h3>通过</h3>
<div class="value">{total_passed}</div>
</div>
<div class="summary-card failed">
<h3>失败</h3>
<div class="value">{total_failed}</div>
</div>
<div class="summary-card skipped">
<h3>跳过</h3>
<div class="value">{total_skipped}</div>
</div>
</div>
"""
# 添加测试套件详情
for suite_name, suite in self.suites.items():
html_content += f"""
<div class="suite">
<div class="suite-header">
<h2>{suite_name}</h2>
<p>通过率: {suite.pass_rate:.1f}% ({suite.passed_count}/{suite.total_count})</p>
</div>
<div class="test-list">
"""
for test in suite.tests:
status_class = test.status
error_html = ""
if test.error_message:
error_html = f'<div class="error-message">{test.error_message}</div>'
html_content += f"""
<div class="test-item {status_class}">
<div>
<span class="test-name">{test.name}</span>
<span class="test-duration">{test.duration:.2f}s</span>
{error_html}
</div>
<span class="test-status {status_class}">{test.status.upper()}</span>
</div>
"""
html_content += """
</div>
</div>
"""
html_content += """
<div class="footer">
<p>Generated by Python E2E Test Framework</p>
</div>
</div>
</body>
</html>
"""
with open(filepath, "w", encoding="utf-8") as f:
f.write(html_content)
print(f"📊 HTML报告已生成: {filepath}")
return str(filepath)
def generate_json_report(self, filename: str = "test_report.json") -> str:
"""生成JSON报告"""
filepath = self.report_dir / filename
report_data = {
"report_info": {
"generated_at": datetime.now().isoformat(),
"start_time": self.start_time.isoformat() if self.start_time else None,
"end_time": self.end_time.isoformat() if self.end_time else None,
},
"summary": {
"total": sum(s.total_count for s in self.suites.values()),
"passed": sum(s.passed_count for s in self.suites.values()),
"failed": sum(s.failed_count for s in self.suites.values()),
"skipped": sum(s.skipped_count for s in self.suites.values()),
},
"suites": {}
}
for suite_name, suite in self.suites.items():
report_data["suites"][suite_name] = {
"summary": {
"total": suite.total_count,
"passed": suite.passed_count,
"failed": suite.failed_count,
"skipped": suite.skipped_count,
"pass_rate": suite.pass_rate,
},
"tests": [asdict(test) for test in suite.tests]
}
with open(filepath, "w", encoding="utf-8") as f:
json.dump(report_data, f, ensure_ascii=False, indent=2)
print(f"📊 JSON报告已生成: {filepath}")
return str(filepath)
def generate_all_reports(self) -> Dict[str, str]:
"""生成所有报告"""
return {
"html": self.generate_html_report(),
"json": self.generate_json_report(),
}