feat(executor): 重构用例加载与执行逻辑,支持参数化变量优先级
- 引入 CaseEntity 包装器,实现数据模型与执行上下文解耦。 - 移除加载阶段的 deepcopy,优化大规模参数化用例的内存占用。 - 实现 perform 阶段的局部变量注入,确保参数化数据优先级高于全局缓存。
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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/ 目录下的类并执行方法"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user