Files
InterfaceAutoTest/core/executor.py
CNWei 2116016a0d feat(executor): 重构用例加载与执行逻辑,支持参数化变量优先级
- 引入 CaseEntity 包装器,实现数据模型与执行上下文解耦。
 - 移除加载阶段的 deepcopy,优化大规模参数化用例的内存占用。
 - 实现 perform 阶段的局部变量注入,确保参数化数据优先级高于全局缓存。
2026-03-11 17:11:19 +08:00

139 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# coding=utf-8
"""
@desc: 核心测试用例执行引擎
"""
import logging
import importlib
from typing import Any, List, Optional
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,context: Optional[dict[str, Any]] = None) -> Any:
"""执行单个用例支持直接请求和PO模式调用"""
context = context or {}
# 1. 局部变量优先级注入
# 备份全局缓存,将当前行数据合并进去
old_cache = self.exchanger._variable_cache.copy()
self.exchanger._variable_cache.update(context)
try:
# 2. 动态更新标题(如果 context 中包含 title
current_title = context.get("title") or case_info.title
logger.info(f"🚀 执行用例: {current_title}")
# 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
finally:
# 4. 关键:清理现场,还原全局变量池
self.exchanger._variable_cache = old_cache
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)