feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
测试报告生成器模块
|
||||
|
||||
提供测试报告生成功能,支持多种报告格式。
|
||||
"""
|
||||
|
||||
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(),
|
||||
}
|
||||
Reference in New Issue
Block a user