fix:stage_group

This commit is contained in:
gongwenxin
2025-06-05 18:29:04 +08:00
parent cf0df24530
commit b72406df99
15 changed files with 5303 additions and 5235 deletions
-228
View File
@@ -1,228 +0,0 @@
from typing import List, Dict, Any, Optional, Callable, Union
import datetime
import logging
from enum import Enum
from .test_framework_core import ValidationResult, APIRequestContext, APIResponseContext
from .api_caller.caller import APICallDetail # 需要APICallDetail来记录每个步骤的调用
class ScenarioStepDefinition:
"""定义API场景中的一个单独步骤。"""
def __init__(self,
name: str,
endpoint_spec_lookup_key: str, # 用于从全局API规范中查找端点定义的键
request_overrides: Optional[Dict[str, Any]] = None,
expected_status_codes: Optional[List[int]] = None,
response_assertions: Optional[List[Callable[[APIResponseContext, Dict[str, Any]], List[ValidationResult]]]] = None,
outputs_to_context: Optional[Dict[str, str]] = None):
"""
Args:
name: 步骤的可读名称。
endpoint_spec_lookup_key: 用于查找端点定义的键 (例如 "METHOD /path" 或 YAPI的_id)。
request_overrides: 覆盖默认请求参数的字典。值可以是占位符,如 "{{scenario_context.user_id}}"
支持的键有: "path_params", "query_params", "headers", "body"
expected_status_codes: 预期的HTTP响应状态码列表。如果为None,则不进行特定状态码检查(除非由response_assertions处理)。
response_assertions: 自定义断言函数列表。每个函数接收 APIResponseContext 和 scenario_context,返回 ValidationResult 列表。
outputs_to_context: 从响应中提取数据到场景上下文的字典。
键是存储到场景上下文中的变量名,值是提取路径 (例如 "response.body.data.id")。
"""
self.name = name
self.endpoint_spec_lookup_key = endpoint_spec_lookup_key
self.request_overrides = request_overrides if request_overrides is not None else {}
self.expected_status_codes = expected_status_codes if expected_status_codes is not None else []
self.response_assertions = response_assertions if response_assertions is not None else []
self.outputs_to_context = outputs_to_context if outputs_to_context is not None else {}
self.logger = logging.getLogger(f"scenario.step.{name}")
class BaseAPIScenario:
"""
API场景测试用例的基类。
用户应继承此类来创建具体的测试场景。
"""
# --- 元数据 (由子类定义) ---
id: str = "base_api_scenario"
name: str = "基础API场景"
description: str = "这是一个基础API场景,应由具体场景继承。"
tags: List[str] = []
steps: List[ScenarioStepDefinition] = [] # 子类需要填充此列表
def __init__(self,
global_api_spec: Dict[str, Any], # 完整的API规范字典 (YAPI/Swagger解析后的原始字典)
parsed_api_endpoints: List[Dict[str, Any]], # 解析后的端点列表,用于通过 lookup_key 查找
llm_service: Optional[Any] = None):
"""
初始化API场景。
Args:
global_api_spec: 完整的API规范字典。
parsed_api_endpoints: 从YAPI/Swagger解析出来的端点对象列表(通常是YAPIEndpoint或SwaggerEndpoint的to_dict()结果)。
这些对象应包含用于匹配 `endpoint_spec_lookup_key` 的字段。
llm_service: APITestOrchestrator 传入的 LLMService 实例 (可选)。
"""
self.global_api_spec = global_api_spec
self.parsed_api_endpoints = parsed_api_endpoints # 用于快速查找
self.llm_service = llm_service
self.logger = logging.getLogger(f"scenario.{self.id}")
self.logger.info(f"API场景 '{self.id}' ({self.name}) 已初始化。")
def _get_endpoint_spec_from_global(self, lookup_key: str) -> Optional[Dict[str, Any]]:
"""
根据提供的 lookup_key 从 self.parsed_api_endpoints 中查找并返回端点定义。
查找逻辑可能需要根据 lookup_key 的格式 (例如, YAPI _id, METHOD /path) 进行调整。
简单实现:假设 lookup_key 是 METHOD /path 或 title。
"""
self.logger.debug(f"尝试为场景步骤查找端点: '{lookup_key}'")
for endpoint_data in self.parsed_api_endpoints:
# 尝试匹配 "METHOD /path" 格式 (常见于SwaggerEndpoint)
method_path_key = f"{str(endpoint_data.get('method', '')).upper()} {endpoint_data.get('path', '')}"
if lookup_key == method_path_key:
self.logger.debug(f"通过 'METHOD /path' ('{method_path_key}') 找到端点。")
return endpoint_data
# 尝试匹配 title (常见于YAPIEndpoint)
if lookup_key == endpoint_data.get('title'):
self.logger.debug(f"通过 'title' ('{endpoint_data.get('title')}') 找到端点。")
return endpoint_data
# 尝试匹配 YAPI 的 _id (如果可用)
if str(lookup_key) == str(endpoint_data.get('_id')): # 转换为字符串以确保比较
self.logger.debug(f"通过 YAPI '_id' ('{endpoint_data.get('_id')}') 找到端点。")
return endpoint_data
# 尝试匹配 Swagger/OpenAPI 的 operationId
if lookup_key == endpoint_data.get('operationId'):
self.logger.debug(f"通过 'operationId' ('{endpoint_data.get('operationId')}') 找到端点。")
return endpoint_data
self.logger.warning(f"未能在 parsed_api_endpoints 中找到 lookup_key 为 '{lookup_key}' 的端点。")
return None
def before_scenario(self, scenario_context: Dict[str, Any]):
"""在场景所有步骤执行前调用 (可选,供子类覆盖)"""
self.logger.debug(f"Hook: before_scenario for '{self.id}'")
pass
def after_scenario(self, scenario_context: Dict[str, Any], scenario_result: 'ExecutedScenarioResult'):
"""在场景所有步骤执行后调用 (可选,供子类覆盖)"""
self.logger.debug(f"Hook: after_scenario for '{self.id}'")
pass
def before_step(self, step_definition: ScenarioStepDefinition, scenario_context: Dict[str, Any]):
"""在每个步骤执行前调用 (可选,供子类覆盖)"""
self.logger.debug(f"Hook: before_step '{step_definition.name}' for '{self.id}'")
pass
def after_step(self, step_definition: ScenarioStepDefinition, step_result: 'ExecutedScenarioStepResult', scenario_context: Dict[str, Any]):
"""在每个步骤执行后调用 (可选,供子类覆盖)"""
self.logger.debug(f"Hook: after_step '{step_definition.name}' for '{self.id}'")
pass
class ExecutedScenarioStepResult:
"""存储单个API场景步骤执行后的结果。"""
class Status(str, Enum):
PASSED = "通过"
FAILED = "失败"
ERROR = "执行错误"
SKIPPED = "跳过"
def __init__(self,
step_name: str,
status: Status,
message: str = "",
validation_points: Optional[List[ValidationResult]] = None,
duration: float = 0.0,
api_call_detail: Optional[APICallDetail] = None,
extracted_outputs: Optional[Dict[str, Any]] = None):
self.step_name = step_name
self.status = status
self.message = message
self.validation_points = validation_points if validation_points is not None else []
self.duration = duration
self.api_call_detail = api_call_detail # 存储此步骤的API调用详情
self.extracted_outputs = extracted_outputs if extracted_outputs is not None else {} # 从此步骤提取并存入上下文的值
self.timestamp = datetime.datetime.now()
def to_dict(self) -> Dict[str, Any]:
return {
"step_name": self.step_name,
"status": self.status.value,
"message": self.message,
"duration_seconds": self.duration,
"timestamp": self.timestamp.isoformat(),
"validation_points": [vp.to_dict() if hasattr(vp, 'to_dict') else {"passed": vp.passed, "message": vp.message, "details": vp.details} for vp in self.validation_points],
"api_call_detail": self.api_call_detail.to_dict() if self.api_call_detail and hasattr(self.api_call_detail, 'to_dict') else None,
"extracted_outputs": self.extracted_outputs
}
class ExecutedScenarioResult:
"""存储整个API场景执行后的结果。"""
class Status(str, Enum):
PASSED = "通过" # 所有步骤都通过
FAILED = "失败" # 任何一个步骤失败或出错
SKIPPED = "跳过" # 整个场景被跳过
def __init__(self,
scenario_id: str,
scenario_name: str,
overall_status: Status = Status.SKIPPED,
message: str = ""):
self.scenario_id = scenario_id
self.scenario_name = scenario_name
self.overall_status = overall_status
self.message = message
self.executed_steps: List[ExecutedScenarioStepResult] = []
self.scenario_context_final_state: Dict[str, Any] = {}
self.start_time = datetime.datetime.now()
self.end_time: Optional[datetime.datetime] = None
def add_step_result(self, result: ExecutedScenarioStepResult):
self.executed_steps.append(result)
def finalize_scenario_result(self, final_context: Dict[str, Any]):
self.end_time = datetime.datetime.now()
self.scenario_context_final_state = final_context
if not self.executed_steps and self.overall_status == ExecutedScenarioResult.Status.SKIPPED:
pass # 保持 SKIPPED
elif any(step.status == ExecutedScenarioStepResult.Status.ERROR for step in self.executed_steps):
self.overall_status = ExecutedScenarioResult.Status.FAILED
if not self.message: self.message = "场景中至少一个步骤执行出错。"
elif any(step.status == ExecutedScenarioStepResult.Status.FAILED for step in self.executed_steps):
self.overall_status = ExecutedScenarioResult.Status.FAILED
if not self.message: self.message = "场景中至少一个步骤失败。"
elif all(step.status == ExecutedScenarioStepResult.Status.SKIPPED for step in self.executed_steps) and self.executed_steps:
self.overall_status = ExecutedScenarioResult.Status.SKIPPED # 如果所有步骤都跳过了
if not self.message: self.message = "场景中的所有步骤都被跳过。"
elif not self.executed_steps: # 没有步骤执行,也不是初始的SKIPPED
self.overall_status = ExecutedScenarioResult.Status.FAILED # 或 ERROR
if not self.message: self.message = "场景中没有步骤被执行。"
else: # 所有步骤都通过
self.overall_status = ExecutedScenarioResult.Status.PASSED
if not self.message: self.message = "场景所有步骤成功通过。"
@property
def duration(self) -> float:
if self.start_time and self.end_time:
return (self.end_time - self.start_time).total_seconds()
return 0.0
def to_dict(self) -> Dict[str, Any]:
return {
"scenario_id": self.scenario_id,
"scenario_name": self.scenario_name,
"overall_status": self.overall_status.value,
"message": self.message,
"duration_seconds": f"{self.duration:.2f}",
"start_time": self.start_time.isoformat(),
"end_time": self.end_time.isoformat() if self.end_time else None,
"executed_steps": [step.to_dict() for step in self.executed_steps],
"scenario_context_final_state": self.scenario_context_final_state # 可能包含敏感信息,按需处理
}
def to_json(self, pretty=True) -> str:
import json # 局部导入
indent = 2 if pretty else None
# 对于 scenario_context_final_state,可能需要自定义序列化器来处理复杂对象
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False, default=str)
@@ -1,90 +0,0 @@
import os
import importlib.util
import inspect
import logging
from typing import List, Type, Dict, Optional
from .scenario_framework import BaseAPIScenario # 从新的场景框架模块导入
class ScenarioRegistry:
"""
负责发现、加载和管理所有自定义的 BaseAPIScenario 类。
"""
def __init__(self, scenarios_dir: Optional[str] = None):
"""
初始化 ScenarioRegistry。
Args:
scenarios_dir: 存放自定义API场景 (.py 文件) 的目录路径。如果为None,则不进行发现。
"""
self.scenarios_dir = scenarios_dir
self.logger = logging.getLogger(__name__)
self._registry: Dict[str, Type[BaseAPIScenario]] = {}
self._scenario_classes: List[Type[BaseAPIScenario]] = []
if self.scenarios_dir:
self.discover_scenarios()
else:
self.logger.info("ScenarioRegistry 初始化时未提供 scenarios_dir,跳过场景发现。")
def discover_scenarios(self):
"""
扫描指定目录及其所有子目录,动态导入模块,并注册所有继承自 BaseAPIScenario 的类。
"""
if not self.scenarios_dir or not os.path.isdir(self.scenarios_dir):
self.logger.warning(f"API场景目录不存在或不是一个目录: {self.scenarios_dir}")
return
self.logger.info(f"开始从目录 '{self.scenarios_dir}' 及其子目录发现API场景...")
found_count = 0
for root_dir, _, files in os.walk(self.scenarios_dir):
for filename in files:
if filename.endswith(".py") and not filename.startswith("__"):
module_name = filename[:-3]
file_path = os.path.join(root_dir, filename)
try:
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self.logger.debug(f"成功导入API场景模块: {module_name}{file_path}")
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and issubclass(obj, BaseAPIScenario) and obj is not BaseAPIScenario:
if not hasattr(obj, 'id') or not obj.id:
self.logger.error(f"API场景类 '{obj.__name__}' 在文件 '{file_path}' 中缺少有效的 'id' 属性,已跳过注册。")
continue
if obj.id in self._registry:
self.logger.warning(f"发现重复的API场景 ID: '{obj.id}' (来自类 '{obj.__name__}' in {file_path})。之前的定义将被覆盖。")
self._registry[obj.id] = obj
# 更新 _scenario_classes 列表
existing_class_indices = [i for i, sc_class in enumerate(self._scenario_classes) if sc_class.id == obj.id]
if existing_class_indices:
for index in sorted(existing_class_indices, reverse=True):
del self._scenario_classes[index]
self._scenario_classes.append(obj)
found_count += 1
self.logger.info(f"已注册API场景: '{obj.id}' ({getattr(obj, 'name', 'N/A')}) 来自类 '{obj.__name__}' (路径: {file_path})")
else:
self.logger.error(f"无法为文件 '{file_path}' 创建模块规范 (用于API场景)。")
except ImportError as e:
self.logger.error(f"导入API场景模块 '{module_name}''{file_path}' 失败: {e}", exc_info=True)
except AttributeError as e:
self.logger.error(f"在API场景模块 '{module_name}' ({file_path}) 中查找场景时出错: {e}", exc_info=True)
except Exception as e:
self.logger.error(f"处理API场景文件 '{file_path}' 时发生未知错误: {e}", exc_info=True)
# 场景通常不需要像单个测试用例那样排序执行顺序,除非有特定需求
# 如果需要,可以添加类似 execution_order 的属性并排序
# self._scenario_classes.sort(key=lambda sc_class: (getattr(sc_class, 'execution_order', 100), sc_class.__name__))
self.logger.info(f"API场景发现完成。总共注册了 {len(self._registry)} 个独特的API场景 (基于ID)。发现并加载了 {len(self._scenario_classes)} 个API场景类。")
def get_scenario_by_id(self, scenario_id: str) -> Optional[Type[BaseAPIScenario]]:
"""根据ID获取已注册的API场景类。"""
return self._registry.get(scenario_id)
def get_all_scenario_classes(self) -> List[Type[BaseAPIScenario]]:
"""获取所有已注册的API场景类列表。"""
return list(self._scenario_classes) # 返回副本
+351 -94
View File
@@ -6,6 +6,7 @@ import time
from typing import List, Dict, Any, Callable, Optional, Union
from enum import Enum
from datetime import datetime
from dataclasses import dataclass, field
# Add Pydantic BaseModel for APIOperationSpec
from pydantic import BaseModel
@@ -25,14 +26,145 @@ except ImportError:
logger = logging.getLogger(__name__)
# 定义 APIOperationSpec
class APIOperationSpec(BaseModel):
# Forward declaration for type hinting if ParsedAPISpec is used directly
ParsedAPISpec = "ParsedAPISpec"
@dataclass
class APIOperationSpec:
"""封装了从全局API规范中解析出来的特定API操作的详细信息"""
method: str
path: str
spec: Dict[str, Any] # 原始API端点定义字典
spec: Dict[str, Any] # API操作的完整OpenAPI/Swagger规范字典部分
operation_id: Optional[str] = None
summary: Optional[str] = None
description: Optional[str] = None
tags: List[str] = field(default_factory=list)
@dataclass
class StageStepDefinition:
"""定义测试阶段中的单个步骤"""
name: str
endpoint_spec_lookup_key: str # 用于在API规范中查找此步骤对应的端点
request_overrides: Dict[str, Any] = field(default_factory=dict)
expected_status_codes: List[int] = field(default_factory=lambda: [200, 201])
# Corrected type hint for response_assertions
response_assertions: List[Callable[[Any, Dict[str, Any]], ValidationResult]] = field(default_factory=list) # response_context, stage_context -> ValidationResult
outputs_to_context: Dict[str, str] = field(default_factory=dict) # 从响应提取到阶段上下文的映射
description: Optional[str] = None
order: int = 0 # ADDED order attribute
class ExecutedStageStepResult:
"""存储单个测试阶段步骤的执行结果"""
class Status(str, Enum):
PENDING = "待定"
PASSED = "通过"
FAILED = "失败"
ERROR = "执行错误"
SKIPPED = "跳过"
def __init__(self,
step_name: str,
status: Status = Status.PENDING,
message: str = "",
lookup_key: Optional[str] = None,
resolved_endpoint: Optional[str] = None,
request_details: Optional[Dict[str, Any]] = None,
api_call_details: Optional[Dict[str, Any]] = None, # 存储 APICallDetail.to_dict() 的结果
validation_points: Optional[List[Dict[str, Any]]] = None, # 存储 ValidationResult.to_dict() 的结果列表
duration_seconds: float = 0.0,
context_after_step: Optional[Dict[str, Any]] = None,
description: Optional[str] = None):
self.step_name = step_name
self.description = description
self.status = status
self.message = message
self.lookup_key = lookup_key
self.resolved_endpoint = resolved_endpoint
self.request_details = request_details if request_details is not None else {}
self.api_call_details = api_call_details if api_call_details is not None else {}
self.validation_points = validation_points if validation_points is not None else []
self.duration_seconds = duration_seconds
self.context_after_step = context_after_step if context_after_step is not None else {}
self.timestamp: datetime = datetime.now()
def to_dict(self) -> Dict[str, Any]:
return {
"step_name": self.step_name,
"description": self.description,
"status": self.status.value,
"message": self.message,
"lookup_key": self.lookup_key,
"resolved_endpoint": self.resolved_endpoint,
"duration_seconds": f"{self.duration_seconds:.4f}",
"timestamp": self.timestamp.isoformat(),
"request_details": self.request_details,
"api_call_details": self.api_call_details,
"validation_points": self.validation_points,
"context_snapshot_after_step": self.context_after_step # 可以选择是否完整记录上下文
}
class ExecutedStageResult:
"""存储整个测试阶段的执行结果"""
class Status(str, Enum):
PENDING = "待定"
PASSED = "通过"
FAILED = "失败"
ERROR = "执行错误"
SKIPPED = "跳过"
def __init__(self,
stage_id: str,
stage_name: str,
description: Optional[str] = None,
api_group_metadata: Optional[Dict[str, Any]] = None, # 存储阶段应用到的API分组元数据
tags: Optional[List[str]] = None, # <-- Added tags parameter
overall_status: Status = Status.PENDING,
message: str = ""):
self.stage_id = stage_id
self.stage_name = stage_name
self.description = description
self.api_group_metadata = api_group_metadata
self.tags = tags if tags is not None else [] # <-- Store tags
self.overall_status = overall_status
self.message = message
self.executed_steps: List[ExecutedStageStepResult] = []
self.final_context_snapshot: Optional[Dict[str, Any]] = None
self.start_time: datetime = datetime.now()
self.end_time: Optional[datetime] = None
self.duration: float = 0.0
def add_step_result(self, step_result: ExecutedStageStepResult):
self.executed_steps.append(step_result)
def finalize_stage_result(self, final_context: Optional[Dict[str, Any]] = None): # Renamed from finalize_result
self.end_time = datetime.now()
self.duration = (self.end_time - self.start_time).total_seconds()
self.final_context_snapshot = final_context if final_context is not None else {}
# 确定最终状态的逻辑可以放在这里,或者由编排器在调用后设置
if not self.executed_steps and self.overall_status == ExecutedStageResult.Status.PENDING:
self.overall_status = ExecutedStageResult.Status.SKIPPED # 如果没有步骤执行,且状态未被其他方式设置
self.message = self.message or "阶段中没有步骤被执行或所有步骤被跳过。"
# 更复杂的最终状态判定可能需要编排器逻辑
def to_dict(self) -> Dict[str, Any]:
return {
"stage_id": self.stage_id,
"stage_name": self.stage_name,
"description": self.description,
"api_group_metadata": self.api_group_metadata,
"tags": self.tags, # <-- Added tags to output
"overall_status": self.overall_status.value,
"message": self.message,
"duration_seconds": f"{self.duration:.2f}",
"start_time": self.start_time.isoformat(),
"end_time": self.end_time.isoformat() if self.end_time else None,
"executed_steps": [step.to_dict() for step in self.executed_steps],
"final_context_snapshot": self.final_context_snapshot
}
# 默认的操作类型关键字映射
# 键是标准化的操作类型,值是可能出现在API标题中的关键字列表
@@ -45,31 +177,6 @@ DEFAULT_OPERATION_KEYWORDS: Dict[str, List[str]] = {
# 可以根据需要添加更多通用操作类型
}
class StageStepDefinition:
"""定义API测试阶段中的单个步骤。"""
def __init__(self,
name: str,
endpoint_spec_lookup_key: Union[str, Dict[str, str]], # 例如 "GET /pets/{petId}" 或 {"method": "GET", "path": "/pets/{petId}"} 或操作类型 "add"
description: Optional[str] = None, # <--- 添加 description 参数
request_overrides: Optional[Dict[str, Any]] = None,
expected_status_codes: Optional[List[int]] = None,
response_assertions: Optional[List[Callable[[APIResponseContext, Dict[str, Any]], List[ValidationResult]]]] = None,
outputs_to_context: Optional[Dict[str, str]] = None,
order: int = 0): # 新增执行顺序
self.name = name
self.endpoint_spec_lookup_key = endpoint_spec_lookup_key
self.description = description # <--- 设置 description 属性
self.request_overrides = request_overrides or {}
self.expected_status_codes = expected_status_codes or []
self.response_assertions = response_assertions or []
self.outputs_to_context = outputs_to_context or {}
self.order = order
if not isinstance(self.endpoint_spec_lookup_key, (str, dict)):
raise ValueError("StageStepDefinition: endpoint_spec_lookup_key must be a string (operation type or method/path) or a dict {'method': 'X', 'path': 'Y'}")
class BaseAPIStage:
"""
API测试阶段的基类。
@@ -84,22 +191,33 @@ class BaseAPIStage:
# 由子类定义,表示此Stage中的API调用步骤
steps: List[StageStepDefinition] = []
# 新增: 控制当此阶段不适用于任何API分组时,是否标记为失败
fail_if_not_applicable_to_any_group: bool = False
# 新增: 控制当一个步骤失败/错误时,是否继续执行后续步骤
continue_on_failure: bool = False
def __init__(self,
api_group_metadata: Dict[str, Any],
apis_in_group: List[Dict[str, Any]], # 当前分组内所有API的定义列表 (字典格式)
apis_in_group: List[Union[YAPIEndpoint, SwaggerEndpoint]], # MODIFIED TYPE HINT
llm_service: Optional[LLMService] = None,
global_api_spec: Optional[ParsedAPISpec] = None, # <--- 修改类型注解
operation_keywords: Optional[Dict[str, List[str]]] = None):
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
self.current_api_group_metadata = api_group_metadata
self.current_apis_in_group = apis_in_group # 这些应该是已经解析好的API定义字典
self.api_group_metadata = api_group_metadata # Let's ensure this common name is also available.
self.apis_in_group = apis_in_group # CORRECTED attribute name
self.global_api_spec: ParsedAPISpec = global_api_spec
self.llm_service = llm_service
self.global_api_spec = global_api_spec # 保留 Optional[ParsedAPISpec]
self._operation_keywords = operation_keywords or DEFAULT_OPERATION_KEYWORDS
self._operation_keywords = operation_keywords if operation_keywords is not None else {} # CORRECTED assignment
self._matched_endpoints: Dict[str, Dict[str, Any]] = {} # 存储按操作类型匹配到的端点定义
self._match_api_endpoints_in_group() # 初始化时自动匹配
# Calling _match_api_endpoints_in_group() here might be problematic if it relies on
# subclass-specific setup or if it's intended to be called by the subclass.
# For now, let's comment it out from the base __init__ to avoid potential errors
# until its role and safety here are confirmed.
# self._match_api_endpoints_in_group()
# 确保子类定义了ID
if not self.id:
@@ -107,22 +225,127 @@ class BaseAPIStage:
self.logger.warning(f"BaseAPIStage subclass '{self.__class__.__name__}' does not have an 'id' attribute defined. Defaulting to class name: '{self.id}'. It is recommended to set a unique id.")
# 对步骤进行排序
self.steps = sorted(self.steps, key=lambda s: s.order)
if self.steps: # Ensure steps exist before trying to sort
self.steps = sorted(self.steps, key=lambda s: s.order)
else:
self.steps = []
def get_api_spec_for_operation(self,
lookup_key: str,
global_api_spec: ParsedAPISpec, # 确保类型正确
api_group_name: Optional[str] = None
) -> Optional[APIOperationSpec]:
"""
根据查找键从提供的API规范中获取特定API操作的详细信息。
这个方法需要子类根据其查找逻辑来实现,或者提供一个通用的基于operationId或path+method的查找。
def is_applicable_to_api_group(self, api_group_metadata: Dict[str, Any], apis_in_group: List[Dict[str, Any]]) -> bool:
Args:
lookup_key: 用于查找API操作的键 (例如 operationId, "METHOD /path", 或自定义键)。
global_api_spec: 完整的已解析API规范对象。
api_group_name: (可选) 当前API分组的名称,用于更精确的查找或作用域限定。
Returns:
APIOperationSpec 对象如果找到,否则 None。
"""
self.logger.debug(f"Attempting to find API spec for lookup_key='{lookup_key}', api_group='{api_group_name}'")
# 尝试基于 operationId 查找 (如果 lookup_key 看起来像 operationId)
# 简单的启发式:不包含空格和斜杠的键可能是 operationId
if ' ' not in lookup_key and '/' not in lookup_key:
for endpoint in global_api_spec.endpoints:
endpoint_dict = endpoint.to_dict() # 将YAPIEndpoint或SwaggerEndpoint转换为标准化字典
op_id = endpoint_dict.get('operationId')
if op_id == lookup_key:
# 检查此端点是否属于当前API分组 (如果提供了api_group_name)
if api_group_name:
tags = endpoint_dict.get('tags', [])
if api_group_name not in tags:
self.logger.debug(f"Endpoint with operationId '{lookup_key}' found, but not in group '{api_group_name}'. Tags: {tags}")
continue # 不在当前组,继续查找
self.logger.info(f"Found API for operationId '{lookup_key}' (group: {api_group_name}). Path: {endpoint_dict.get('path')}")
return APIOperationSpec(
method=endpoint_dict.get('method','').upper(),
path=endpoint_dict.get('path',''),
spec=endpoint_dict, # 传递整个端点字典作为规范
operation_id=op_id,
summary=endpoint_dict.get('summary'),
description=endpoint_dict.get('description'),
tags=endpoint_dict.get('tags', [])
)
# 尝试基于 "METHOD /path" 格式的 lookup_key 查找
# (这是一个更通用的查找方式,但可能需要更仔细的路径匹配逻辑)
parts = lookup_key.split(' ', 1)
if len(parts) == 2:
method_to_find = parts[0].upper()
path_to_find = parts[1]
for endpoint in global_api_spec.endpoints:
endpoint_dict = endpoint.to_dict()
if endpoint_dict.get('method','').upper() == method_to_find and endpoint_dict.get('path','') == path_to_find:
if api_group_name:
tags = endpoint_dict.get('tags', [])
if api_group_name not in tags:
self.logger.debug(f"Endpoint '{lookup_key}' found, but not in group '{api_group_name}'. Tags: {tags}")
continue
self.logger.info(f"Found API for method/path '{lookup_key}' (group: {api_group_name}).")
return APIOperationSpec(
method=method_to_find,
path=path_to_find,
spec=endpoint_dict,
operation_id=endpoint_dict.get('operationId'),
summary=endpoint_dict.get('summary'),
description=endpoint_dict.get('description'),
tags=endpoint_dict.get('tags', [])
)
self.logger.warning(f"Could not find API operation spec for lookup_key: '{lookup_key}' (api_group: '{api_group_name}') using default search logic (operationId or METHOD /path). Consider overriding get_api_spec_for_operation in your Stage class for custom lookup logic.")
return None
# --- 生命周期钩子 ---
def before_stage(self, stage_context: Dict[str, Any], global_api_spec: ParsedAPISpec, api_group_name: Optional[str]):
"""在测试阶段所有步骤执行之前调用。"""
self.logger.debug(f"Executing before_stage for stage '{self.id}', group '{api_group_name}'. Initial context: {stage_context}")
pass
def after_stage(self, stage_result: ExecutedStageResult, stage_context: Dict[str, Any], global_api_spec: ParsedAPISpec, api_group_name: Optional[str]):
"""在测试阶段所有步骤执行完毕之后调用(无论成功、失败或错误)。"""
self.logger.debug(f"Executing after_stage for stage '{self.id}', group '{api_group_name}'. Result status: {stage_result.overall_status.value}. Final context: {stage_context}")
pass
def before_step(self, step: StageStepDefinition, stage_context: Dict[str, Any], global_api_spec: ParsedAPISpec, api_group_name: Optional[str]):
"""在每个测试步骤执行之前调用。"""
self.logger.debug(f"Executing before_step for stage '{self.id}', step '{step.name}', group '{api_group_name}'. Current context: {stage_context}")
pass
def after_step(self, step: StageStepDefinition, step_result: ExecutedStageStepResult, stage_context: Dict[str, Any], global_api_spec: ParsedAPISpec, api_group_name: Optional[str]):
"""在每个测试步骤执行之后调用(无论成功、失败或错误)。"""
self.logger.debug(f"Executing after_step for stage '{self.id}', step '{step.name}', group '{api_group_name}'. Step status: {step_result.status.value}. Context after step: {stage_context}")
pass
def is_applicable_to_api_group(self, api_group_name: Optional[str], global_api_spec: ParsedAPISpec) -> bool:
"""
判断此测试阶段是否适用于给定的API分组。
子类可以重写此方法以实现更复杂的适用性逻辑。
Args:
api_group_metadata: API分组的元数据 (例如 YAPI category name/id 或 Swagger tag name/description)
apis_in_group: 该分组内的API端点定义列表。
Returns:
True 如果此阶段适用于该API分组,否则 False。
默认情况下,如果阶段没有定义特定的适用性逻辑(例如,通过`tags`属性或覆盖此方法),
它将适用于所有API分组(如果 `api_group_name` 是 `None`,表示全局应用)或者
适用于名称与阶段 `tags` 中任何一个匹配的API分组。
子类可以覆盖此方法以实现更复杂的适用性判断逻辑
"""
return True # 默认应用于所有分组
if not self.tags: # 如果阶段没有定义任何标签
self.logger.debug(f"Stage '{self.id}' has no specific tags defined. Applying to group '{api_group_name}' by default (true if group is None, or if specific logic isn't overridden to be more restrictive).")
return True # 默认适用于所有分组或全局
if api_group_name is None:
# 如果阶段有标签,但当前评估的是全局应用场景 (api_group_name is None)
# 那么它通常不应该在全局应用,除非设计者明确希望如此。
# 这里的行为可以调整:如果阶段有标签,它是否还应该应用于"全局"?
# 当前逻辑:如果阶段有标签,它只应用于匹配这些标签的分组。
self.logger.debug(f"Stage '{self.id}' has tags {self.tags}, but is being checked against global scope (api_group_name is None). Defaulting to not applicable to global if tags are present.")
return False
applicable = api_group_name in self.tags
self.logger.debug(f"Stage '{self.id}' (tags: {self.tags}) applicability to API group '{api_group_name}': {applicable}")
return applicable
def _match_api_endpoints_in_group(self):
"""
@@ -212,6 +435,10 @@ class BaseAPIStage:
if not match_found and hasattr(endpoint_obj, 'operation_id') and endpoint_obj.operation_id == lookup_key:
match_found = True
# 4. Check by YAPI _id (convert to string for safe comparison)
if not match_found and hasattr(endpoint_obj, '_id') and endpoint_obj._id is not None and str(endpoint_obj._id) == str(lookup_key):
match_found = True
if match_found:
self.logger.info(f"'{self.id}': 找到匹配操作 '{lookup_key}' -> Method: {endpoint_obj.method}, Path: {endpoint_obj.path}")
@@ -305,7 +532,7 @@ class ExecutedStageStepResult:
FAILED = "失败"
ERROR = "执行错误"
SKIPPED = "跳过"
PENDING = "处理中" # 新增:表示步骤正在等待或预处理
PENDING = "处理中"
def __init__(self,
step_name: str,
@@ -313,14 +540,14 @@ class ExecutedStageStepResult:
message: str = "",
validation_points: Optional[List[ValidationResult]] = None,
duration: float = 0.0,
api_call_detail: Optional[APICallDetail] = None, # 记录此步骤的API调用详情
api_call_detail: Optional[APICallDetail] = None,
extracted_outputs: Optional[Dict[str, Any]] = None,
description: Optional[str] = None, # <--- 添加 description
lookup_key: Optional[Union[str, Dict[str, str]]] = None, # <--- 添加 lookup_key
resolved_endpoint: Optional[str] = None, # <--- 添加 resolved_endpoint
request_details: Optional[Dict[str, Any]] = None, # <--- 添加 request_details
context_after_step: Optional[Dict[str, Any]] = None # <--- 添加 context_after_step
):
description: Optional[str] = None,
lookup_key: Optional[Union[str, Dict[str, str]]] = None,
resolved_endpoint: Optional[str] = None,
request_details: Optional[Dict[str, Any]] = None,
context_after_step: Optional[Dict[str, Any]] = None
):
self.step_name = step_name
self.status = status
self.message = message
@@ -329,52 +556,77 @@ class ExecutedStageStepResult:
self.timestamp = time.time()
self.api_call_detail = api_call_detail
self.extracted_outputs = extracted_outputs or {}
self.description = description # <--- 设置属性
self.lookup_key = lookup_key # <--- 设置属性
self.resolved_endpoint = resolved_endpoint # <--- 设置属性
self.request_details = request_details # <--- 设置属性
self.context_after_step = context_after_step # <--- 设置属性
self.description = description
self.lookup_key = lookup_key
self.resolved_endpoint = resolved_endpoint
self.request_details = request_details
self.context_after_step = context_after_step
def finalize_step_result(self):
"""如果步骤的主消息为空,则根据验证点结果更新它。"""
if not self.message and hasattr(self, 'validation_points') and self.validation_points:
failed_vp_messages = [
vp.message for vp in self.validation_points
if isinstance(vp, ValidationResult) and not vp.passed and vp.message
]
if failed_vp_messages:
self.message = "; ".join(failed_vp_messages)
def to_dict(self) -> Dict[str, Any]:
vps_details = []
if self.validation_points: # self.validation_points is List[Dict[str, Any]]
for vp_dict in self.validation_points: # vp_dict is a Dict from ValidationResult.to_dict()
# Access dictionary keys instead of object attributes
vp_details_content = vp_dict.get('details')
vp_passed = vp_dict.get('passed', False)
vp_message = vp_dict.get('message', '')
processed_detail = {"passed": vp_passed, "message": vp_message}
if vp_details_content and isinstance(vp_details_content, dict):
try:
#只取部分关键信息或确保可序列化
if "status_code" in vp_details_content:
processed_detail["status_code"] = vp_details_content["status_code"]
# 不直接序列化整个 response body 以免过大
# You might want to add other relevant serializable fields from vp_details_content
except TypeError:
# Update message if details were not serializable, though this part might be less reachable now
processed_detail["message"] = f"{vp_message} (Details not fully serializable)"
vps_details_for_output = []
if self.validation_points: # self.validation_points is List[ValidationResult]
for vp_obj in self.validation_points: # vp_obj is a ValidationResult object
if not isinstance(vp_obj, ValidationResult): # Defensive check
logger.warning(f"Step '{self.step_name}': Found non-ValidationResult item in validation_points: {type(vp_obj)}")
continue
vps_details.append(processed_detail)
processed_detail = {"passed": vp_obj.passed, "message": vp_obj.message}
details_content = getattr(vp_obj, 'details', None)
if details_content and isinstance(details_content, dict):
try:
# 只取部分关键信息或确保可序列化
if "status_code" in details_content:
processed_detail["status_code_in_validation"] = details_content["status_code"]
# 可以添加其他从 details_content 中提取的可序列化字段
except TypeError:
# 如果 details 无法完全序列化,更新消息
processed_detail["message"] = f"{vp_obj.message} (Details not fully serializable)"
elif details_content: # 如果 details 不是字典但存在
processed_detail["details_type"] = type(details_content).__name__
vps_details_for_output.append(processed_detail)
# 如果 finalize_step_result 已经被调用,self.message 可能已经更新
# 否则,这里的逻辑会再次尝试整合 (如果 self.message 仍然为空)
current_message = self.message
if not current_message and self.validation_points:
failed_vp_messages = [
vp_obj.message for vp_obj in self.validation_points
if isinstance(vp_obj, ValidationResult) and not vp_obj.passed and vp_obj.message
]
if failed_vp_messages:
current_message = "; ".join(failed_vp_messages)
return {
"step_name": self.step_name,
"description": self.description, # <--- 添加到输出
"lookup_key": self.lookup_key if isinstance(self.lookup_key, str) else str(self.lookup_key), # <--- 添加到输出 (确保字符串化)
"resolved_endpoint": self.resolved_endpoint, # <--- 添加到输出
"description": self.description,
"lookup_key": str(self.lookup_key) if self.lookup_key is not None else None,
"resolved_endpoint": self.resolved_endpoint,
"status": self.status.value,
"message": self.message or "; ".join([vp_dict.get('message', '') for vp_dict in self.validation_points if not vp_dict.get('passed')]),
"message": current_message, # 使用当前或整合后的消息
"duration_seconds": f"{self.duration:.4f}",
"timestamp": time.strftime('%Y-%m-%dT%H:%M:%S%z', time.localtime(self.timestamp)),
"validation_points": vps_details,
"api_call_curl": self.api_call_detail.curl_command if self.api_call_detail else None,
"request_details": self.request_details, # <--- 添加到输出
"extracted_outputs": {k: str(v)[:200] + '...' if isinstance(v, (str, bytes)) and len(v) > 200 else v
"validation_points": vps_details_for_output,
"api_call_curl": getattr(getattr(self, 'api_call_detail', None), 'curl_command', 'N/A'),
"request_details": self.request_details,
"extracted_outputs": {k: str(v)[:200] + '...' if isinstance(v, (str, bytes)) and len(v) > 200 else v
for k, v in self.extracted_outputs.items()},
"context_after_step_summary": {k: str(v)[:50] + '...' if isinstance(v, str) and len(v) > 50 else (type(v).__name__ if not isinstance(v, (str, int, float, bool, list, dict)) else v) for k,v in (self.context_after_step or {}).items()} # <--- 添加到输出 (摘要)
"context_after_step_summary": {
k: str(v)[:50] + '...' if isinstance(v, str) and len(v) > 50 else (
type(v).__name__ if not isinstance(v, (str, int, float, bool, list, dict, type(None))) else v
) for k,v in (self.context_after_step or {}).items()
}
}
@@ -391,25 +643,29 @@ class ExecutedStageResult:
stage_id: str,
stage_name: str,
api_group_metadata: Optional[Dict[str, Any]] = None,
description: Optional[str] = None): # <--- 添加 description 参数
description: Optional[str] = None, # <--- 添加 description 参数
tags: Optional[List[str]] = None, # <-- Added tags parameter
overall_status: Status = Status.PENDING,
message: str = ""):
self.stage_id = stage_id
self.stage_name = stage_name
self.description = description
self.api_group_metadata = api_group_metadata
self.overall_status: ExecutedStageResult.Status = ExecutedStageResult.Status.PENDING
self.start_time: datetime = datetime.now()
self.end_time: Optional[datetime] = None
self.duration: float = 0.0
self.message: Optional[str] = None
self.tags = tags if tags is not None else [] # <-- Store tags
self.overall_status = overall_status
self.message = message
self.executed_steps: List[ExecutedStageStepResult] = [] # 确保初始化为空列表
self.final_context: Optional[Dict[str, Any]] = None
# executed_steps_count 应该是一个属性,或者在 to_dict 中计算
self.start_time: datetime = datetime.now() # Corrected
self.end_time: Optional[datetime] = None # Corrected type hint
self.duration: float = 0.0
def add_step_result(self, step_result: ExecutedStageStepResult):
self.executed_steps.append(step_result)
def finalize_stage_result(self, final_context: Optional[Dict[str, Any]] = None):
self.end_time = datetime.now()
self.end_time = datetime.now() # Corrected
self.duration = (self.end_time - self.start_time).total_seconds()
self.final_context = final_context
@@ -452,6 +708,7 @@ class ExecutedStageResult:
"stage_name": self.stage_name,
"description": self.description, # <--- 添加 description 到输出
"api_group_name": self.api_group_metadata.get("name", "N/A"),
"tags": self.tags, # <-- Added tags to output
"overall_status": self.overall_status.value,
"duration_seconds": f"{self.duration:.2f}",
"start_time": self.start_time.strftime('%Y-%m-%dT%H:%M:%S%z'),
+70 -75
View File
@@ -6,6 +6,7 @@ import importlib.util
import inspect
import logging
from typing import List, Type, Dict, Optional
from pathlib import Path
from .stage_framework import BaseAPIStage # 导入新的 BaseAPIStage
@@ -13,111 +14,105 @@ logger = logging.getLogger(__name__)
class StageRegistry:
"""
负责发现、加载和管理 BaseAPIStage 子类
负责发现、加载和管理所有API测试阶段 (stages)
"""
def __init__(self, stages_dir: Optional[str] = None):
"""
初始化 StageRegistry。
Args:
stages_dir: 存放自定义 BaseAPIStage Python文件的目录路径。
如果为 None 或无效路径,则不会加载任何自定义阶段。
stages_dir: 存放自定义 APIStage Python文件的目录路径。
如果为 None,则不会加载任何阶段。
"""
self.logger = logging.getLogger(__name__)
self.stages_dir = stages_dir
self.stages_dir: Optional[Path] = Path(stages_dir) if stages_dir else None
self._stages: Dict[str, Type[BaseAPIStage]] = {}
self._errors: List[str] = []
if self.stages_dir and os.path.isdir(self.stages_dir):
self.logger.info(f"StageRegistry: 开始从目录 '{self.stages_dir}' 加载测试阶段...")
self._discover_and_load_stages()
if self._errors:
for error in self._errors:
self.logger.error(f"StageRegistry: 加载阶段时发生错误: {error}")
self.logger.info(f"StageRegistry: 加载完成。共加载 {len(self._stages)} 个测试阶段。")
elif stages_dir: # 如果提供了stages_dir但不是有效目录
self.logger.warning(f"StageRegistry: 提供的阶段目录 '{stages_dir}' 无效或不存在。将不会加载任何自定义阶段。")
else: # 如果 stages_dir 未提供
self.logger.info("StageRegistry: 未提供阶段目录,将不会加载任何自定义阶段。")
if self.stages_dir:
if not self.stages_dir.is_dir():
logger.warning(f"指定的阶段目录不存在或不是一个目录: {self.stages_dir}")
# Optionally, you could raise an error here or handle it as no stages found.
# For now, it will just result in no stages being loaded.
self.stages_dir = None # Prevent further processing if dir is invalid
else:
self.discover_stages()
else:
logger.info("没有提供阶段目录,将不加载任何自定义测试阶段。")
def _discover_and_load_stages(self):
"""发现并加载指定目录下的所有 BaseAPIStage 子类。"""
if not self.stages_dir or not os.path.isdir(self.stages_dir):
self.logger.warning(f"StageRegistry: 阶段目录 '{self.stages_dir}' 无效或不存在,无法发现阶段")
def discover_stages(self):
"""从指定的目录中发现并加载所有 BaseAPIStage 子类。"""
if not self.stages_dir or not self.stages_dir.is_dir():
logger.debug("阶段目录未设置或无效,跳过阶段发现")
return
self.logger.info(f"StageRegistry: 开始从目录 '{self.stages_dir}' 及其子目录发现测试阶段...")
found_count = 0
# 使用 os.walk 进行递归扫描
for root_dir, _, files in os.walk(self.stages_dir):
self._stages = {}
self._errors = []
logger.info(f"开始从目录发现测试阶段: {self.stages_dir}")
for root, _, files in os.walk(self.stages_dir):
for filename in files:
if filename.endswith(".py") and not filename.startswith("__"):
module_name = filename[:-3]
module_path = os.path.join(root_dir, filename) # 使用 root_dir
if filename.endswith('.py') and not filename.startswith('_'):
file_path = Path(root) / filename
module_name = f"ddms_compliance_suite.stages.{file_path.stem}" # 可以根据需要调整模块命名
try:
spec = importlib.util.spec_from_file_location(module_name, module_path)
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self.logger.debug(f"StageRegistry: 成功导入模块: {module_name}{module_path}")
logger.debug(f"成功加载模块: {module_name}{file_path}")
for name, cls in inspect.getmembers(module, inspect.isclass):
if issubclass(cls, BaseAPIStage) and cls is not BaseAPIStage:
if not hasattr(cls, 'id') or not cls.id: # 检查id属性是否存在且不为空
stage_id = cls.__name__
self.logger.warning(f"测试阶段类 '{cls.__name__}' (在模块 '{module_name}' 来自路径 '{module_path}') 未定义有效 'id' 属性,将使用类名 '{stage_id}' 作为其ID。建议为每个阶段设置唯一的 'id'")
else:
if hasattr(cls, 'id') and cls.id:
stage_id = cls.id
if stage_id in self._stages:
self.logger.warning(f"重复的测试阶段ID '{stage_id}' (来自类 '{cls.__name__}' 在模块 '{module_name}' 来自路径 '{module_path}')。之前的定义将被覆盖。请确保阶段ID唯一。")
self._stages[stage_id] = cls
found_count +=1
self.logger.info(f"StageRegistry: 已注册测试阶段: '{stage_id}' (类: {cls.__name__}) 从模块 '{module_name}' (路径: {module_path})")
if stage_id in self._stages:
warning_msg = f"发现重复的阶段ID '{stage_id}'{file_path}。已加载的阶段来自 {self._stages[stage_id].__module__}。将被忽略。"
logger.warning(warning_msg)
self._errors.append(warning_msg)
else:
self._stages[stage_id] = cls
logger.info(f"成功注册测试阶段: {stage_id} (来自 {module_name}.{name})")
else:
warning_msg = f"{file_path} 中发现的类 {name} 继承自 BaseAPIStage 但缺少 'id' 属性或ID为空。将被忽略。"
logger.warning(warning_msg)
self._errors.append(warning_msg)
else:
self._errors.append(f"无法为文件 '{module_path}' 创建模块规范。")
self.logger.error(f"StageRegistry: 无法为文件 '{module_path}' 创建模块规范。")
error_msg = f"无法为文件创建模块规范: {file_path}"
logger.error(error_msg)
self._errors.append(error_msg)
except ImportError as e:
error_msg = f"导入模块 '{module_name}' (从 '{module_path}') 失败: {e}"
error_msg = f"导入模块 {module_name}{file_path} 失败: {e}"
logger.error(error_msg, exc_info=True)
self._errors.append(error_msg)
self.logger.error(f"StageRegistry: {error_msg}", exc_info=True)
except Exception as e:
error_msg = f"加载或检查模块 '{module_name}' (从 '{module_path}') 时发生未知错误: {e}"
error_msg = f"加载或检查文件 {file_path} 时发生未知错误: {e}"
logger.error(error_msg, exc_info=True)
self._errors.append(error_msg)
self.logger.error(f"StageRegistry: {error_msg}", exc_info=True)
if found_count == 0 and not self._errors:
self.logger.info(f"StageRegistry: 在 '{self.stages_dir}' 及其子目录中未找到符合条件的测试阶段文件或类。")
elif self._errors:
self.logger.warning(f"StageRegistry: 测试阶段发现过程中遇到 {len(self._errors)} 个错误。请检查日志。")
# 注意:StageRegistry 目前没有像 TestCaseRegistry 那样的排序逻辑,
# 如果需要按特定顺序执行 Stages (独立于 API group),未来可以添加。
logger.info(f"测试阶段发现完成。共加载 {len(self._stages)} 个阶段。发现 {len(self._errors)} 个错误。")
def get_stage_class_by_id(self, stage_id: str) -> Optional[Type[BaseAPIStage]]:
"""根据ID获取已注册的测试阶段类。"""
return self._stages.get(stage_id)
def get_all_stage_classes(self) -> List[Type[BaseAPIStage]]:
"""获取所有已注册的测试阶段类的列表。"""
def get_all_stages(self) -> List[Type[BaseAPIStage]]:
"""返回所有已发现和加载的测试阶段类的列表"""
return list(self._stages.values())
def get_load_errors(self) -> List[str]:
"""获取加载过程中发生的错误信息列表"""
def get_stage_by_id(self, stage_id: str) -> Optional[Type[BaseAPIStage]]:
"""根据ID获取特定的测试阶段类"""
return self._stages.get(stage_id)
def get_discovery_errors(self) -> List[str]:
"""返回在发现过程中遇到的任何错误信息。"""
return self._errors
def clear_stages(self):
"""清空所有已加载的阶段和错误信息。"""
self._stages = {}
self._errors = []
logger.debug("StageRegistry 已清空。")
def reload_stages(self):
"""
重新加载所有测试阶段。会清空当前已加载的阶段和错误记录。
"""
self.logger.info(f"StageRegistry: 正在从目录 '{self.stages_dir}' 重新加载所有测试阶段...")
self._stages.clear()
self._errors.clear()
if self.stages_dir and os.path.isdir(self.stages_dir):
self._discover_and_load_stages()
if self._errors:
for error in self._errors:
self.logger.error(f"StageRegistry (重载时): 加载阶段时发生错误: {error}")
self.logger.info(f"StageRegistry: 重新加载完成。共加载 {len(self._stages)} 个测试阶段。")
elif self.stages_dir:
self.logger.warning(f"StageRegistry (重载时): 提供的阶段目录 '{self.stages_dir}' 无效或不存在。没有加载任何自定义阶段。")
"""清空并重新从目录加载所有阶段。"""
self.clear_stages()
if self.stages_dir:
self.discover_stages()
else:
self.logger.info("StageRegistry (重载时): 未配置阶段目录,没有加载任何自定义阶段。")
logger.info("没有配置阶段目录,无法重新加载阶段。")
File diff suppressed because it is too large Load Diff