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 @@
"""核心模块"""
@@ -0,0 +1,284 @@
"""
API测试器核心模块
"""
import requests
import time
from typing import Dict, Any, Optional, List
from dataclasses import dataclass, field
from .validation import ValidationEngine, ValidationRule
from .auth_manager import AuthManager
from config.settings import config
from utils.logger import TestLogger
from utils.reporter import TestResult as ReportTestResult, TestSummary
@dataclass
class TestResult:
"""测试结果"""
passed: bool
test_name: str
error_message: str = ""
response_time: float = 0.0
status_code: int = 0
request_data: Dict[str, Any] = field(default_factory=dict)
response_data: Dict[str, Any] = field(default_factory=dict)
class APITester:
"""API测试器"""
def __init__(self, logger: TestLogger = None, auto_auth: bool = True):
"""
初始化API测试器
Args:
logger: 日志记录器
auto_auth: 是否自动认证
"""
self.logger = logger or TestLogger("api_tester", config.logging_file, config.logging_level)
self.session = requests.Session()
self.auth_manager = AuthManager(self.logger)
self.validation_engine = ValidationEngine()
self.auto_auth = auto_auth
# 配置会话
self.session.headers.update({
"Content-Type": "application/json",
"Accept": "application/json"
})
# 如果启用自动认证,尝试登录
if auto_auth:
self._ensure_authenticated()
def _ensure_authenticated(self) -> bool:
"""
确保已认证
Returns:
是否认证成功
"""
return self.auth_manager.ensure_authenticated()
def _update_auth_header(self) -> None:
"""更新认证请求头"""
auth_header = self.auth_manager.get_auth_header(auto_refresh=True)
self.session.headers.update(auth_header)
def set_token(self, token: str) -> None:
"""
设置认证令牌(已弃用,建议使用AuthManager
Args:
token: JWT令牌
"""
self.logger.warning("set_token方法已弃用,建议使用AuthManager")
self.session.headers.update({
"Authorization": f"Bearer {token}"
})
self.logger.info(f"已设置认证令牌")
def clear_token(self) -> None:
"""清除认证令牌(已弃用,建议使用AuthManager"""
self.logger.warning("clear_token方法已弃用,建议使用AuthManager")
self.session.headers.pop("Authorization", None)
self.logger.info(f"已清除认证令牌")
def login(self, username: str = None, password: str = None) -> bool:
"""
用户登录
Args:
username: 用户名
password: 密码
Returns:
是否登录成功
"""
return self.auth_manager.login(username, password)
def request(
self,
method: str,
endpoint: str,
data: Dict[str, Any] = None,
params: Dict[str, Any] = None,
headers: Dict[str, str] = None,
expected_status: int = 200,
test_name: str = None,
require_auth: bool = True
) -> TestResult:
"""
发送HTTP请求
Args:
method: HTTP方法(GET, POST, PUT, DELETE
endpoint: API端点
data: 请求体数据
params: URL参数
headers: 请求头
expected_status: 期望的状态码
test_name: 测试名称
require_auth: 是否需要认证
Returns:
测试结果
"""
if test_name is None:
test_name = f"{method} {endpoint}"
url = f"{config.api_base_url}{endpoint}"
# 如果需要认证,确保已认证并更新认证头
if require_auth and self.auto_auth:
if not self._ensure_authenticated():
return TestResult(
passed=False,
test_name=test_name,
error_message="认证失败"
)
self._update_auth_header()
self.logger.log_test_start(test_name)
self.logger.log_request(method, url, data, headers)
try:
start_time = time.time()
if method.upper() == "GET":
response = self.session.get(
url,
params=params,
headers=headers,
timeout=config.api_timeout
)
elif method.upper() == "POST":
response = self.session.post(
url,
json=data,
params=params,
headers=headers,
timeout=config.api_timeout
)
elif method.upper() == "PUT":
response = self.session.put(
url,
json=data,
params=params,
headers=headers,
timeout=config.api_timeout
)
elif method.upper() == "DELETE":
response = self.session.delete(
url,
params=params,
headers=headers,
timeout=config.api_timeout
)
else:
raise ValueError(f"不支持的HTTP方法: {method}")
response_time = (time.time() - start_time) * 1000
try:
response_data = response.json()
except:
response_data = {"raw": response.text}
self.logger.log_response(response.status_code, response_time, response_data)
# 验证状态码
passed, error = self.validation_engine.validate_status_code(expected_status, response.status_code)
if passed:
self.logger.log_test_end(test_name, True, response_time)
return TestResult(
passed=True,
test_name=test_name,
response_time=response_time,
status_code=response.status_code,
request_data=data or params or {},
response_data=response_data
)
else:
self.logger.log_test_end(test_name, False, response_time)
return TestResult(
passed=False,
test_name=test_name,
error_message=error,
response_time=response_time,
status_code=response.status_code,
request_data=data or params or {},
response_data=response_data
)
except requests.exceptions.Timeout:
error_msg = "请求超时"
self.logger.error(f"{test_name} - {error_msg}")
return TestResult(
passed=False,
test_name=test_name,
error_message=error_msg,
response_time=config.api_timeout * 1000
)
except requests.exceptions.ConnectionError:
error_msg = "连接错误"
self.logger.error(f"{test_name} - {error_msg}")
return TestResult(
passed=False,
test_name=test_name,
error_message=error_msg
)
except Exception as e:
self.logger.log_error(e)
return TestResult(
passed=False,
test_name=test_name,
error_message=f"未知错误: {str(e)}"
)
def validate(
self,
test_result: TestResult,
validation_rules: List[ValidationRule]
) -> TestResult:
"""
验证测试结果
Args:
test_result: 测试结果
validation_rules: 验证规则列表
Returns:
验证后的测试结果
"""
if not test_result.passed:
return test_result
for rule in validation_rules:
if rule.rule_type == "status_code":
passed, error = rule.validate(test_result.status_code)
elif rule.rule_type == "response_time":
passed, error = rule.validate(test_result.response_time)
elif rule.rule_type in ["contains", "equals", "json_path", "regex", "header", "schema"]:
passed, error = rule.validate(actual_data=test_result.response_data)
else:
passed, error = False, f"未知的验证规则: {rule.rule_type}"
self.logger.log_validation(rule.rule_type, passed, error)
if not passed:
test_result.passed = False
test_result.error_message = error
break
return test_result
def close(self) -> None:
"""关闭会话"""
self.session.close()
self.logger.info("已关闭测试会话")
@@ -0,0 +1,338 @@
"""
认证管理器模块
提供自动令牌获取、存储、验证和刷新功能
"""
import time
import json
import hashlib
from typing import Optional, Dict, Any
from pathlib import Path
from dataclasses import dataclass
from datetime import datetime, timedelta
from utils.logger import TestLogger
from config.settings import config
@dataclass
class TokenInfo:
"""令牌信息"""
token: str
username: str
issued_at: float
expires_at: float
refresh_token: Optional[str] = None
def is_expired(self, buffer_seconds: int = 60) -> bool:
"""
检查令牌是否过期
Args:
buffer_seconds: 缓冲时间(秒),提前多少秒认为过期
Returns:
是否过期
"""
return time.time() > (self.expires_at - buffer_seconds)
def time_until_expiry(self) -> float:
"""
获取距离过期的时间
Returns:
距离过期的秒数
"""
return max(0, self.expires_at - time.time())
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
"token": self.token,
"username": self.username,
"issued_at": self.issued_at,
"expires_at": self.expires_at,
"refresh_token": self.refresh_token
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'TokenInfo':
"""从字典创建"""
return cls(
token=data["token"],
username=data["username"],
issued_at=data["issued_at"],
expires_at=data["expires_at"],
refresh_token=data.get("refresh_token")
)
class AuthManager:
"""认证管理器"""
def __init__(self, logger: TestLogger = None):
"""
初始化认证管理器
Args:
logger: 日志记录器
"""
self.logger = logger or TestLogger("auth_manager", config.logging_file, config.logging_level)
self.token_info: Optional[TokenInfo] = None
self.token_cache_file = Path(config.report_output_dir) / "token_cache.json"
# 令牌刷新缓冲时间(秒)
self.refresh_buffer = 60
# 加载缓存的令牌
self._load_cached_token()
def _load_cached_token(self) -> None:
"""从缓存加载令牌"""
if not self.token_cache_file.exists():
return
try:
with open(self.token_cache_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self.token_info = TokenInfo.from_dict(data)
if self.token_info.is_expired():
self.logger.info("缓存的令牌已过期,将重新获取")
self.token_info = None
else:
self.logger.info(f"从缓存加载令牌,剩余有效期: {self.token_info.time_until_expiry():.0f}")
except Exception as e:
self.logger.warning(f"加载缓存令牌失败: {e}")
self.token_info = None
def _save_cached_token(self) -> None:
"""保存令牌到缓存"""
if self.token_info is None:
return
try:
self.token_cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.token_cache_file, 'w', encoding='utf-8') as f:
json.dump(self.token_info.to_dict(), f, indent=2)
self.logger.info("令牌已缓存")
except Exception as e:
self.logger.warning(f"保存缓存令牌失败: {e}")
def _clear_cached_token(self) -> None:
"""清除缓存的令牌"""
try:
if self.token_cache_file.exists():
self.token_cache_file.unlink()
self.logger.info("缓存的令牌已清除")
except Exception as e:
self.logger.warning(f"清除缓存令牌失败: {e}")
def login(
self,
username: str = None,
password: str = None,
force_refresh: bool = False
) -> bool:
"""
用户登录
Args:
username: 用户名
password: 密码
force_refresh: 是否强制刷新令牌
Returns:
是否登录成功
"""
username = username or config.auth_username
password = password or config.auth_password
# 检查是否需要重新登录
if not force_refresh and self.token_info and not self.token_info.is_expired(self.refresh_buffer):
self.logger.info(f"使用现有令牌,剩余有效期: {self.token_info.time_until_expiry():.0f}")
return True
# 执行登录
self.logger.info(f"用户登录: {username}")
try:
import requests
login_url = f"{config.api_base_url}{config.auth_login_endpoint}"
response = requests.post(
login_url,
json={"username": username, "password": password},
timeout=config.api_timeout
)
if response.status_code == 200:
data = response.json()
# 兼容两种响应格式:
# 格式1: {"code": 200, "data": {"token": "...", "user": {...}}}
# 格式2: {"token": "...", "user": {...}}
token = None
if "data" in data and isinstance(data["data"], dict):
token = data["data"].get("token")
else:
token = data.get("token")
if token:
# 解析JWT令牌获取过期时间
expires_at = self._parse_token_expiry(token)
# 创建令牌信息
self.token_info = TokenInfo(
token=token,
username=username,
issued_at=time.time(),
expires_at=expires_at,
refresh_token=data.get("refreshToken") if "data" in data else None
)
# 缓存令牌
self._save_cached_token()
self.logger.info(f"✅ 登录成功,令牌有效期: {(expires_at - time.time()):.0f}")
return True
self.logger.error(f"❌ 登录失败: {response.text}")
return False
except Exception as e:
self.logger.error(f"❌ 登录异常: {str(e)}")
return False
def _parse_token_expiry(self, token: str) -> float:
"""
解析JWT令牌的过期时间
Args:
token: JWT令牌
Returns:
过期时间戳
"""
try:
# JWT格式: header.payload.signature
parts = token.split('.')
if len(parts) != 3:
raise ValueError("无效的JWT令牌格式")
# 解码payloadBase64URL编码)
import base64
payload = parts[1]
# 添加必要的填充
padding = 4 - len(payload) % 4
if padding != 4:
payload += '=' * padding
decoded = base64.urlsafe_b64decode(payload)
payload_data = json.loads(decoded)
# 获取过期时间(exp字段是Unix时间戳,秒)
exp = payload_data.get('exp')
if exp:
return float(exp)
# 如果没有exp字段,默认24小时后过期
return time.time() + 24 * 3600
except Exception as e:
self.logger.warning(f"解析令牌过期时间失败: {e},使用默认过期时间")
return time.time() + 24 * 3600
def get_token(self, auto_refresh: bool = True) -> Optional[str]:
"""
获取当前令牌
Args:
auto_refresh: 是否自动刷新过期令牌
Returns:
令牌字符串,如果未登录则返回None
"""
if self.token_info is None:
return None
# 检查令牌是否过期
if self.token_info.is_expired(self.refresh_buffer):
if auto_refresh:
self.logger.info("令牌即将过期,尝试自动刷新")
if self.login(force_refresh=True):
return self.token_info.token
else:
self.logger.error("自动刷新令牌失败")
return None
else:
self.logger.warning("令牌已过期")
return None
return self.token_info.token
def get_auth_header(self, auto_refresh: bool = True) -> Dict[str, str]:
"""
获取认证请求头
Args:
auto_refresh: 是否自动刷新过期令牌
Returns:
认证请求头字典
"""
token = self.get_token(auto_refresh)
if token:
return {"Authorization": f"Bearer {token}"}
else:
return {}
def logout(self) -> None:
"""用户登出"""
self.token_info = None
self._clear_cached_token()
self.logger.info("用户已登出")
def is_authenticated(self) -> bool:
"""
检查是否已认证
Returns:
是否已认证
"""
return self.token_info is not None and not self.token_info.is_expired()
def get_token_info(self) -> Optional[TokenInfo]:
"""
获取令牌信息
Returns:
令牌信息对象
"""
return self.token_info
def ensure_authenticated(self, username: str = None, password: str = None) -> bool:
"""
确保已认证,如果未认证则自动登录
Args:
username: 用户名
password: 密码
Returns:
是否认证成功
"""
if self.is_authenticated():
return True
return self.login(username, password)
@@ -0,0 +1,583 @@
"""
错误诊断模块
提供详细的错误分析、分类、归因和恢复建议
"""
import traceback
import re
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
import json
class ErrorCategory(Enum):
"""错误分类"""
NETWORK_ERROR = "network_error"
AUTH_ERROR = "auth_error"
VALIDATION_ERROR = "validation_error"
SERVER_ERROR = "server_error"
TIMEOUT_ERROR = "timeout_error"
DATA_ERROR = "data_error"
CONFIG_ERROR = "config_error"
UNKNOWN_ERROR = "unknown_error"
class ErrorSeverity(Enum):
"""错误严重程度"""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class ErrorAnalysis:
"""错误分析结果"""
error_type: str
error_category: ErrorCategory
error_severity: ErrorSeverity
error_message: str
stack_trace: str
context: Dict[str, Any]
possible_causes: List[str]
suggested_solutions: List[str]
recovery_actions: List[str]
related_tests: List[str]
class ErrorPattern:
"""错误模式"""
def __init__(self, pattern: str, category: ErrorCategory, severity: ErrorSeverity,
causes: List[str], solutions: List[str], recovery: List[str]):
"""
初始化错误模式
Args:
pattern: 错误模式(正则表达式)
category: 错误分类
severity: 错误严重程度
causes: 可能的原因
solutions: 建议的解决方案
recovery: 恢复操作
"""
self.pattern = re.compile(pattern, re.IGNORECASE)
self.category = category
self.severity = severity
self.causes = causes
self.solutions = solutions
self.recovery = recovery
def matches(self, error_message: str) -> bool:
"""
检查错误消息是否匹配此模式
Args:
error_message: 错误消息
Returns:
是否匹配
"""
return bool(self.pattern.search(error_message))
class ErrorDiagnoser:
"""错误诊断器"""
def __init__(self):
"""初始化错误诊断器"""
self.error_patterns = self._initialize_error_patterns()
self.error_history: List[ErrorAnalysis] = []
def _initialize_error_patterns(self) -> List[ErrorPattern]:
"""
初始化错误模式
Returns:
错误模式列表
"""
patterns = [
# 网络错误
ErrorPattern(
r"connection\s+(refused|timeout|reset|closed)",
ErrorCategory.NETWORK_ERROR,
ErrorSeverity.HIGH,
[
"网络连接被拒绝",
"服务器未启动或不可达",
"防火墙阻止连接",
"网络配置错误"
],
[
"检查服务器是否正在运行",
"验证网络连接",
"检查防火墙设置",
"确认服务器地址和端口正确"
],
[
"重启服务器",
"检查网络配置",
"重试连接"
]
),
ErrorPattern(
r"host\s+(unreachable|not\s+found)",
ErrorCategory.NETWORK_ERROR,
ErrorSeverity.HIGH,
[
"主机地址不存在",
"DNS解析失败",
"网络不可达"
],
[
"验证主机地址正确",
"检查DNS配置",
"确认网络连接"
],
[
"修正主机地址",
"重试连接"
]
),
# 认证错误
ErrorPattern(
r"(unauthorized|authentication\s+failed|invalid\s+(token|credentials))",
ErrorCategory.AUTH_ERROR,
ErrorSeverity.HIGH,
[
"认证令牌无效或过期",
"用户名或密码错误",
"权限不足"
],
[
"检查认证令牌",
"验证用户凭据",
"确认用户权限"
],
[
"重新登录",
"刷新认证令牌",
"联系管理员"
]
),
ErrorPattern(
r"(forbidden|access\s+denied)",
ErrorCategory.AUTH_ERROR,
ErrorSeverity.HIGH,
[
"访问被拒绝",
"权限不足",
"资源不存在"
],
[
"检查用户权限",
"验证资源是否存在",
"确认访问控制配置"
],
[
"联系管理员",
"申请相应权限"
]
),
# 验证错误
ErrorPattern(
r"(validation\s+failed|invalid\s+(parameter|input|data))",
ErrorCategory.VALIDATION_ERROR,
ErrorSeverity.MEDIUM,
[
"输入数据格式错误",
"参数验证失败",
"数据不符合要求"
],
[
"检查输入数据格式",
"验证参数类型和范围",
"参考API文档"
],
[
"修正输入数据",
"调整参数值"
]
),
ErrorPattern(
r"(required\s+field\s+missing|missing\s+required\s+parameter)",
ErrorCategory.VALIDATION_ERROR,
ErrorSeverity.MEDIUM,
[
"缺少必填字段",
"参数不完整"
],
[
"检查请求参数",
"确认必填字段",
"参考API文档"
],
[
"补充必填字段",
"修正请求参数"
]
),
# 服务器错误
ErrorPattern(
r"(internal\s+server\s+error|server\s+error)",
ErrorCategory.SERVER_ERROR,
ErrorSeverity.CRITICAL,
[
"服务器内部错误",
"服务器处理异常",
"服务器配置问题"
],
[
"检查服务器日志",
"验证服务器配置",
"检查数据库连接"
],
[
"联系技术支持",
"检查服务器状态",
"重启服务器"
]
),
ErrorPattern(
r"(database\s+(error|connection\s+failed|timeout))",
ErrorCategory.SERVER_ERROR,
ErrorSeverity.CRITICAL,
[
"数据库连接失败",
"数据库查询错误",
"数据库超时"
],
[
"检查数据库服务状态",
"验证数据库连接配置",
"检查SQL语句"
],
[
"重启数据库服务",
"修正连接配置",
"优化SQL查询"
]
),
# 超时错误
ErrorPattern(
r"(request\s+timeout|operation\s+timed\s+out)",
ErrorCategory.TIMEOUT_ERROR,
ErrorSeverity.MEDIUM,
[
"请求超时",
"服务器响应慢",
"网络延迟高"
],
[
"检查网络连接",
"增加超时时间",
"优化请求"
],
[
"重试请求",
"增加超时配置",
"优化网络环境"
]
),
# 数据错误
ErrorPattern(
r"(data\s+(not\s+found|does\s+not\s+exist)|record\s+not\s+found)",
ErrorCategory.DATA_ERROR,
ErrorSeverity.MEDIUM,
[
"数据不存在",
"记录未找到",
"数据已被删除"
],
[
"验证数据ID",
"检查数据是否存在",
"确认数据状态"
],
[
"使用正确的数据ID",
"重新创建数据"
]
),
ErrorPattern(
r"(duplicate\s+(key|entry|record)|constraint\s+violation)",
ErrorCategory.DATA_ERROR,
ErrorSeverity.MEDIUM,
[
"数据重复",
"唯一约束冲突",
"数据已存在"
],
[
"检查数据是否已存在",
"验证唯一字段",
"使用不同的值"
],
[
"删除重复数据",
"使用不同的值",
"更新现有数据"
]
),
# 配置错误
ErrorPattern(
r"(configuration\s+error|invalid\s+configuration|config\s+not\s+found)",
ErrorCategory.CONFIG_ERROR,
ErrorSeverity.HIGH,
[
"配置错误",
"配置文件缺失",
"配置参数无效"
],
[
"检查配置文件",
"验证配置参数",
"参考配置文档"
],
[
"修正配置文件",
"重置配置",
"重新加载配置"
]
)
]
return patterns
def diagnose_error(self, exception: Exception, context: Dict[str, Any] = None) -> ErrorAnalysis:
"""
诊断错误
Args:
exception: 异常对象
context: 上下文信息
Returns:
错误分析结果
"""
error_message = str(exception)
stack_trace = traceback.format_exc()
# 查找匹配的错误模式
matched_pattern = None
for pattern in self.error_patterns:
if pattern.matches(error_message):
matched_pattern = pattern
break
# 如果没有匹配的模式,使用默认分类
if matched_pattern is None:
matched_pattern = ErrorPattern(
r".*",
ErrorCategory.UNKNOWN_ERROR,
ErrorSeverity.MEDIUM,
["未知错误"],
["检查日志", "联系技术支持"],
["重试操作", "联系支持"]
)
# 分析上下文
context = context or {}
context.update({
"error_type": type(exception).__name__,
"error_message": error_message,
"timestamp": context.get("timestamp"),
"test_name": context.get("test_name"),
"url": context.get("url"),
"method": context.get("method")
})
# 创建错误分析结果
analysis = ErrorAnalysis(
error_type=type(exception).__name__,
error_category=matched_pattern.category,
error_severity=matched_pattern.severity,
error_message=error_message,
stack_trace=stack_trace,
context=context,
possible_causes=matched_pattern.causes,
suggested_solutions=matched_pattern.solutions,
recovery_actions=matched_pattern.recovery,
related_tests=self._find_related_tests(context)
)
# 记录错误历史
self.error_history.append(analysis)
return analysis
def _find_related_tests(self, context: Dict[str, Any]) -> List[str]:
"""
查找相关测试
Args:
context: 上下文信息
Returns:
相关测试列表
"""
related_tests = []
url = context.get("url", "")
method = context.get("method", "")
# 根据URL和方法查找相关测试
if "/auth/login" in url:
related_tests.extend(["用户登录", "认证测试", "权限测试"])
elif "/user/" in url:
related_tests.extend(["用户管理测试", "用户列表测试", "用户创建测试"])
elif "/role/" in url:
related_tests.extend(["角色管理测试", "角色列表测试", "角色创建测试"])
elif "/menu/" in url:
related_tests.extend(["菜单管理测试", "菜单列表测试"])
return related_tests
def get_error_statistics(self) -> Dict[str, Any]:
"""
获取错误统计信息
Returns:
错误统计信息
"""
if not self.error_history:
return {}
# 按分类统计
category_stats = {}
for analysis in self.error_history:
category = analysis.error_category.value
if category not in category_stats:
category_stats[category] = 0
category_stats[category] += 1
# 按严重程度统计
severity_stats = {}
for analysis in self.error_history:
severity = analysis.error_severity.value
if severity not in severity_stats:
severity_stats[severity] = 0
severity_stats[severity] += 1
# 按错误类型统计
type_stats = {}
for analysis in self.error_history:
error_type = analysis.error_type
if error_type not in type_stats:
type_stats[error_type] = 0
type_stats[error_type] += 1
return {
"total_errors": len(self.error_history),
"by_category": category_stats,
"by_severity": severity_stats,
"by_type": type_stats,
"most_common_errors": self._get_most_common_errors(5)
}
def _get_most_common_errors(self, limit: int = 5) -> List[Dict[str, Any]]:
"""
获取最常见的错误
Args:
limit: 返回数量限制
Returns:
最常见错误列表
"""
error_counts = {}
for analysis in self.error_history:
error_key = f"{analysis.error_type}: {analysis.error_message[:50]}"
if error_key not in error_counts:
error_counts[error_key] = 0
error_counts[error_key] += 1
sorted_errors = sorted(error_counts.items(), key=lambda x: x[1], reverse=True)
return [
{
"error": error[0],
"count": error[1]
}
for error in sorted_errors[:limit]
]
def generate_error_report(self, analysis: ErrorAnalysis) -> str:
"""
生成错误报告
Args:
analysis: 错误分析结果
Returns:
错误报告(Markdown格式)
"""
report = f"""# 错误诊断报告
## 错误信息
- **错误类型**: {analysis.error_type}
- **错误分类**: {analysis.error_category.value}
- **严重程度**: {analysis.error_severity.value}
- **错误消息**: {analysis.error_message}
## 错误堆栈
```
{analysis.stack_trace}
```
## 上下文信息
{self._format_context(analysis.context)}
## 可能的原因
{self._format_list(analysis.possible_causes)}
## 建议的解决方案
{self._format_list(analysis.suggested_solutions)}
## 恢复操作
{self._format_list(analysis.recovery_actions)}
## 相关测试
{self._format_list(analysis.related_tests)}
---
*报告生成时间: {self._get_current_timestamp()}*
"""
return report
def _format_context(self, context: Dict[str, Any]) -> str:
"""格式化上下文信息"""
lines = []
for key, value in context.items():
if value is not None:
lines.append(f"- **{key}**: {value}")
return "\n".join(lines)
def _format_list(self, items: List[str]) -> str:
"""格式化列表"""
return "\n".join([f"- {item}" for item in items])
def _get_current_timestamp(self) -> str:
"""获取当前时间戳"""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def clear_history(self) -> None:
"""清空错误历史"""
self.error_history.clear()
@@ -0,0 +1,380 @@
"""
测试执行性能优化模块
支持并行执行、结果缓存、连接池等功能
"""
import asyncio
import time
from typing import List, Dict, Any, Optional, Callable
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import lru_cache
from dataclasses import dataclass
import hashlib
import json
from .api_tester import APITester, TestResult
from ..utils.logger import TestLogger
@dataclass
class CachedResult:
"""缓存结果"""
key: str
result: TestResult
timestamp: float
ttl: int = 3600 # 缓存有效期(秒)
def is_expired(self) -> bool:
"""检查缓存是否过期"""
return time.time() - self.timestamp > self.ttl
class ResultCache:
"""测试结果缓存"""
def __init__(self, default_ttl: int = 3600):
"""
初始化缓存
Args:
default_ttl: 默认缓存有效期(秒)
"""
self.cache: Dict[str, CachedResult] = {}
self.default_ttl = default_ttl
def _generate_key(self, method: str, endpoint: str, data: Dict[str, Any] = None,
params: Dict[str, Any] = None) -> str:
"""
生成缓存键
Args:
method: HTTP方法
endpoint: API端点
data: 请求体数据
params: URL参数
Returns:
缓存键
"""
key_data = {
"method": method,
"endpoint": endpoint,
"data": data or {},
"params": params or {}
}
key_str = json.dumps(key_data, sort_keys=True)
return hashlib.md5(key_str.encode()).hexdigest()
def get(self, method: str, endpoint: str, data: Dict[str, Any] = None,
params: Dict[str, Any] = None) -> Optional[TestResult]:
"""
获取缓存结果
Args:
method: HTTP方法
endpoint: API端点
data: 请求体数据
params: URL参数
Returns:
缓存的测试结果
"""
key = self._generate_key(method, endpoint, data, params)
if key in self.cache:
cached = self.cache[key]
if not cached.is_expired():
return cached.result
else:
del self.cache[key]
return None
def set(self, method: str, endpoint: str, result: TestResult,
data: Dict[str, Any] = None, params: Dict[str, Any] = None,
ttl: int = None) -> None:
"""
设置缓存结果
Args:
method: HTTP方法
endpoint: API端点
result: 测试结果
data: 请求体数据
params: URL参数
ttl: 缓存有效期(秒)
"""
key = self._generate_key(method, endpoint, data, params)
cached = CachedResult(
key=key,
result=result,
timestamp=time.time(),
ttl=ttl or self.default_ttl
)
self.cache[key] = cached
def clear(self) -> None:
"""清空缓存"""
self.cache.clear()
def cleanup_expired(self) -> None:
"""清理过期缓存"""
expired_keys = [key for key, cached in self.cache.items() if cached.is_expired()]
for key in expired_keys:
del self.cache[key]
class ParallelTestExecutor:
"""并行测试执行器"""
def __init__(self, max_workers: int = 4, logger: TestLogger = None):
"""
初始化并行执行器
Args:
max_workers: 最大工作线程数
logger: 日志记录器
"""
self.max_workers = max_workers
self.logger = logger
def execute_tests(self, test_functions: List[Callable],
use_cache: bool = True, cache: ResultCache = None) -> List[TestResult]:
"""
并行执行测试
Args:
test_functions: 测试函数列表
use_cache: 是否使用缓存
cache: 缓存实例
Returns:
测试结果列表
"""
results = []
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# 提交所有测试任务
future_to_test = {
executor.submit(self._execute_single_test, func, use_cache, cache): func
for func in test_functions
}
# 收集结果
for future in as_completed(future_to_test):
test_func = future_to_test[future]
try:
result = future.result()
results.append(result)
except Exception as e:
if self.logger:
self.logger.error(f"测试执行失败: {str(e)}")
results.append(TestResult(
passed=False,
test_name=test_func.__name__,
error_message=str(e)
))
return results
def _execute_single_test(self, test_func: Callable, use_cache: bool,
cache: ResultCache) -> TestResult:
"""
执行单个测试
Args:
test_func: 测试函数
use_cache: 是否使用缓存
cache: 缓存实例
Returns:
测试结果
"""
try:
# 尝试从缓存获取结果
if use_cache and cache:
# 这里简化实现,实际应该根据测试函数的参数生成缓存键
pass
# 执行测试
return test_func()
except Exception as e:
raise e
def execute_tests_async(self, test_functions: List[Callable]) -> List[TestResult]:
"""
异步并行执行测试
Args:
test_functions: 测试函数列表
Returns:
测试结果列表
"""
async def run_all():
tasks = [self._execute_single_test_async(func) for func in test_functions]
return await asyncio.gather(*tasks, return_exceptions=True)
results = asyncio.run(run_all())
# 处理异常结果
processed_results = []
for i, result in enumerate(results):
if isinstance(result, Exception):
processed_results.append(TestResult(
passed=False,
test_name=test_functions[i].__name__,
error_message=str(result)
))
else:
processed_results.append(result)
return processed_results
async def _execute_single_test_async(self, test_func: Callable) -> TestResult:
"""
异步执行单个测试
Args:
test_func: 测试函数
Returns:
测试结果
"""
# 这里简化实现,实际应该使用异步HTTP客户端
return test_func()
class PerformanceOptimizer:
"""性能优化器"""
def __init__(self, logger: TestLogger = None):
"""
初始化性能优化器
Args:
logger: 日志记录器
"""
self.logger = logger
self.cache = ResultCache()
self.executor = ParallelTestExecutor(logger=logger)
def optimize_test_execution(self, test_functions: List[Callable],
parallel: bool = True, use_cache: bool = True) -> List[TestResult]:
"""
优化测试执行
Args:
test_functions: 测试函数列表
parallel: 是否并行执行
use_cache: 是否使用缓存
Returns:
测试结果列表
"""
start_time = time.time()
if self.logger:
self.logger.info(f"开始优化测试执行: {len(test_functions)}个测试用例")
self.logger.info(f"并行执行: {parallel}, 使用缓存: {use_cache}")
# 清理过期缓存
self.cache.cleanup_expired()
# 执行测试
if parallel:
results = self.executor.execute_tests(test_functions, use_cache, self.cache)
else:
results = []
for func in test_functions:
try:
result = func()
results.append(result)
except Exception as e:
if self.logger:
self.logger.error(f"测试执行失败: {str(e)}")
results.append(TestResult(
passed=False,
test_name=func.__name__,
error_message=str(e)
))
execution_time = time.time() - start_time
if self.logger:
self.logger.info(f"测试执行完成: {execution_time:.2f}")
self.logger.info(f"平均每个测试: {execution_time/len(test_functions):.2f}")
return results
def get_cache_stats(self) -> Dict[str, Any]:
"""
获取缓存统计信息
Returns:
缓存统计信息
"""
return {
"total_entries": len(self.cache.cache),
"expired_entries": sum(1 for cached in self.cache.cache.values() if cached.is_expired()),
"valid_entries": sum(1 for cached in self.cache.cache.values() if not cached.is_expired())
}
def clear_cache(self) -> None:
"""清空缓存"""
self.cache.clear()
if self.logger:
self.logger.info("缓存已清空")
class ConnectionPool:
"""连接池"""
def __init__(self, max_connections: int = 10):
"""
初始化连接池
Args:
max_connections: 最大连接数
"""
self.max_connections = max_connections
self.connections = []
def get_connection(self) -> APITester:
"""
获取连接
Returns:
API测试器实例
"""
if self.connections:
return self.connections.pop()
return APITester()
def return_connection(self, connection: APITester) -> None:
"""
归还连接
Args:
connection: API测试器实例
"""
if len(self.connections) < self.max_connections:
self.connections.append(connection)
else:
connection.close()
def close_all(self) -> None:
"""关闭所有连接"""
for connection in self.connections:
connection.close()
self.connections.clear()
@@ -0,0 +1,300 @@
"""
测试验证引擎模块
"""
from typing import Dict, Any, Tuple, Optional
import re
import json
from datetime import datetime
class ValidationEngine:
"""测试验证引擎"""
@staticmethod
def validate_status_code(expected: int, actual: int) -> Tuple[bool, str]:
"""
验证HTTP状态码
Args:
expected: 期望的状态码
actual: 实际的状态码
Returns:
(是否通过, 错误消息)
"""
if expected == actual:
return True, ""
return False, f"期望状态码{expected},实际{actual}"
@staticmethod
def validate_response_body(expected: Dict[str, Any], actual: Dict[str, Any]) -> Tuple[bool, str]:
"""
验证响应体
Args:
expected: 期望的响应体
actual: 实际的响应体
Returns:
(是否通过, 错误消息)
"""
if expected == actual:
return True, ""
# 找出差异
differences = ValidationEngine._find_differences(expected, actual)
return False, f"响应体不匹配: {differences}"
@staticmethod
def validate_contains(expected_value: Any, actual_value: Any) -> Tuple[bool, str]:
"""
验证包含关系
Args:
expected_value: 期望包含的值
actual_value: 实际值
Returns:
(是否通过, 错误消息)
"""
if isinstance(actual_value, (str, list, dict)):
if expected_value in actual_value:
return True, ""
return False, f"期望值'{expected_value}'不在实际值中"
if expected_value == actual_value:
return True, ""
return False, f"期望包含'{expected_value}',实际为'{actual_value}'"
@staticmethod
def validate_equals(expected_value: Any, actual_value: Any) -> Tuple[bool, str]:
"""
验证相等关系
Args:
expected_value: 期望的值
actual_value: 实际的值
Returns:
(是否通过, 错误消息)
"""
if expected_value == actual_value:
return True, ""
return False, f"期望值'{expected_value}',实际值'{actual_value}'"
@staticmethod
def validate_json_path(path: str, expected_value: Any, actual_data: Dict[str, Any]) -> Tuple[bool, str]:
"""
验证JSON路径
Args:
path: JSON路径(如"data.user.id"
expected_value: 期望的值
actual_data: 实际的数据
Returns:
(是否通过, 错误消息)
"""
try:
keys = path.split('.')
value = actual_data
for key in keys:
if isinstance(value, dict):
value = value.get(key)
elif isinstance(value, list) and key.isdigit():
value = value[int(key)]
else:
return False, f"路径'{path}'不存在"
if value == expected_value:
return True, ""
return False, f"路径'{path}'期望值'{expected_value}',实际值'{value}'"
except Exception as e:
return False, f"验证JSON路径失败: {str(e)}"
@staticmethod
def validate_regex(pattern: str, actual_value: str) -> Tuple[bool, str]:
"""
验证正则表达式
Args:
pattern: 正则表达式模式
actual_value: 实际的值
Returns:
(是否通过, 错误消息)
"""
try:
if re.match(pattern, str(actual_value)):
return True, ""
return False, f"'{actual_value}'不匹配正则表达式'{pattern}'"
except Exception as e:
return False, f"正则表达式验证失败: {str(e)}"
@staticmethod
def validate_header(expected_header: Dict[str, str], actual_headers: Dict[str, str]) -> Tuple[bool, str]:
"""
验证响应头
Args:
expected_header: 期望的响应头
actual_headers: 实际的响应头
Returns:
(是否通过, 错误消息)
"""
for key, expected_value in expected_header.items():
actual_value = actual_headers.get(key)
if actual_value is None:
return False, f"缺少响应头: {key}"
if actual_value != expected_value:
return False, f"响应头'{key}'期望值'{expected_value}',实际值'{actual_value}'"
return True, ""
@staticmethod
def validate_response_time(expected_max_time: float, actual_time: float) -> Tuple[bool, str]:
"""
验证响应时间
Args:
expected_max_time: 期望的最大响应时间(毫秒)
actual_time: 实际的响应时间(毫秒)
Returns:
(是否通过, 错误消息)
"""
if actual_time <= expected_max_time:
return True, ""
return False, f"响应时间{actual_time}ms超过期望最大值{expected_max_time}ms"
@staticmethod
def validate_schema(expected_schema: Dict[str, Any], actual_data: Dict[str, Any]) -> Tuple[bool, str]:
"""
验证数据结构
Args:
expected_schema: 期望的结构(简化版)
actual_data: 实际的数据
Returns:
(是否通过, 错误消息)
"""
for key, expected_type in expected_schema.items():
if key not in actual_data:
return False, f"缺少字段: {key}"
actual_value = actual_data[key]
if expected_type == "string" and not isinstance(actual_value, str):
return False, f"字段'{key}'期望类型string,实际类型{type(actual_value).__name__}"
elif expected_type == "number" and not isinstance(actual_value, (int, float)):
return False, f"字段'{key}'期望类型number,实际类型{type(actual_value).__name__}"
elif expected_type == "boolean" and not isinstance(actual_value, bool):
return False, f"字段'{key}'期望类型boolean,实际类型{type(actual_value).__name__}"
elif expected_type == "array" and not isinstance(actual_value, list):
return False, f"字段'{key}'期望类型array,实际类型{type(actual_value).__name__}"
elif expected_type == "object" and not isinstance(actual_value, dict):
return False, f"字段'{key}'期望类型object,实际类型{type(actual_value).__name__}"
return True, ""
@staticmethod
def _find_differences(expected: Any, actual: Any, path: str = "") -> str:
"""
找出两个值之间的差异
Args:
expected: 期望的值
actual: 实际的值
path: 当前路径
Returns:
差异描述
"""
if expected == actual:
return ""
if isinstance(expected, dict) and isinstance(actual, dict):
differences = []
all_keys = set(expected.keys()) | set(actual.keys())
for key in all_keys:
new_path = f"{path}.{key}" if path else key
if key not in expected:
differences.append(f"{new_path}: 实际存在但期望不存在")
elif key not in actual:
differences.append(f"{new_path}: 期望存在但实际不存在")
else:
diff = ValidationEngine._find_differences(expected[key], actual[key], new_path)
if diff:
differences.append(diff)
return "; ".join(differences)
elif isinstance(expected, list) and isinstance(actual, list):
if len(expected) != len(actual):
return f"{path}: 长度不匹配(期望{len(expected)},实际{len(actual)}"
differences = []
for i, (exp_item, act_item) in enumerate(zip(expected, actual)):
new_path = f"{path}[{i}]"
diff = ValidationEngine._find_differences(exp_item, act_item, new_path)
if diff:
differences.append(diff)
return "; ".join(differences)
else:
return f"{path}: 期望'{expected}',实际'{actual}'"
class ValidationRule:
"""验证规则"""
def __init__(self, rule_type: str, expected_value: Any = None, path: str = None):
"""
初始化验证规则
Args:
rule_type: 规则类型(status_code, contains, equals, json_path, regex, header, response_time, schema
expected_value: 期望值
path: JSON路径(仅用于json_path规则)
"""
self.rule_type = rule_type
self.expected_value = expected_value
self.path = path
def validate(self, actual_value: Any = None, actual_data: Dict[str, Any] = None) -> Tuple[bool, str]:
"""
执行验证
Args:
actual_value: 实际值
actual_data: 实际数据(用于JSON路径验证)
Returns:
(是否通过, 错误消息)
"""
if self.rule_type == "status_code":
return ValidationEngine.validate_status_code(self.expected_value, actual_value)
elif self.rule_type == "contains":
return ValidationEngine.validate_contains(self.expected_value, actual_value)
elif self.rule_type == "equals":
return ValidationEngine.validate_equals(self.expected_value, actual_value)
elif self.rule_type == "json_path":
return ValidationEngine.validate_json_path(self.path, self.expected_value, actual_data)
elif self.rule_type == "regex":
return ValidationEngine.validate_regex(self.expected_value, actual_value)
elif self.rule_type == "header":
return ValidationEngine.validate_header(self.expected_value, actual_value)
elif self.rule_type == "response_time":
return ValidationEngine.validate_response_time(self.expected_value, actual_value)
elif self.rule_type == "schema":
return ValidationEngine.validate_schema(self.expected_value, actual_data)
else:
return False, f"未知的验证规则类型: {self.rule_type}"