feat(executor): 重构用例加载与执行逻辑,支持参数化变量优先级

- 引入 CaseEntity 包装器,实现数据模型与执行上下文解耦。
 - 移除加载阶段的 deepcopy,优化大规模参数化用例的内存占用。
 - 实现 perform 阶段的局部变量注入,确保参数化数据优先级高于全局缓存。
This commit is contained in:
2026-03-11 17:11:19 +08:00
parent 293b5160fe
commit 2116016a0d
7 changed files with 201 additions and 69 deletions

View File

@@ -13,6 +13,7 @@ import logging
import allure
from pathlib import Path
from dataclasses import dataclass
from core import settings
from core.executor import WorkflowExecutor
from core.session import Session
@@ -30,6 +31,12 @@ session = Session(settings.base_url)
exchanger = Exchange(settings.DATA_DIR / "extract.yaml") # 指向 data/extract.yaml
executor = WorkflowExecutor(session, exchanger)
@dataclass
class CaseEntity:
"""用例执行实体:解耦模型数据与执行上下文"""
step_data: CaseInfo
row_context: dict[str, Any]
class TestTemplateBase:
"""
@@ -56,12 +63,12 @@ class CaseDataLoader:
yield from base_path.rglob("test_*.yaml")
@classmethod
def load_cases(cls, file_path: Path) -> List[CaseInfo]:
def load_cases(cls, file_path: Path) -> List[CaseEntity]:
"""
加载单个 YAML 文件并转化为 CaseInfo 列表
包含参数化数据的自动拆解逻辑
"""
cases = []
entities = []
try:
# 1. 使用重构后的 YamlProcessor 加载原始字典
processor = FileHandle(file_path)
@@ -69,13 +76,24 @@ class CaseDataLoader:
if not raw_data:
return []
# 1. 提取参数化数据
parametrize_data = raw_data.pop("parametrize", None)
# 2. 实例化唯一的模板对象 (Pydantic 校验)
# 此时占位符 ${var} 会被 SmartInt/SmartDict 校验器放行
template_case = CaseInfo(**raw_data)
# 2. 检查是否存在参数化字段
if "parametrize" in raw_data and isinstance(raw_data["parametrize"], list):
cases.extend(cls._parse_parametrize(raw_data))
if parametrize_data and isinstance(parametrize_data, list) and len(parametrize_data) >= 2:
# 3. 参数化拆分
headers = parametrize_data[0]
for row in parametrize_data[1:]:
row_map = dict(zip(headers, row))
# 包装为实体,存入引用而非副本
entities.append(CaseEntity(step_data=template_case, row_context=row_map))
else:
# 3. 普通单条用例封装
cases.append(CaseInfo(**raw_data))
# 普通用例,上下文为空
entities.append(CaseEntity(step_data=template_case, row_context={}))
except YamlLoadError:
# YamlProcessor 已经记录了 error 日志,这里直接跳过
@@ -85,7 +103,7 @@ class CaseDataLoader:
except Exception as e:
logger.error(f"加载用例发生未知异常 [{file_path.name}]: {e}")
return cases
return entities
@staticmethod
def _parse_parametrize(raw_data: dict[str, Any]) -> List[CaseInfo]:
@@ -121,9 +139,10 @@ class CaseDataLoader:
return case_list
@classmethod
def get_all_cases(cls, cases_dir: Union[str, Path]) -> List[CaseInfo]:
def get_all_cases(cls, cases_dir: Union[str, Path]) -> List[CaseEntity]:
"""
全量获取接口:供 CaseGenerator 调用
全量获取接口:供 CaseGenerator 调用 frank
"""
all_cases = []
for file in cls.fetch_yaml_files(cases_dir):
@@ -165,6 +184,8 @@ class CaseGenerator:
#
# # 4. 挂载
# method_name = f"test_{file_path.stem}"
case_title = case_info.row_context.get("title") or case_info.step_data.title
method_name = f"test_case_{index}_{case_info.title[:20]}"
safe_name = "".join([c if c.isalnum() else "_" for c in method_name])
# setattr(target_cls, method_name, dynamic_test_method)
@@ -173,8 +194,10 @@ class CaseGenerator:
@staticmethod
# def _create_case_method(case_template: Dict, fields: List[str], values: List[Any], ids: List[str]):
def _create_case_method(case_template: CaseInfo):
def _create_case_method(entity: CaseEntity):
"""封装具体的 pytest 执行节点"""
case_template = entity.step_data
context = entity.row_context
# 预取 Allure 层级信息
# epic = case_template.get("epic", settings.allure_epic)
@@ -194,7 +217,7 @@ class CaseGenerator:
# current_params = dict(zip(fields, case_args))
# case_exec_data = {**case_template, **current_params}
# case_title = current_params.get("title", "未命名用例")
case_title = case_template.title or "未命名用例"
case_title = context.get("title") or case_template.title or "未命名用例"
# 日志记录 (利用 instance 标注来源)
logger.info(f"🚀 [Runner] Class: {instance.__class__.__name__} | Case: {case_title}")
@@ -202,7 +225,7 @@ class CaseGenerator:
# 执行与断言
allure.dynamic.title(case_title)
# executor.perform(case_exec_data)
executor.perform(case_template)
executor.perform(case_template,context=context)
# 手动链路装饰 (Allure)
# run_actual_case = allure.epic(epic)(run_actual_case)

View File

@@ -7,7 +7,7 @@
import logging
import importlib
from typing import Any, List
from typing import Any, List, Optional
from pydantic import TypeAdapter
@@ -28,9 +28,20 @@ class WorkflowExecutor:
self.session = session
self.exchanger = exchanger
def perform(self, case_info: CaseInfo) -> Any:
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)
@@ -62,6 +73,10 @@ class WorkflowExecutor:
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/ 目录下的类并执行方法"""

View File

@@ -21,7 +21,7 @@ load_dotenv()
# --- 目录配置 ---
TEST_CASE_DIR = BASE_DIR / "test_cases"
EXTRACT_CACHE = BASE_DIR / "data/extract.yaml"
OUTPUT_DIR = BASE_DIR / "outputs"
SCREENSHOT_DIR = OUTPUT_DIR / "screenshots"
@@ -33,8 +33,14 @@ REPORT_DIR = BASE_DIR / "reports"
CONFIG_DIR = BASE_DIR / "config"
DATA_DIR = BASE_DIR / "data"
# 需要初始化的目录列表
REQUIRED_DIRS = [LOG_DIR, LOG_BACKUP_DIR, ALLURE_TEMP, SCREENSHOT_DIR]
# 核心 API 目录路径
API_PACKAGE = "api"
LOG_SOURCE = LOG_DIR / "pytest.log"
EXTRACT_CACHE = BASE_DIR / "data/extract.yaml"
# 可选:显式映射(类名 -> 完整模块路径),解决文件名不规则的问题
API_MAP = {
@@ -55,4 +61,4 @@ rsa_private = ""
if __name__ == '__main__':
print(BASE_DIR)
print(BASE_DIR, db_host, db_port, db_user, db_password, db_database)