feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -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令牌格式")
|
||||
|
||||
# 解码payload(Base64URL编码)
|
||||
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}"
|
||||
Reference in New Issue
Block a user