fix(exchange,case_validator),refactor(),feat(model): 解决 Pydantic 模型初始化与变量占位符的类型冲突,优化变量替换逻辑,重构 CaseInfo 模型并引入延迟校验机制
- 引入 SmartInt 和 SmartDict 类型,支持 YAML 占位符与业务类型的自动转换。 - 优化 CaseInfo 互斥校验逻辑,确保 request 与 api_action 二选一。 - 统一使用 Pydantic V2 的 model_config 规范。 - 将变量替换时机提前至模型实例化之前,支持占位符在校验前完成真实值注入, 保证了 int/bool 等字段的类型转换正确性。 - 优化断言渲染时机,支持响应提取值关联。
This commit is contained in:
116
core/executor.py
116
core/executor.py
@@ -7,59 +7,82 @@
|
||||
|
||||
import logging
|
||||
import importlib
|
||||
from typing import Any
|
||||
from commons.models.case_model import CaseInfo
|
||||
from typing import Any, List
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from core import settings
|
||||
from core.models import CaseInfo, ValidateItem, RequestModel, ApiActionModel
|
||||
from core.session import Session
|
||||
from commons.exchange import Exchange
|
||||
from commons.asserts import Asserts
|
||||
from core.exchange import Exchange
|
||||
from utils.case_validator import CaseValidator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 定义一个复用的适配器(减少初始化开销)
|
||||
VALIDATE_LIST_ADAPTER = TypeAdapter(List[ValidateItem])
|
||||
|
||||
|
||||
class WorkflowExecutor:
|
||||
def __init__(self, session: Session, exchanger: Exchange):
|
||||
self.session = session
|
||||
self.exchanger = exchanger
|
||||
|
||||
def perform(self, case_data: dict) -> Any:
|
||||
def perform(self, case_info: CaseInfo) -> Any:
|
||||
"""执行单个用例:支持直接请求和PO模式调用"""
|
||||
# 1. 变量替换(将 ${var} 替换为真实值)
|
||||
rendered_case = self.exchanger.replace(case_data)
|
||||
try:
|
||||
# raw_data = case_info.model_dump(by_alias=True, exclude_none=True)
|
||||
# 1. 变量替换(将 ${var} 替换为真实值)
|
||||
# rendered_dict = self.exchanger.replace(raw_data)
|
||||
# rendered_case = CaseInfo.model_validate(rendered_dict)
|
||||
# --- 2. 决定执行模式 ---
|
||||
if case_info.is_po_mode():
|
||||
# PO 模式:仅渲染 api_action
|
||||
action_dict = case_info.api_action.model_dump(by_alias=True, exclude_none=True)
|
||||
rendered_action_dict = self.exchanger.replace(action_dict)
|
||||
# 重新校验以修复类型(如 params 里的 int)
|
||||
rendered_action = ApiActionModel.model_validate(rendered_action_dict)
|
||||
# PO 模式:反射调用
|
||||
|
||||
# 2. 决定执行模式
|
||||
if "api_action" in rendered_case:
|
||||
# --- PO 模式:反射调用业务层 ---
|
||||
action = rendered_case["api_action"]
|
||||
resp = self._execute_po_method(
|
||||
class_name=action["class"],
|
||||
method_name=action["method"],
|
||||
params=action.get("params", {})
|
||||
)
|
||||
else:
|
||||
# --- 数据驱动模式:直接发送请求 ---
|
||||
# 使用 Pydantic 校验 request 结构
|
||||
case_info = CaseInfo(**rendered_case)
|
||||
request_info = case_info.request.model_dump(by_alias=True, exclude_none=True)
|
||||
resp = self.session.request(**request_info)
|
||||
resp = self._execute_po_method(action=rendered_action)
|
||||
else:
|
||||
# 接口模式:直接请求
|
||||
# 直接将 RequestModel 转为字典传给 session.request
|
||||
request_kwargs = case_info.request.model_dump(by_alias=True, exclude_none=True)
|
||||
rendered_req_dict = self.exchanger.replace(request_kwargs)
|
||||
rendered_request = RequestModel.model_validate(rendered_req_dict)
|
||||
|
||||
# 3. 提取变量 (接口关联)
|
||||
if rendered_case.get("extract"):
|
||||
for var_name, extract_info in rendered_case["extract"].items():
|
||||
self.exchanger.extract(resp, var_name, *extract_info)
|
||||
request_kwargs = rendered_request.model_dump(by_alias=True, exclude_none=True)
|
||||
resp = self.session.request(**request_kwargs)
|
||||
|
||||
# 4. 断言校验
|
||||
if rendered_case.get("validate"):
|
||||
Asserts.validate(resp, rendered_case["validate"])
|
||||
# --- 3. 后置处理 (提取 & 断言) ---
|
||||
self._post_process(resp, case_info)
|
||||
|
||||
return resp
|
||||
return resp
|
||||
except Exception as e:
|
||||
logger.error(f"用例执行失败: {case_info.title} | 原因: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _execute_po_method(self, class_name: str, method_name: str, params: dict):
|
||||
def _execute_po_method(self, action: ApiActionModel):
|
||||
"""核心反射逻辑:根据字符串动态加载 api/ 目录下的类并执行方法"""
|
||||
class_name = action.api_class
|
||||
method_name = action.method
|
||||
params = action.params or {}
|
||||
# 1. 确定模块路径:优先级策略
|
||||
# 优先级 1: 显式映射 (API_MAP)
|
||||
module_name = settings.API_MAP.get(class_name)
|
||||
|
||||
# 优先级 2: 规约命名 (UserAPI -> api.user_api)
|
||||
if not module_name:
|
||||
base_name = class_name.lower().replace('api', '')
|
||||
module_name = f"{settings.API_PACKAGE}.{base_name}_api"
|
||||
|
||||
try:
|
||||
# 1. 动态导入模块(假设都在 api 目录下)
|
||||
# 例如 class_name 是 UserAPI,则尝试从 api.user 导入
|
||||
# 这里简单处理,你可以根据你的文件名约定进一步优化逻辑
|
||||
module_name = f"api.{class_name.lower().replace('api', '')}"
|
||||
# module_name = f"api.{class_name.lower().replace('api', '')}"
|
||||
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
# 2. 获取类并实例化
|
||||
@@ -68,8 +91,33 @@ class WorkflowExecutor:
|
||||
|
||||
# 3. 调用方法并返回结果
|
||||
method = getattr(api_instance, method_name)
|
||||
logger.info(f"🚀 调用业务层: {class_name}.{method_name} 参数: {params}")
|
||||
logger.info(f"调用业务层: {class_name}.{method_name} 参数: {params}")
|
||||
return method(**params)
|
||||
except ImportError as e:
|
||||
logger.error(f"模块导入失败: 在 '{module_name}' 未找到对应文件。请检查文件名或 settings.API_MAP 配置。")
|
||||
raise e
|
||||
except AttributeError as e:
|
||||
logger.error(f"成员获取失败: 模块 '{module_name}' 中不存在类或方法 '{class_name}.{method_name}'。")
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"反射调用失败: {class_name}.{method_name} -> {e}")
|
||||
raise
|
||||
raise
|
||||
|
||||
def _post_process(self, resp: Any, rendered_case: CaseInfo):
|
||||
# 3. 提取变量 (接口关联)
|
||||
if rendered_case.extract:
|
||||
for var_name, extract_info in rendered_case.extract.items():
|
||||
self.exchanger.extract(resp, var_name, *extract_info)
|
||||
|
||||
# 4. 断言校验
|
||||
if rendered_case.validate_data:
|
||||
# raw_validate_list = [i.model_dump(by_alias=True) for i in rendered_case.validate_data]
|
||||
raw_validate_list = [
|
||||
item.model_dump(by_alias=True) if isinstance(item, ValidateItem) else item
|
||||
for item in rendered_case.validate_data
|
||||
]
|
||||
|
||||
rendered_validate_list = self.exchanger.replace(raw_validate_list)
|
||||
# 重新通过 Adapter 触发类型修复 (str -> int)
|
||||
final_validate_data = VALIDATE_LIST_ADAPTER.validate_python(rendered_validate_list)
|
||||
CaseValidator.validate(resp, final_validate_data)
|
||||
|
||||
Reference in New Issue
Block a user