08ea5fbe98
添加用户管理视图、API和状态管理文件
507 lines
17 KiB
Python
507 lines
17 KiB
Python
"""
|
|
增强版报告生成器模块
|
|
支持趋势分析、历史报告对比、性能基准对比等功能
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Dict, Any, List, Optional
|
|
from dataclasses import dataclass, asdict
|
|
import statistics
|
|
|
|
from .reporter import TestResult, TestSummary, ReportGenerator
|
|
|
|
|
|
@dataclass
|
|
class TrendData:
|
|
"""趋势数据"""
|
|
date: str
|
|
total: int
|
|
passed: int
|
|
failed: int
|
|
pass_rate: float
|
|
avg_response_time: float
|
|
total_time: float
|
|
|
|
|
|
@dataclass
|
|
class PerformanceBenchmark:
|
|
"""性能基准数据"""
|
|
endpoint: str
|
|
method: str
|
|
avg_response_time: float
|
|
min_response_time: float
|
|
max_response_time: float
|
|
p95_response_time: float
|
|
p99_response_time: float
|
|
|
|
|
|
class EnhancedReportGenerator(ReportGenerator):
|
|
"""增强版报告生成器"""
|
|
|
|
def __init__(self, output_dir: str = "reports", history_dir: str = "reports/history"):
|
|
"""
|
|
初始化增强版报告生成器
|
|
|
|
Args:
|
|
output_dir: 报告输出目录
|
|
history_dir: 历史报告目录
|
|
"""
|
|
super().__init__(output_dir)
|
|
self.history_dir = Path(history_dir)
|
|
self.history_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def generate_trend_report(self, days: int = 7) -> str:
|
|
"""
|
|
生成趋势分析报告
|
|
|
|
Args:
|
|
days: 分析最近几天的数据
|
|
|
|
Returns:
|
|
报告文件路径
|
|
"""
|
|
trend_data = self._load_trend_data(days)
|
|
|
|
if not trend_data:
|
|
return ""
|
|
|
|
report_path = self.output_dir / f"trend_report_{datetime.now().strftime('%Y%m%d')}.html"
|
|
|
|
html_content = self._generate_trend_html(trend_data)
|
|
|
|
with open(report_path, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
return str(report_path)
|
|
|
|
def generate_comparison_report(self, current_results: List[TestResult],
|
|
previous_results: List[TestResult] = None) -> str:
|
|
"""
|
|
生成对比报告
|
|
|
|
Args:
|
|
current_results: 当前测试结果
|
|
previous_results: 之前的测试结果
|
|
|
|
Returns:
|
|
报告文件路径
|
|
"""
|
|
if previous_results is None:
|
|
previous_results = self._load_latest_results()
|
|
|
|
if not previous_results:
|
|
return ""
|
|
|
|
comparison_data = self._compare_results(current_results, previous_results)
|
|
|
|
report_path = self.output_dir / f"comparison_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
|
|
|
|
html_content = self._generate_comparison_html(comparison_data)
|
|
|
|
with open(report_path, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
return str(report_path)
|
|
|
|
def generate_performance_report(self, results: List[TestResult],
|
|
benchmarks: Dict[str, PerformanceBenchmark] = None) -> str:
|
|
"""
|
|
生成性能报告
|
|
|
|
Args:
|
|
results: 测试结果列表
|
|
benchmarks: 性能基准数据
|
|
|
|
Returns:
|
|
报告文件路径
|
|
"""
|
|
performance_data = self._analyze_performance(results)
|
|
|
|
if benchmarks:
|
|
performance_data = self._compare_with_benchmarks(performance_data, benchmarks)
|
|
|
|
report_path = self.output_dir / f"performance_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
|
|
|
|
html_content = self._generate_performance_html(performance_data)
|
|
|
|
with open(report_path, 'w', encoding='utf-8') as f:
|
|
f.write(html_content)
|
|
|
|
return str(report_path)
|
|
|
|
def _load_trend_data(self, days: int) -> List[TrendData]:
|
|
"""
|
|
加载趋势数据
|
|
|
|
Args:
|
|
days: 加载最近几天的数据
|
|
|
|
Returns:
|
|
趋势数据列表
|
|
"""
|
|
trend_data = []
|
|
|
|
for i in range(days):
|
|
date = datetime.now() - timedelta(days=i)
|
|
date_str = date.strftime('%Y%m%d')
|
|
|
|
report_file = self.history_dir / f"test_report_{date_str}.json"
|
|
|
|
if report_file.exists():
|
|
with open(report_file, 'r', encoding='utf-8') as f:
|
|
report_data = json.load(f)
|
|
|
|
summary = report_data.get('summary', {})
|
|
results = report_data.get('results', [])
|
|
|
|
avg_response_time = statistics.mean([r['response_time'] for r in results]) if results else 0
|
|
|
|
trend_data.append(TrendData(
|
|
date=date.strftime('%Y-%m-%d'),
|
|
total=summary.get('total', 0),
|
|
passed=summary.get('passed', 0),
|
|
failed=summary.get('failed', 0),
|
|
pass_rate=summary.get('pass_rate', 0),
|
|
avg_response_time=avg_response_time,
|
|
total_time=summary.get('total_time', 0)
|
|
))
|
|
|
|
return sorted(trend_data, key=lambda x: x.date)
|
|
|
|
def _load_latest_results(self) -> List[TestResult]:
|
|
"""
|
|
加载最新的测试结果
|
|
|
|
Returns:
|
|
测试结果列表
|
|
"""
|
|
report_files = list(self.history_dir.glob("test_report_*.json"))
|
|
|
|
if not report_files:
|
|
return []
|
|
|
|
latest_file = max(report_files, key=lambda f: f.stat().st_mtime)
|
|
|
|
with open(latest_file, 'r', encoding='utf-8') as f:
|
|
report_data = json.load(f)
|
|
|
|
return [TestResult(**r) for r in report_data.get('results', [])]
|
|
|
|
def _compare_results(self, current: List[TestResult], previous: List[TestResult]) -> Dict[str, Any]:
|
|
"""
|
|
对比测试结果
|
|
|
|
Args:
|
|
current: 当前测试结果
|
|
previous: 之前的测试结果
|
|
|
|
Returns:
|
|
对比数据
|
|
"""
|
|
current_summary = self._calculate_summary(current)
|
|
previous_summary = self._calculate_summary(previous)
|
|
|
|
return {
|
|
"current": current_summary,
|
|
"previous": previous_summary,
|
|
"changes": {
|
|
"total": current_summary["total"] - previous_summary["total"],
|
|
"passed": current_summary["passed"] - previous_summary["passed"],
|
|
"failed": current_summary["failed"] - previous_summary["failed"],
|
|
"pass_rate": current_summary["pass_rate"] - previous_summary["pass_rate"],
|
|
"avg_response_time": current_summary["avg_response_time"] - previous_summary["avg_response_time"]
|
|
}
|
|
}
|
|
|
|
def _analyze_performance(self, results: List[TestResult]) -> Dict[str, Any]:
|
|
"""
|
|
分析性能数据
|
|
|
|
Args:
|
|
results: 测试结果列表
|
|
|
|
Returns:
|
|
性能分析数据
|
|
"""
|
|
response_times = [r.response_time for r in results if r.response_time > 0]
|
|
|
|
if not response_times:
|
|
return {}
|
|
|
|
return {
|
|
"avg_response_time": statistics.mean(response_times),
|
|
"min_response_time": min(response_times),
|
|
"max_response_time": max(response_times),
|
|
"median_response_time": statistics.median(response_times),
|
|
"std_response_time": statistics.stdev(response_times) if len(response_times) > 1 else 0,
|
|
"p95_response_time": self._calculate_percentile(response_times, 95),
|
|
"p99_response_time": self._calculate_percentile(response_times, 99),
|
|
"total_tests": len(results),
|
|
"failed_tests": sum(1 for r in results if not r.success)
|
|
}
|
|
|
|
def _compare_with_benchmarks(self, performance_data: Dict[str, Any],
|
|
benchmarks: Dict[str, PerformanceBenchmark]) -> Dict[str, Any]:
|
|
"""
|
|
与性能基准对比
|
|
|
|
Args:
|
|
performance_data: 性能数据
|
|
benchmarks: 性能基准
|
|
|
|
Returns:
|
|
对比后的性能数据
|
|
"""
|
|
comparisons = []
|
|
|
|
for endpoint, benchmark in benchmarks.items():
|
|
current_avg = performance_data.get("avg_response_time", 0)
|
|
|
|
comparison = {
|
|
"endpoint": endpoint,
|
|
"method": benchmark.method,
|
|
"benchmark_avg": benchmark.avg_response_time,
|
|
"current_avg": current_avg,
|
|
"diff": current_avg - benchmark.avg_response_time,
|
|
"diff_percent": ((current_avg - benchmark.avg_response_time) / benchmark.avg_response_time * 100) if benchmark.avg_response_time > 0 else 0,
|
|
"status": "good" if current_avg <= benchmark.avg_response_time * 1.2 else "warning" if current_avg <= benchmark.avg_response_time * 1.5 else "critical"
|
|
}
|
|
|
|
comparisons.append(comparison)
|
|
|
|
performance_data["benchmarks"] = comparisons
|
|
|
|
return performance_data
|
|
|
|
def _calculate_percentile(self, data: List[float], percentile: int) -> float:
|
|
"""
|
|
计算百分位数
|
|
|
|
Args:
|
|
data: 数据列表
|
|
percentile: 百分位数
|
|
|
|
Returns:
|
|
百分位数值
|
|
"""
|
|
sorted_data = sorted(data)
|
|
index = (percentile / 100) * (len(sorted_data) - 1)
|
|
|
|
if index.is_integer():
|
|
return sorted_data[int(index)]
|
|
else:
|
|
lower = sorted_data[int(index)]
|
|
upper = sorted_data[int(index) + 1]
|
|
return lower + (upper - lower) * (index - int(index))
|
|
|
|
def _calculate_summary(self, results: List[TestResult]) -> Dict[str, Any]:
|
|
"""
|
|
计算测试摘要
|
|
|
|
Args:
|
|
results: 测试结果列表
|
|
|
|
Returns:
|
|
测试摘要
|
|
"""
|
|
total = len(results)
|
|
passed = sum(1 for r in results if r.success)
|
|
failed = total - passed
|
|
response_times = [r.response_time for r in results if r.response_time > 0]
|
|
|
|
return {
|
|
"total": total,
|
|
"passed": passed,
|
|
"failed": failed,
|
|
"pass_rate": (passed / total * 100) if total > 0 else 0,
|
|
"avg_response_time": statistics.mean(response_times) if response_times else 0
|
|
}
|
|
|
|
def _generate_trend_html(self, trend_data: List[TrendData]) -> str:
|
|
"""生成趋势分析HTML"""
|
|
# 这里简化实现,实际应该使用图表库(如Chart.js)
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>趋势分析报告</title>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; padding: 20px; }}
|
|
.trend-table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
|
|
.trend-table th, .trend-table td {{ border: 1px solid #ddd; padding: 8px; text-align: center; }}
|
|
.trend-table th {{ background-color: #4CAF50; color: white; }}
|
|
.trend-table tr:nth-child(even) {{ background-color: #f2f2f2; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>测试趋势分析报告</h1>
|
|
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
|
|
|
<table class="trend-table">
|
|
<tr>
|
|
<th>日期</th>
|
|
<th>总数</th>
|
|
<th>通过</th>
|
|
<th>失败</th>
|
|
<th>通过率</th>
|
|
<th>平均响应时间</th>
|
|
</tr>
|
|
{"".join([f"""
|
|
<tr>
|
|
<td>{d.date}</td>
|
|
<td>{d.total}</td>
|
|
<td>{d.passed}</td>
|
|
<td>{d.failed}</td>
|
|
<td>{d.pass_rate:.2f}%</td>
|
|
<td>{d.avg_response_time:.2f}ms</td>
|
|
</tr>
|
|
""" for d in trend_data])}
|
|
</table>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
def _generate_comparison_html(self, comparison_data: Dict[str, Any]) -> str:
|
|
"""生成对比分析HTML"""
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>测试对比报告</title>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; padding: 20px; }}
|
|
.comparison-table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
|
|
.comparison-table th, .comparison-table td {{ border: 1px solid #ddd; padding: 8px; text-align: center; }}
|
|
.comparison-table th {{ background-color: #4CAF50; color: white; }}
|
|
.positive {{ color: green; }}
|
|
.negative {{ color: red; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>测试对比报告</h1>
|
|
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
|
|
|
<table class="trend-table">
|
|
<tr>
|
|
<th>指标</th>
|
|
<th>当前</th>
|
|
<th>之前</th>
|
|
<th>变化</th>
|
|
</tr>
|
|
<tr>
|
|
<td>总数</td>
|
|
<td>{comparison_data['current']['total']}</td>
|
|
<td>{comparison_data['previous']['total']}</td>
|
|
<td class="{'positive' if comparison_data['changes']['total'] >= 0 else 'negative'}">
|
|
{comparison_data['changes']['total']:+d}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>通过</td>
|
|
<td>{comparison_data['current']['passed']}</td>
|
|
<td>{comparison_data['previous']['passed']}</td>
|
|
<td class="{'positive' if comparison_data['changes']['passed'] >= 0 else 'negative'}">
|
|
{comparison_data['changes']['passed']:+d}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>失败</td>
|
|
<td>{comparison_data['current']['failed']}</td>
|
|
<td>{comparison_data['previous']['failed']}</td>
|
|
<td class="{'negative' if comparison_data['changes']['failed'] > 0 else 'positive'}">
|
|
{comparison_data['changes']['failed']:+d}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>通过率</td>
|
|
<td>{comparison_data['current']['pass_rate']:.2f}%</td>
|
|
<td>{comparison_data['previous']['pass_rate']:.2f}%</td>
|
|
<td class="{'positive' if comparison_data['changes']['pass_rate'] >= 0 else 'negative'}">
|
|
{comparison_data['changes']['pass_rate']:+.2f}%
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
def _generate_performance_html(self, performance_data: Dict[str, Any]) -> str:
|
|
"""生成性能分析HTML"""
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>性能分析报告</title>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; padding: 20px; }}
|
|
.performance-table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
|
|
.performance-table th, .performance-table td {{ border: 1px solid #ddd; padding: 8px; text-align: center; }}
|
|
.performance-table th {{ background-color: #4CAF50; color: white; }}
|
|
.good {{ color: green; }}
|
|
.warning {{ color: orange; }}
|
|
.critical {{ color: red; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>性能分析报告</h1>
|
|
<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
|
|
|
<h2>性能指标</h2>
|
|
<table class="performance-table">
|
|
<tr>
|
|
<th>指标</th>
|
|
<th>值</th>
|
|
</tr>
|
|
<tr>
|
|
<td>平均响应时间</td>
|
|
<td>{performance_data.get('avg_response_time', 0):.2f}ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>最小响应时间</td>
|
|
<td>{performance_data.get('min_response_time', 0):.2f}ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>最大响应时间</td>
|
|
<td>{performance_data.get('max_response_time', 0):.2f}ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>中位数响应时间</td>
|
|
<td>{performance_data.get('median_response_time', 0):.2f}ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>P95响应时间</td>
|
|
<td>{performance_data.get('p95_response_time', 0):.2f}ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>P99响应时间</td>
|
|
<td>{performance_data.get('p99_response_time', 0):.2f}ms</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
def save_to_history(self, results: List[TestResult], summary: TestSummary) -> None:
|
|
"""
|
|
保存测试结果到历史记录
|
|
|
|
Args:
|
|
results: 测试结果列表
|
|
summary: 测试摘要
|
|
"""
|
|
date_str = datetime.now().strftime('%Y%m%d')
|
|
history_file = self.history_dir / f"test_report_{date_str}.json"
|
|
|
|
report_data = {
|
|
"summary": summary.to_dict(),
|
|
"results": [r.to_dict() for r in results],
|
|
"generated_at": datetime.now().isoformat()
|
|
}
|
|
|
|
with open(history_file, 'w', encoding='utf-8') as f:
|
|
json.dump(report_data, f, ensure_ascii=False, indent=2) |