feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -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(),
}