refactor(exceptions): 统一异常体系并替换零散异常抛出
1. 新增FrameworkError作为框架技术异常基类,统一处理非业务校验类错误 2. 为所有异常类添加error_code和trace_id支持,完善异常序列化能力 3. 将多处原生异常、RuntimeError替换为标准领域/框架异常 4. 优化WSDL解析逻辑,添加详细的警告日志便于排查问题
This commit is contained in:
parent
e9a1ea1050
commit
1003b842e1
@ -46,22 +46,48 @@ class ExternalSystemError(Exception):
|
||||
|
||||
所有本上下文抛出的异常都应继承此类,最外层统一捕获后映射为 HTTP 响应。
|
||||
``status_code`` 作为异常到 HTTP 状态码的映射依据,子类按需覆盖。
|
||||
``error_code`` 作为稳定错误码,子类以类变量覆盖,用于驱动协议响应与日志关联。
|
||||
``trace_id`` 用于跨链路追踪关联。
|
||||
"""
|
||||
|
||||
status_code: int = 500
|
||||
error_code: str = "EXTERNAL_SYSTEM_ERROR"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
*,
|
||||
details: dict[str, Any] | None = None,
|
||||
trace_id: str | None = None,
|
||||
) -> None:
|
||||
"""初始化异常实例。
|
||||
|
||||
参数:
|
||||
message: 人类可读错误信息。
|
||||
details: 业务字段字典,默认空字典。
|
||||
trace_id: 调用链路追踪 ID,用于跨链路关联。
|
||||
"""
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.details = details or {}
|
||||
self.trace_id = trace_id
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""序列化为字典,便于跨层传递、日志记录与 API 响应。
|
||||
|
||||
返回 ``error_code`` / ``message`` / ``trace_id`` 三个基础字段,
|
||||
并展开 ``details`` 中的业务字段。
|
||||
"""
|
||||
return {
|
||||
"error_code": self.error_code,
|
||||
"message": self.message,
|
||||
"trace_id": self.trace_id,
|
||||
**self.details,
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
"""返回 ``[error_code] message`` 格式的字符串表示。"""
|
||||
return f"[{self.error_code}] {self.message}"
|
||||
|
||||
|
||||
# ─── 业务规则异常 ──────────────────────────────────────────────────────────
|
||||
@ -75,24 +101,32 @@ class DomainValidationError(ExternalSystemError, ValueError):
|
||||
"""
|
||||
|
||||
status_code = 400
|
||||
error_code = "VALIDATION_ERROR"
|
||||
|
||||
|
||||
class InvalidSlugError(DomainValidationError):
|
||||
"""slug 非法或命中保留字。"""
|
||||
|
||||
error_code = "INVALID_SLUG"
|
||||
|
||||
|
||||
class EnvironmentDisabledError(DomainValidationError):
|
||||
"""禁用环境不能设为默认环境。"""
|
||||
|
||||
error_code = "ENVIRONMENT_DISABLED"
|
||||
|
||||
|
||||
class ImportConflictError(DomainValidationError):
|
||||
"""导入时发现 slug 冲突。"""
|
||||
|
||||
error_code = "IMPORT_CONFLICT"
|
||||
|
||||
|
||||
class SsrfViolationError(DomainValidationError):
|
||||
"""SSRF 安全规则违反。"""
|
||||
|
||||
status_code = 403
|
||||
error_code = "SSRF_VIOLATION"
|
||||
|
||||
|
||||
class CircuitBreakerStateError(DomainValidationError):
|
||||
@ -102,6 +136,8 @@ class CircuitBreakerStateError(DomainValidationError):
|
||||
表示熔断器未处于打开或半开状态,无需也无法重置。
|
||||
"""
|
||||
|
||||
error_code = "CIRCUIT_BREAKER_STATE_INVALID"
|
||||
|
||||
|
||||
class EntityNotFoundError(ExternalSystemError):
|
||||
"""领域实体不存在。
|
||||
@ -112,6 +148,7 @@ class EntityNotFoundError(ExternalSystemError):
|
||||
"""
|
||||
|
||||
status_code = 404
|
||||
error_code = "NOT_FOUND"
|
||||
|
||||
|
||||
class ReferencedError(ExternalSystemError):
|
||||
@ -127,12 +164,25 @@ class ReferencedError(ExternalSystemError):
|
||||
"""
|
||||
|
||||
status_code = 409
|
||||
error_code = "REFERENCED"
|
||||
|
||||
def __init__(self, references: dict[str, dict[str, list[str]]]) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
references: dict[str, dict[str, list[str]]],
|
||||
*,
|
||||
trace_id: str | None = None,
|
||||
) -> None:
|
||||
"""初始化被引用无法删除异常。
|
||||
|
||||
参数:
|
||||
references: 被引用关系字典,结构示例见类 docstring。
|
||||
trace_id: 调用链路追踪 ID,用于跨链路关联。
|
||||
"""
|
||||
self.references = references
|
||||
super().__init__(
|
||||
f"以下外部系统仍被引用,无法删除: {references}",
|
||||
details={"references": references},
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
|
||||
@ -145,6 +195,7 @@ class ConflictError(ExternalSystemError):
|
||||
"""
|
||||
|
||||
status_code = 409
|
||||
error_code = "CONFLICT"
|
||||
|
||||
|
||||
# ─── 框架技术异常 ──────────────────────────────────────────────────────────
|
||||
@ -158,6 +209,8 @@ class FrameworkError(ExternalSystemError):
|
||||
而非"业务规则不允许"。
|
||||
"""
|
||||
|
||||
error_code = "FRAMEWORK_ERROR"
|
||||
|
||||
|
||||
class AdapterNotFoundError(FrameworkError):
|
||||
"""找不到对应 ``adapter_type`` 的适配器。
|
||||
@ -166,12 +219,14 @@ class AdapterNotFoundError(FrameworkError):
|
||||
"""
|
||||
|
||||
status_code = 404
|
||||
error_code = "ADAPTER_NOT_FOUND"
|
||||
|
||||
|
||||
class AuthPluginNotFoundError(FrameworkError):
|
||||
"""找不到对应 ``auth_type`` 的认证插件。"""
|
||||
|
||||
status_code = 404
|
||||
error_code = "AUTH_PLUGIN_NOT_FOUND"
|
||||
|
||||
|
||||
class IntegrationOperationNotRegisteredError(FrameworkError):
|
||||
@ -186,6 +241,7 @@ class IntegrationOperationNotRegisteredError(FrameworkError):
|
||||
"""
|
||||
|
||||
status_code = 400
|
||||
error_code = "OPERATION_NOT_REGISTERED"
|
||||
|
||||
|
||||
class ExecutionError(FrameworkError):
|
||||
@ -196,6 +252,7 @@ class ExecutionError(FrameworkError):
|
||||
"""
|
||||
|
||||
status_code = 502
|
||||
error_code = "EXECUTION_ERROR"
|
||||
|
||||
|
||||
class RateLimitExceededError(ExecutionError):
|
||||
@ -210,6 +267,7 @@ class RateLimitExceededError(ExecutionError):
|
||||
"""
|
||||
|
||||
status_code = 429
|
||||
error_code = "RATE_LIMIT_EXCEEDED"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -220,7 +278,19 @@ class RateLimitExceededError(ExecutionError):
|
||||
system_id: int | str | None = None,
|
||||
env_key: str | None = None,
|
||||
details: dict[str, Any] | None = None,
|
||||
trace_id: str | None = None,
|
||||
) -> None:
|
||||
"""初始化限流超限异常。
|
||||
|
||||
参数:
|
||||
message: 人类可读错误信息。
|
||||
retry_after: 建议客户端等待秒数(令牌恢复时间)。
|
||||
limit_type: 限流类型,``qps`` 或 ``concurrency``。
|
||||
system_id: 限流维度系统 ID。
|
||||
env_key: 限流维度环境键。
|
||||
details: 业务字段字典,与上述参数合并。
|
||||
trace_id: 调用链路追踪 ID,用于跨链路关联。
|
||||
"""
|
||||
merged: dict[str, Any] = dict(details or {})
|
||||
if retry_after is not None:
|
||||
merged["retry_after"] = retry_after
|
||||
@ -230,7 +300,7 @@ class RateLimitExceededError(ExecutionError):
|
||||
merged["system_id"] = system_id
|
||||
if env_key is not None:
|
||||
merged["env_key"] = env_key
|
||||
super().__init__(message, details=merged)
|
||||
super().__init__(message, details=merged, trace_id=trace_id)
|
||||
|
||||
|
||||
class CircuitOpenError(ExecutionError):
|
||||
@ -244,6 +314,7 @@ class CircuitOpenError(ExecutionError):
|
||||
"""
|
||||
|
||||
status_code = 503
|
||||
error_code = "CIRCUIT_OPEN"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -253,7 +324,18 @@ class CircuitOpenError(ExecutionError):
|
||||
system_id: int | str | None = None,
|
||||
env_key: str | None = None,
|
||||
details: dict[str, Any] | None = None,
|
||||
trace_id: str | None = None,
|
||||
) -> None:
|
||||
"""初始化熔断器打开异常。
|
||||
|
||||
参数:
|
||||
message: 人类可读错误信息。
|
||||
retry_after: 剩余冷却秒数(熔断器进入 half_open 的等待时间)。
|
||||
system_id: 熔断维度系统 ID。
|
||||
env_key: 熔断维度环境键。
|
||||
details: 业务字段字典,与上述参数合并。
|
||||
trace_id: 调用链路追踪 ID,用于跨链路关联。
|
||||
"""
|
||||
merged: dict[str, Any] = dict(details or {})
|
||||
if retry_after is not None:
|
||||
merged["retry_after"] = retry_after
|
||||
@ -261,7 +343,7 @@ class CircuitOpenError(ExecutionError):
|
||||
merged["system_id"] = system_id
|
||||
if env_key is not None:
|
||||
merged["env_key"] = env_key
|
||||
super().__init__(message, details=merged)
|
||||
super().__init__(message, details=merged, trace_id=trace_id)
|
||||
|
||||
|
||||
class SecretResolutionError(FrameworkError):
|
||||
@ -272,6 +354,7 @@ class SecretResolutionError(FrameworkError):
|
||||
"""
|
||||
|
||||
status_code = 400
|
||||
error_code = "SECRET_RESOLUTION_FAILED"
|
||||
|
||||
|
||||
class AuthError(FrameworkError):
|
||||
@ -282,6 +365,7 @@ class AuthError(FrameworkError):
|
||||
"""
|
||||
|
||||
status_code = 403
|
||||
error_code = "AUTH_ERROR"
|
||||
|
||||
|
||||
class QuotaExceededError(ExecutionError):
|
||||
@ -300,6 +384,7 @@ class QuotaExceededError(ExecutionError):
|
||||
"""
|
||||
|
||||
status_code = 429
|
||||
error_code = "QUOTA_EXCEEDED"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -312,7 +397,21 @@ class QuotaExceededError(ExecutionError):
|
||||
system_id: int | str | None = None,
|
||||
env_key: str | None = None,
|
||||
details: dict[str, Any] | None = None,
|
||||
trace_id: str | None = None,
|
||||
) -> None:
|
||||
"""初始化配额耗尽异常。
|
||||
|
||||
参数:
|
||||
message: 人类可读错误信息。
|
||||
quota_key: 配额键。
|
||||
limit_value: 配额上限。
|
||||
used_value: 已用配额。
|
||||
remaining: 剩余配额(通常为 0)。
|
||||
system_id: 配额维度系统 ID。
|
||||
env_key: 配额维度环境键。
|
||||
details: 业务字段字典,与上述参数合并。
|
||||
trace_id: 调用链路追踪 ID,用于跨链路关联。
|
||||
"""
|
||||
merged: dict[str, Any] = dict(details or {})
|
||||
if quota_key is not None:
|
||||
merged["quota_key"] = quota_key
|
||||
@ -326,7 +425,7 @@ class QuotaExceededError(ExecutionError):
|
||||
merged["system_id"] = system_id
|
||||
if env_key is not None:
|
||||
merged["env_key"] = env_key
|
||||
super().__init__(message, details=merged)
|
||||
super().__init__(message, details=merged, trace_id=trace_id)
|
||||
|
||||
|
||||
class AccessDeniedError(FrameworkError):
|
||||
@ -338,6 +437,7 @@ class AccessDeniedError(FrameworkError):
|
||||
"""
|
||||
|
||||
status_code = 403
|
||||
error_code = "ACCESS_DENIED"
|
||||
|
||||
|
||||
class WebhookError(FrameworkError):
|
||||
@ -347,3 +447,4 @@ class WebhookError(FrameworkError):
|
||||
"""
|
||||
|
||||
status_code = 400
|
||||
error_code = "WEBHOOK_ERROR"
|
||||
|
||||
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from yuxi.external_systems.exceptions import DomainValidationError
|
||||
from yuxi.external_systems.framework.adapters.http.models import (
|
||||
RequestBuildState,
|
||||
)
|
||||
@ -34,7 +35,7 @@ class FormRequestBuilder(RequestBuilder):
|
||||
state.body = body_params or None
|
||||
if state.body is not None:
|
||||
if not isinstance(state.body, dict):
|
||||
raise ValueError("form 请求体必须是字典")
|
||||
raise DomainValidationError("form 请求体必须是字典")
|
||||
# 注意:httpx 发送 files 时会自动设置带 boundary 的 Content-Type,
|
||||
# 此处不手动设置,避免 boundary 缺失。
|
||||
state.body = {k: (None, str(v)) for k, v in state.body.items()}
|
||||
|
||||
@ -246,7 +246,8 @@ def _extract_parameters(
|
||||
|
||||
try:
|
||||
input_message = operation.input
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
logger.warning(f"WSDL 解析失败,返回空列表: {exc}")
|
||||
return []
|
||||
|
||||
# 尝试从 body type 的 elements 提取
|
||||
@ -267,14 +268,15 @@ def _extract_parameters(
|
||||
if param is not None:
|
||||
parameters.append(param)
|
||||
return parameters
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as exc:
|
||||
logger.warning(f"WSDL 解析失败,回退到签名解析: {exc}")
|
||||
|
||||
# 兜底:从 signature 字符串提取顶层参数名
|
||||
try:
|
||||
signature = str(input_message.signature())
|
||||
return _parse_signature_parameters(signature)
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
logger.warning(f"WSDL 解析失败,返回空列表: {exc}")
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ from yuxi.external_systems.exceptions import (
|
||||
AuthError,
|
||||
CircuitOpenError,
|
||||
ExecutionError,
|
||||
FrameworkError,
|
||||
QuotaExceededError,
|
||||
RateLimitExceededError,
|
||||
SecretResolutionError,
|
||||
@ -275,7 +276,7 @@ class SystemExecutor:
|
||||
context_builder 未注入时抛 NotImplementedError(过渡方案,见设计方案 §10.2)。
|
||||
"""
|
||||
if self._context_builder is None:
|
||||
raise NotImplementedError("context_builder 未注入,系统级路径暂不可用")
|
||||
raise FrameworkError("context_builder 未注入,系统级路径暂不可用")
|
||||
return await self._context_builder.build(tool, env)
|
||||
|
||||
def _build_hooks(
|
||||
|
||||
@ -28,6 +28,10 @@ from yuxi.external_systems.core.contracts import (
|
||||
SecretResolver,
|
||||
)
|
||||
from yuxi.external_systems.core.ports import SecretRotationPolicyRepository
|
||||
from yuxi.external_systems.exceptions import (
|
||||
DomainValidationError,
|
||||
EntityNotFoundError,
|
||||
)
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
|
||||
|
||||
@ -74,9 +78,9 @@ class SecretRotationSchedulerImpl:
|
||||
"""
|
||||
policy = await self._repo.get_by_id(policy_id)
|
||||
if policy is None:
|
||||
raise ValueError(f"轮换策略不存在: {policy_id}")
|
||||
raise EntityNotFoundError("轮换策略不存在", details={"identifier": policy_id})
|
||||
if policy.rotation_mode == "manual" and new_secret is None:
|
||||
raise ValueError("manual 模式需要手动提供 new_secret")
|
||||
raise DomainValidationError("manual 模式需要手动提供 new_secret")
|
||||
return {"success": True, "policy_id": policy_id, "new_secret": new_secret}
|
||||
|
||||
async def get_overdue_policies(self) -> list[Any]:
|
||||
|
||||
@ -46,6 +46,10 @@ from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from yuxi.external_systems.core.ports import ToolTestCaseRepository
|
||||
from yuxi.external_systems.exceptions import (
|
||||
EntityNotFoundError,
|
||||
FrameworkError,
|
||||
)
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
|
||||
# 执行回调类型:tool_slug + parameters + env_key → 执行结果
|
||||
@ -241,11 +245,11 @@ class TestCaseRunnerImpl:
|
||||
"""
|
||||
callback = execute_callback or self._execute_callback
|
||||
if callback is None:
|
||||
raise ValueError("未提供执行回调,无法运行测试用例")
|
||||
raise FrameworkError("未提供执行回调,无法运行测试用例")
|
||||
|
||||
test_case = await self._repo.get_by_id(test_case_id)
|
||||
if test_case is None:
|
||||
raise ValueError(f"测试用例不存在: {test_case_id}")
|
||||
raise EntityNotFoundError("测试用例不存在", details={"identifier": test_case_id})
|
||||
|
||||
started_at = utc_now_naive()
|
||||
try:
|
||||
|
||||
@ -28,6 +28,7 @@ from yuxi.external_systems.exceptions import (
|
||||
AuthError,
|
||||
DomainValidationError,
|
||||
ExecutionError,
|
||||
FrameworkError,
|
||||
RateLimitExceededError,
|
||||
)
|
||||
from yuxi.external_systems.integrations.dynamics365.error_extractor import (
|
||||
@ -238,7 +239,7 @@ def _ensure_dataverse_product_line(system_config: dict[str, Any]) -> None:
|
||||
if product_line == "dataverse":
|
||||
return
|
||||
if product_line == "finance_operations":
|
||||
raise NotImplementedError("F&O 产品线将在 P1 实现")
|
||||
raise FrameworkError("F&O 产品线将在 P1 实现")
|
||||
raise DomainValidationError(f"未知的 product_line: {product_line}")
|
||||
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ from yuxi.external_systems.exceptions import (
|
||||
AuthError,
|
||||
DomainValidationError,
|
||||
ExecutionError,
|
||||
FrameworkError,
|
||||
RateLimitExceededError,
|
||||
)
|
||||
from yuxi.external_systems.integrations.sap_s4hana.error_extractor import (
|
||||
@ -288,7 +289,7 @@ def _ensure_supported_deployment(system_config: dict[str, Any]) -> str:
|
||||
if deployment == "cloud":
|
||||
return deployment
|
||||
if deployment == "btp":
|
||||
raise NotImplementedError("BTP 部署模式将在 P2 实现")
|
||||
raise FrameworkError("BTP 部署模式将在 P2 实现")
|
||||
raise DomainValidationError(f"未知的 deployment: {deployment}")
|
||||
|
||||
|
||||
|
||||
@ -19,6 +19,10 @@ import re
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from yuxi.external_systems.exceptions import (
|
||||
DomainValidationError,
|
||||
EntityNotFoundError,
|
||||
)
|
||||
from yuxi.external_systems.integrations.slack.constants import (
|
||||
PAGE_SIZE,
|
||||
)
|
||||
@ -69,7 +73,7 @@ class SlackChannelResolver:
|
||||
return await self._resolve_channel_by_name(channel_input[1:])
|
||||
if channel_input.startswith("@"):
|
||||
return await self._resolve_user_by_name(channel_input[1:])
|
||||
raise ValueError(f"无法解析的 channel 输入: {channel_input}(需以 # / @ 开头或为编码 ID)")
|
||||
raise DomainValidationError(f"无法解析的 channel 输入: {channel_input}(需以 # / @ 开头或为编码 ID)")
|
||||
|
||||
async def _resolve_channel_by_name(self, name: str) -> str:
|
||||
"""通过 ``conversations.list`` cursor 分页查找 name 匹配的频道。"""
|
||||
@ -92,7 +96,7 @@ class SlackChannelResolver:
|
||||
cursor = (resp.get("response_metadata") or {}).get("next_cursor", "")
|
||||
if not cursor:
|
||||
break
|
||||
raise ValueError(f"未找到频道: #{name}")
|
||||
raise EntityNotFoundError("频道不存在", details={"identifier": name})
|
||||
|
||||
async def _resolve_user_by_name(self, name: str) -> str:
|
||||
"""通过 ``users.list`` cursor 分页查找 name 匹配的用户。"""
|
||||
@ -112,4 +116,4 @@ class SlackChannelResolver:
|
||||
cursor = (resp.get("response_metadata") or {}).get("next_cursor", "")
|
||||
if not cursor:
|
||||
break
|
||||
raise ValueError(f"未找到用户: @{name}")
|
||||
raise EntityNotFoundError("用户不存在", details={"identifier": name})
|
||||
|
||||
@ -783,8 +783,8 @@ class ImportService(ImportServicePort):
|
||||
return result
|
||||
if isinstance(raw, list):
|
||||
return [d if isinstance(d, dict) else d.model_dump() for d in raw]
|
||||
raise TypeError(
|
||||
f"适配器 generate_from_asset 返回了不支持的类型 {type(raw).__name__},预期 SystemImportDraft 或 list"
|
||||
raise DomainValidationError(
|
||||
f"适配器 generate_from_asset 返回了不支持的类型 {type(raw).__name__}"
|
||||
)
|
||||
|
||||
def _drafts_from_dicts(self, drafts: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
|
||||
@ -25,6 +25,7 @@ from yuxi.external_systems.core.contracts import (
|
||||
from yuxi.external_systems.exceptions import (
|
||||
DomainValidationError,
|
||||
EntityNotFoundError,
|
||||
FrameworkError,
|
||||
)
|
||||
from yuxi.external_systems.use_cases.dto.test_case import (
|
||||
AlertConfigOutput,
|
||||
@ -256,7 +257,7 @@ class TestCaseService(TestCaseServicePort):
|
||||
"""
|
||||
runner = self._runtime.test_case_runner
|
||||
if runner is None:
|
||||
raise RuntimeError("TestCaseRunner 未注入执行回调,无法执行测试用例")
|
||||
raise FrameworkError("TestCaseRunner 未注入执行回调,无法执行测试用例")
|
||||
|
||||
# 预检:验证用例存在(run 内部也会检查,但提前抛 404 更友好)
|
||||
test_case = await self._repos.tool_test_case.get_by_id(input_dto.case_id)
|
||||
@ -317,7 +318,7 @@ class TestCaseService(TestCaseServicePort):
|
||||
|
||||
runner = self._runtime.test_case_runner
|
||||
if runner is None:
|
||||
raise RuntimeError("TestCaseRunner 未注入执行回调,无法执行测试用例")
|
||||
raise FrameworkError("TestCaseRunner 未注入执行回调,无法执行测试用例")
|
||||
|
||||
succeeded = 0
|
||||
failed = 0
|
||||
|
||||
@ -27,6 +27,7 @@ from yuxi.external_systems.core.contracts import (
|
||||
EventPublisher,
|
||||
UnitOfWork,
|
||||
)
|
||||
from yuxi.external_systems.exceptions import FrameworkError
|
||||
from yuxi.external_systems.use_cases.dto.test_case import (
|
||||
ScheduledRegressionOutput,
|
||||
)
|
||||
@ -79,7 +80,7 @@ class TestRegressionService(TestRegressionServicePort):
|
||||
"""
|
||||
runner = self._runtime.test_case_runner
|
||||
if runner is None:
|
||||
raise RuntimeError("TestCaseRunner 未注入执行回调,无法执行测试回归")
|
||||
raise FrameworkError("TestCaseRunner 未注入执行回调,无法执行测试回归")
|
||||
|
||||
due_cases = await self._repos.tool_test_case.list_scheduled()
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user