Files
wehub-resource-sync 41b710f9c7
CI / Frontend checks (push) Failing after 0s
CI / Backend tests (push) Failing after 1s
I18n Documentation Sync / sync-docs (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:28:40 +08:00

362 lines
12 KiB
Python

"""
错误处理系统单元测试
"""
import time
from unittest.mock import patch, MagicMock
import pytest
from backend.utils.error_handler import (
AutoClipsException, APIError, NetworkError, ConfigurationError,
FileIOError, ProcessingError, ValidationError,
ErrorLevel, ErrorCategory, RetryConfig, CircuitBreaker,
retry_with_backoff, error_context, ErrorHandler, safe_execute
)
class TestAutoClipsException:
"""测试基础异常类"""
def test_exception_creation(self):
"""测试异常创建"""
error = AutoClipsException("测试错误", ErrorCategory.API)
assert error.message == "测试错误"
assert error.category == ErrorCategory.API
assert error.level == ErrorLevel.ERROR
assert error.timestamp > 0
def test_exception_to_dict(self):
"""测试异常转字典"""
original_exception = ValueError("原始错误")
error = AutoClipsException(
"测试错误",
ErrorCategory.API,
ErrorLevel.WARNING,
{"detail": "详细信息"},
original_exception
)
error_dict = error.to_dict()
assert error_dict["message"] == "测试错误"
assert error_dict["category"] == "API"
assert error_dict["level"] == "WARNING"
assert error_dict["details"]["detail"] == "详细信息"
assert "原始错误" in error_dict["original_exception"]
def test_exception_str_representation(self):
"""测试异常字符串表示"""
error = AutoClipsException("测试错误", ErrorCategory.NETWORK)
assert str(error) == "[NETWORK] 测试错误"
class TestSpecificExceptions:
"""测试特定异常类"""
def test_api_error(self):
"""测试API错误"""
error = APIError("API调用失败", status_code=400)
assert error.category == ErrorCategory.API
assert error.details["status_code"] == 400
def test_network_error(self):
"""测试网络错误"""
original_exception = ConnectionError("连接失败")
error = NetworkError("网络错误", original_exception=original_exception)
assert error.category == ErrorCategory.NETWORK
assert error.original_exception == original_exception
def test_file_io_error(self):
"""测试文件IO错误"""
error = FileIOError("文件读取失败", file_path="/test/file.txt")
assert error.category == ErrorCategory.FILE_IO
assert error.details["file_path"] == "/test/file.txt"
def test_processing_error(self):
"""测试处理错误"""
error = ProcessingError("处理失败", step="Step 1")
assert error.category == ErrorCategory.PROCESSING
assert error.details["step"] == "Step 1"
def test_validation_error(self):
"""测试验证错误"""
error = ValidationError("验证失败", field="api_key")
assert error.category == ErrorCategory.VALIDATION
assert error.level == ErrorLevel.WARNING
assert error.details["field"] == "api_key"
class TestRetryConfig:
"""测试重试配置"""
def test_retry_config_defaults(self):
"""测试重试配置默认值"""
config = RetryConfig()
assert config.max_retries == 3
assert config.base_delay == 1.0
assert config.max_delay == 60.0
assert config.exponential_base == 2.0
assert len(config.retryable_exceptions) > 0
def test_retry_config_custom_values(self):
"""测试重试配置自定义值"""
config = RetryConfig(
max_retries=5,
base_delay=2.0,
max_delay=120.0
)
assert config.max_retries == 5
assert config.base_delay == 2.0
assert config.max_delay == 120.0
class TestCircuitBreaker:
"""测试熔断器"""
def test_circuit_breaker_initial_state(self):
"""测试熔断器初始状态"""
cb = CircuitBreaker()
assert cb.state == "CLOSED"
assert cb.failure_count == 0
def test_circuit_breaker_successful_call(self):
"""测试熔断器成功调用"""
cb = CircuitBreaker()
def success_func():
return "success"
result = cb.call(success_func)
assert result == "success"
assert cb.state == "CLOSED"
def test_circuit_breaker_failure_threshold(self):
"""测试熔断器失败阈值"""
cb = CircuitBreaker(failure_threshold=2)
def failing_func():
raise ValueError("测试失败")
# 第一次失败
with patch('time.time', return_value=1000):
with pytest.raises(ValueError):
cb.call(failing_func)
assert cb.state == "CLOSED"
assert cb.failure_count == 1
# 第二次失败,触发熔断
with patch('time.time', return_value=1001):
with pytest.raises(ValueError):
cb.call(failing_func)
assert cb.state == "OPEN"
assert cb.failure_count == 2
def test_circuit_breaker_recovery(self):
"""测试熔断器恢复"""
cb = CircuitBreaker(failure_threshold=1, recovery_timeout=1.0)
def failing_func():
raise ValueError("测试失败")
# 触发熔断
with patch('time.time', return_value=1000):
with pytest.raises(ValueError):
cb.call(failing_func)
assert cb.state == "OPEN"
# 等待恢复时间后,状态变为半开
with patch('time.time', return_value=1002): # 超过恢复时间
def success_func():
return "success"
result = cb.call(success_func)
assert result == "success"
assert cb.state == "CLOSED" # 成功后关闭
class TestRetryDecorator:
"""测试重试装饰器"""
def test_retry_success_on_first_try(self):
"""测试第一次就成功"""
@retry_with_backoff()
def success_func():
return "success"
result = success_func()
assert result == "success"
def test_retry_success_after_failures(self):
"""测试失败后重试成功"""
call_count = 0
@retry_with_backoff(RetryConfig(max_retries=2, base_delay=0.1))
def failing_then_success():
nonlocal call_count
call_count += 1
if call_count < 3:
raise NetworkError("网络错误")
return "success"
result = failing_then_success()
assert result == "success"
assert call_count == 3
def test_retry_max_attempts_exceeded(self):
"""测试超过最大重试次数"""
@retry_with_backoff(RetryConfig(max_retries=1, base_delay=0.1))
def always_failing():
raise APIError("API错误")
with pytest.raises(APIError):
always_failing()
class TestErrorContext:
"""测试错误上下文管理器"""
def test_error_context_no_exception(self):
"""测试无异常的情况"""
with error_context(ErrorCategory.API):
result = "success"
assert result == "success"
def test_error_context_with_exception(self):
"""测试有异常的情况"""
with pytest.raises(APIError):
with error_context(ErrorCategory.API):
raise ValueError("原始错误")
def test_error_context_preserves_auto_clips_exception(self):
"""测试保留AutoClipsException"""
original_error = APIError("API错误")
with pytest.raises(APIError) as exc_info:
with error_context(ErrorCategory.NETWORK):
raise original_error
assert exc_info.value == original_error
class TestErrorHandler:
"""测试错误处理器"""
def test_error_handler_initialization(self):
"""测试错误处理器初始化"""
handler = ErrorHandler()
assert len(handler.error_log) == 0
assert len(handler.circuit_breakers) == 0
def test_error_handler_handle_error(self):
"""测试错误处理"""
handler = ErrorHandler()
error = APIError("测试API错误")
with patch('logging.Logger.error') as mock_logger:
handler.handle_error(error, "测试上下文")
assert len(handler.error_log) == 1
assert handler.error_log[0] == error
mock_logger.assert_called_once()
def test_error_handler_get_circuit_breaker(self):
"""测试获取熔断器"""
handler = ErrorHandler()
cb1 = handler.get_circuit_breaker("test")
cb2 = handler.get_circuit_breaker("test")
assert cb1 is cb2 # 同一个名称返回同一个实例
assert len(handler.circuit_breakers) == 1
def test_error_handler_get_error_summary(self):
"""测试获取错误摘要"""
handler = ErrorHandler()
# 无错误时
summary = handler.get_error_summary()
assert summary["total_errors"] == 0
# 有错误时
handler.handle_error(APIError("API错误1"))
handler.handle_error(NetworkError("网络错误"))
handler.handle_error(APIError("API错误2"))
summary = handler.get_error_summary()
assert summary["total_errors"] == 3
assert summary["error_counts"]["API"] == 2
assert summary["error_counts"]["NETWORK"] == 1
assert summary["latest_error"] is not None
def test_error_handler_clear_error_log(self):
"""测试清空错误日志"""
handler = ErrorHandler()
handler.handle_error(APIError("测试错误"))
assert len(handler.error_log) == 1
handler.clear_error_log()
assert len(handler.error_log) == 0
class TestSafeExecute:
"""测试安全执行函数"""
def test_safe_execute_success(self):
"""测试成功执行"""
def success_func():
return "success"
result = safe_execute(success_func, context="测试")
assert result == "success"
def test_safe_execute_with_retry(self):
"""测试带重试的执行"""
call_count = 0
def failing_then_success():
nonlocal call_count
call_count += 1
if call_count < 2:
raise NetworkError("网络错误")
return "success"
retry_config = RetryConfig(max_retries=1, base_delay=0.1)
result = safe_execute(failing_then_success, context="测试", retry_config=retry_config)
assert result == "success"
assert call_count == 2
def test_safe_execute_handles_auto_clips_exception(self):
"""测试处理AutoClipsException"""
def raise_auto_clips_error():
raise APIError("API错误")
with pytest.raises(APIError):
safe_execute(raise_auto_clips_error, context="测试")
def test_safe_execute_converts_generic_exception(self):
"""测试转换通用异常"""
def raise_generic_error():
raise ValueError("通用错误")
with pytest.raises(AutoClipsException) as exc_info:
safe_execute(raise_generic_error, context="测试")
assert exc_info.value.category == ErrorCategory.SYSTEM
assert "通用错误" in str(exc_info.value)
# 测试辅助函数
def test_error_level_enum():
"""测试错误级别枚举"""
assert ErrorLevel.DEBUG.value == "DEBUG"
assert ErrorLevel.ERROR.value == "ERROR"
assert ErrorLevel.CRITICAL.value == "CRITICAL"
def test_error_category_enum():
"""测试错误分类枚举"""
assert ErrorCategory.API.value == "API"
assert ErrorCategory.NETWORK.value == "NETWORK"
assert ErrorCategory.CONFIGURATION.value == "CONFIGURATION"
if __name__ == '__main__':
pytest.main([__file__])