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

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)