- 引入 SmartInt 和 SmartDict 类型,支持 YAML 占位符与业务类型的自动转换。 - 优化 CaseInfo 互斥校验逻辑,确保 request 与 api_action 二选一。 - 统一使用 Pydantic V2 的 model_config 规范。 - 将变量替换时机提前至模型实例化之前,支持占位符在校验前完成真实值注入, 保证了 int/bool 等字段的类型转换正确性。 - 优化断言渲染时机,支持响应提取值关联。
124 lines
5.3 KiB
Python
124 lines
5.3 KiB
Python
#!/usr/bin/env python
|
||
# coding=utf-8
|
||
|
||
"""
|
||
@desc: 核心测试用例执行引擎
|
||
"""
|
||
|
||
import logging
|
||
import importlib
|
||
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 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_info: CaseInfo) -> Any:
|
||
"""执行单个用例:支持直接请求和PO模式调用"""
|
||
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 模式:反射调用
|
||
|
||
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)
|
||
|
||
request_kwargs = rendered_request.model_dump(by_alias=True, exclude_none=True)
|
||
resp = self.session.request(**request_kwargs)
|
||
|
||
# --- 3. 后置处理 (提取 & 断言) ---
|
||
self._post_process(resp, case_info)
|
||
|
||
return resp
|
||
except Exception as e:
|
||
logger.error(f"用例执行失败: {case_info.title} | 原因: {e}", exc_info=True)
|
||
raise
|
||
|
||
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 = importlib.import_module(module_name)
|
||
|
||
# 2. 获取类并实例化
|
||
cls = getattr(module, class_name)
|
||
api_instance = cls(self.session) # 传入 session 保持会话统一
|
||
|
||
# 3. 调用方法并返回结果
|
||
method = getattr(api_instance, method_name)
|
||
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
|
||
|
||
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)
|