Files
InterfaceAutoTest/core/executor.py
CNWei 6393414ab2 feat,fix(core,docs): 完善核心模块代码注释并添加架构改进文档
- 为 core 目录下主要模块 (models, context, creator, base_api, exchange, executor) 添加了详细的类和方法 Docstring。
   - 新增 docs/架构改进.md 文件。
2026-03-18 11:26:55 +08:00

149 lines
6.4 KiB
Python
Raw Permalink 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 collections import ChainMap
from pydantic import TypeAdapter
from core import settings
from core.context import ExecutionEnv
from core.models import RawSchema, ValidateItem, HttpAction, 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:
"""
工作流执行器。
作为测试执行的核心引擎,负责调度单个用例的完整生命周期:
1. 上下文准备(变量池合并)。
2. 动作路由与执行HTTP 请求或 PO 方法反射调用)。
3. 后处理(变量提取与断言校验)。
"""
@classmethod
def perform(cls, case_info: RawSchema, env: ExecutionEnv, context: Optional[dict[str, Any]] = None) -> Any:
"""执行单个用例支持直接请求和PO模式调用"""
context = context or {}
# --- 重点 1备份并切换上下文 ---
# 保存 Exchange 当前的全局字典引用
original_cache = env.exchanger.global_vars
# 1. 建立优先级变量池 (参数化变量 > 全局提取变量)
# ChainMap 是实现“局部覆盖全局”性能最好的方案
combined_vars = ChainMap(context, original_cache)
# 将 Exchange 的内部缓存临时指向这个合并池
env.exchanger.global_vars = combined_vars
resp = None # 初始化 resp避免异常时引用未定义
try:
# 2. 动态更新标题(如果 context 中包含 title
current_title = context.get("title") or case_info.title
logger.info(f"🚀 执行用例: {current_title}")
raw_action_dict = case_info.action.model_dump(by_alias=True, exclude_none=True)
rendered_action_dict = env.exchanger.replace(raw_action_dict)
# --- 2. 决定执行模式 ---
if case_info.is_po_mode():
# 重新校验以修复类型(如 params 里的 int
rendered_action = ApiActionModel.model_validate(rendered_action_dict)
# PO 模式:反射调用
resp = cls._execute_po_method(rendered_action, env)
else:
# 接口模式:直接请求
rendered_request = HttpAction.model_validate(rendered_action_dict)
request_kwargs = rendered_request.model_dump(by_alias=True, exclude_none=True)
resp = env.session.request(**request_kwargs)
# --- 3. 后处理:提取与断言 ---
cls._post_process(resp, case_info, env, original_cache)
return resp
except Exception as e:
logger.error(f"用例执行失败: {case_info.title} | 原因: {e}", exc_info=True)
raise
finally:
# 兜底确保环境还原 (尽管 try 块中已经还原了一次,这里确保异常情况下也复位)
env.exchanger.global_vars = original_cache
@staticmethod
def _execute_po_method(action: ApiActionModel, env: ExecutionEnv):
"""核心反射逻辑:根据字符串动态加载 api/ 目录下的类并执行方法"""
class_name = action.module
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 目录下)
module = importlib.import_module(module_name)
# 2. 获取类并实例化
cls = getattr(module, class_name)
api_instance = cls(env.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
@classmethod
def _post_process(cls, resp: Any, case_info: RawSchema, env: ExecutionEnv, original_cache: dict):
"""
统一后处理逻辑:处理变量提取(写全局)和断言校验(读局部+全局)
"""
# 记录当前的混合上下文 (ChainMap),供断言使用
combined_vars = env.exchanger.global_vars
# 1. 变量提取 (Write Operation)
if case_info.extract:
try:
# 必须切回 original_cache 才能持久化写入到全局变量池
env.exchanger.global_vars = original_cache
for var_name, extract_info in case_info.extract.items():
env.exchanger.extract(resp, var_name, *extract_info)
finally:
# 提取完成后,切回 combined_vars防止后续逻辑如断言丢失局部变量上下文
env.exchanger.global_vars = combined_vars
# 2. 断言校验 (Read Operation)
if case_info.validate_data:
raw_validate_list = [
item.model_dump(by_alias=True) if isinstance(item, ValidateItem) else item
for item in case_info.validate_data
]
rendered_validate_list = env.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)