#!/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)