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:
2026-03-11 10:29:16 +08:00
parent 69a96a0060
commit 293b5160fe
39 changed files with 1359 additions and 1031 deletions

View File

@@ -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)