refactor: 重构执行引擎为上下文驱动架构
- 优化 WorkflowExecutor 与 Exchange支持 ExecutionEnv 资源注入。 - 实现 Session 级别连接复用与变量池内存镜像化,消除重复 I/O 开销。 - 引入 ChainMap 实现动态上下文切换,解决参数化变量与全局提取变量的优先级覆盖。 - 完善变量提取与断言逻辑,确保跨用例变量流转的可靠性。
This commit is contained in:
170
core/creator.py
170
core/creator.py
@@ -16,25 +16,20 @@ from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from core import settings
|
||||
from core.executor import WorkflowExecutor
|
||||
from core.session import Session
|
||||
from core.exchange import Exchange
|
||||
|
||||
from pydantic import ValidationError
|
||||
from commons.file_processors.yaml_processor import YamlProcessor as FileHandle,YamlLoadError
|
||||
from core.models import CaseInfo # 导入之前定义的 Pydantic 模型
|
||||
from commons.file_processors.yaml_processor import YamlProcessor as FileHandle, YamlLoadError
|
||||
from core.models import RawSchema # 导入之前定义的 Pydantic 模型
|
||||
from typing import Any, List, Type, Generator, Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 初始化全局组件
|
||||
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
|
||||
step_data: RawSchema
|
||||
row_context: dict[str, Any]
|
||||
|
||||
|
||||
@@ -53,7 +48,7 @@ class CaseDataLoader:
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def fetch_yaml_files(cases_dir: str) -> Generator[Path, None, None]:
|
||||
def fetch_yaml_files(cases_dir: Union[str, Path]) -> Generator[Path, None, None]:
|
||||
"""扫描目录并迭代返回 (文件路径, 原始内容)"""
|
||||
base_path = Path(cases_dir)
|
||||
if not base_path.exists():
|
||||
@@ -68,7 +63,7 @@ class CaseDataLoader:
|
||||
加载单个 YAML 文件并转化为 CaseInfo 列表
|
||||
包含参数化数据的自动拆解逻辑
|
||||
"""
|
||||
entities = []
|
||||
entities: List[CaseEntity] = []
|
||||
try:
|
||||
# 1. 使用重构后的 YamlProcessor 加载原始字典
|
||||
processor = FileHandle(file_path)
|
||||
@@ -76,67 +71,45 @@ 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_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:
|
||||
# 普通用例,上下文为空
|
||||
entities.append(CaseEntity(step_data=template_case, row_context={}))
|
||||
entities = cls._parse_parametrize(raw_data)
|
||||
|
||||
except YamlLoadError:
|
||||
# YamlProcessor 已经记录了 error 日志,这里直接跳过
|
||||
pass
|
||||
except ValidationError as e:
|
||||
logger.error(f"用例格式校验失败 [{file_path.name}]:\n{e.json()}")
|
||||
logger.error(f"用例基础格式校验失败 [{file_path.name}]:\n{e.json()}")
|
||||
except Exception as e:
|
||||
logger.error(f"加载用例发生未知异常 [{file_path.name}]: {e}")
|
||||
|
||||
return entities
|
||||
|
||||
@staticmethod
|
||||
def _parse_parametrize(raw_data: dict[str, Any]) -> List[CaseInfo]:
|
||||
def _parse_parametrize(raw_data: dict[str, Any]) -> List[CaseEntity]:
|
||||
"""
|
||||
解析参数化逻辑:将 raw_data 中的 parametrize 展开为多个 CaseInfo 实例
|
||||
"""
|
||||
param_content = raw_data.pop("parametrize")
|
||||
if len(param_content) < 2:
|
||||
logger.warning(f"参数化数据不足(需包含 Header 和至少一行 Data): {raw_data.get('title')}")
|
||||
return [CaseInfo(**raw_data)]
|
||||
entities = []
|
||||
parametrize_data = raw_data.pop("parametrize", None)
|
||||
|
||||
# 第一行作为变量名 (Headers),后续作为数据行
|
||||
headers = param_content[0]
|
||||
data_rows = param_content[1:]
|
||||
# 2. 实例化唯一的模板对象 (Pydantic 校验)
|
||||
template_case = RawSchema.model_validate(raw_data)
|
||||
# template_case = CaseTemplate(**raw_data)
|
||||
|
||||
case_list = []
|
||||
for row in data_rows:
|
||||
# 将变量名和对应行数据打包成字典,例如 {"username": "user1", "title": "测试1"}
|
||||
row_map = dict(zip(headers, row))
|
||||
# 2. 检查是否存在参数化字段
|
||||
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))
|
||||
# 包装为实体,存入引用而非副本
|
||||
# 修正: 使用 model_copy() 避免多个用例共享同一个 Pydantic 模型实例,防止意外修改
|
||||
entities.append(CaseEntity(step_data=template_case.model_copy(), row_context=row_map))
|
||||
else:
|
||||
# 普通用例,上下文为空
|
||||
entities.append(CaseEntity(step_data=template_case.model_copy(), row_context={}))
|
||||
|
||||
# 深拷贝原始模板,避免多行数据互相干扰
|
||||
case_tmp = raw_data.copy()
|
||||
|
||||
# 关键优化:如果参数化里包含 'title',自动更新顶层的 title 字段
|
||||
# 注意:此处仅做初步合并,更复杂的 ${var} 替换由 WorkflowExecutor 的 Exchanger 完成
|
||||
if "title" in row_map:
|
||||
case_tmp["title"] = row_map["title"]
|
||||
|
||||
# 将当前行的数据注入到 CaseInfo 中(此处可以暂存在字段中,或由执行器处理)
|
||||
# 为了保持模型兼容,我们把 row_map 的信息合入 case_tmp
|
||||
case_list.append(CaseInfo(**case_tmp))
|
||||
|
||||
return case_list
|
||||
return entities
|
||||
|
||||
@classmethod
|
||||
def get_all_cases(cls, cases_dir: Union[str, Path]) -> List[CaseEntity]:
|
||||
@@ -149,19 +122,6 @@ class CaseDataLoader:
|
||||
all_cases.extend(cls.load_cases(file))
|
||||
return all_cases
|
||||
|
||||
# @staticmethod
|
||||
# def parse_parametrize(raw_data: Dict, default_name: str) -> Tuple[List[str], List[List[Any]], List[str]]:
|
||||
# """解析参数化结构,返回 (字段名列表, 数据值列表, ID列表)"""
|
||||
# if "parametrize" in raw_data:
|
||||
# fields = raw_data["parametrize"][0]
|
||||
# values = raw_data["parametrize"][1:]
|
||||
# ids = [f"{v[0]}" for v in values]
|
||||
# else:
|
||||
# fields = ["case_data"]
|
||||
# values = [[raw_data]]
|
||||
# ids = [raw_data.get("title", default_name)]
|
||||
# return fields, values, ids
|
||||
|
||||
|
||||
class CaseGenerator:
|
||||
"""
|
||||
@@ -170,67 +130,43 @@ class CaseGenerator:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def build_and_register(cls, target_cls: Type[TestTemplateBase], cases_dir: str):
|
||||
def build_and_register(cls, target_cls: Type[TestTemplateBase], cases_dir: Union[str, Path]):
|
||||
# 1. 通过 Loader 获取数据
|
||||
all_cases=CaseDataLoader.get_all_cases(cases_dir)
|
||||
all_cases = CaseDataLoader.get_all_cases(cases_dir)
|
||||
for index, case_info in enumerate(all_cases):
|
||||
dynamic_test_method=cls._create_case_method(case_info)
|
||||
# for file_path, raw_data in CaseDataLoader.get_all_cases(cases_dir):
|
||||
# # 2. 解析参数化信息
|
||||
# fields, values, ids = CaseDataLoader.parse_parametrize(raw_data, file_path.stem)
|
||||
#
|
||||
# # 3. 生成执行函数 (闭包)
|
||||
# dynamic_test_method = cls._create_case_method(raw_data, fields, values, ids)
|
||||
#
|
||||
# # 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)
|
||||
setattr(target_cls, safe_name, dynamic_test_method)
|
||||
dynamic_test_method = cls._create_case_method(title=case_title, entity=case_info)
|
||||
|
||||
safe_title = "".join([c if c.isalnum() else "_" for c in case_title])[:50]
|
||||
method_name = f"test_{index:03d}_{safe_title}"
|
||||
print(method_name)
|
||||
setattr(target_cls, method_name, dynamic_test_method)
|
||||
print(target_cls.__dict__)
|
||||
logger.debug(f"Successfully registered: {method_name}")
|
||||
|
||||
@staticmethod
|
||||
# def _create_case_method(case_template: Dict, fields: List[str], values: List[Any], ids: List[str]):
|
||||
def _create_case_method(entity: CaseEntity):
|
||||
def _create_case_method(title, entity: CaseEntity):
|
||||
"""封装具体的 pytest 执行节点"""
|
||||
case_template = entity.step_data
|
||||
context = entity.row_context
|
||||
|
||||
# 预取 Allure 层级信息
|
||||
# epic = case_template.get("epic", settings.allure_epic)
|
||||
# feature = case_template.get("feature", settings.allure_feature)
|
||||
# story = case_template.get("story", settings.allure_story)
|
||||
epic = case_template.epic or settings.allure_epic
|
||||
feature = case_template.feature or settings.allure_feature
|
||||
story = case_template.story or settings.allure_story
|
||||
|
||||
@allure.epic(epic)
|
||||
@allure.feature(feature)
|
||||
@allure.story(story)
|
||||
# @pytest.mark.parametrize("case_args", values, ids=ids)
|
||||
# def build_actual_case(instance: TestTemplateBase, case_args: List[Any]):
|
||||
def build_actual_case(instance: TestTemplateBase):
|
||||
# 数据组装
|
||||
# current_params = dict(zip(fields, case_args))
|
||||
# case_exec_data = {**case_template, **current_params}
|
||||
# case_title = current_params.get("title", "未命名用例")
|
||||
case_title = context.get("title") or case_template.title or "未命名用例"
|
||||
def build_actual_case(instance: TestTemplateBase, api_env):
|
||||
# --- 1. 动态设置 Allure 报告属性 ---
|
||||
|
||||
allure.dynamic.epic(case_template.epic or settings.allure_epic)
|
||||
allure.dynamic.feature(case_template.feature or settings.allure_feature)
|
||||
allure.dynamic.story(case_template.story or settings.allure_story)
|
||||
allure.dynamic.title(title)
|
||||
# 日志记录 (利用 instance 标注来源)
|
||||
logger.info(f"🚀 [Runner] Class: {instance.__class__.__name__} | Case: {case_title}")
|
||||
logger.info(f"[Runner] Class: {instance.__class__.__name__} | Case: {title}")
|
||||
try:
|
||||
WorkflowExecutor.perform(case_template, api_env, context=context)
|
||||
|
||||
# 执行与断言
|
||||
allure.dynamic.title(case_title)
|
||||
# executor.perform(case_exec_data)
|
||||
executor.perform(case_template,context=context)
|
||||
|
||||
# 手动链路装饰 (Allure)
|
||||
# run_actual_case = allure.epic(epic)(run_actual_case)
|
||||
# run_actual_case = allure.feature(feature)(run_actual_case)
|
||||
# run_actual_case = allure.story(story)(run_actual_case)
|
||||
except Exception as e:
|
||||
# 可以在这里记录更详细的运行上下文快照
|
||||
logger.error(f"Case 执行失败: {title} | 错误: {e}")
|
||||
raise
|
||||
|
||||
return build_actual_case
|
||||
|
||||
@@ -238,6 +174,6 @@ class CaseGenerator:
|
||||
if __name__ == '__main__':
|
||||
from settings import TEST_CASE_DIR
|
||||
|
||||
print(CaseDataLoader.get_all_cases(TEST_CASE_DIR))
|
||||
# print(CaseDataLoader.get_all_cases(TEST_CASE_DIR))
|
||||
# --- 引导执行 ---
|
||||
# CaseGenerator.build_and_register(TestTemplateBase, settings.TEST_CASE_DIR)
|
||||
CaseGenerator.build_and_register(TestTemplateBase, settings.TEST_CASE_DIR)
|
||||
|
||||
Reference in New Issue
Block a user