- 为 core 目录下主要模块 (models, context, creator, base_api, exchange, executor) 添加了详细的类和方法 Docstring。 - 新增 docs/架构改进.md 文件。
200 lines
7.0 KiB
Python
200 lines
7.0 KiB
Python
#!/usr/bin/env python
|
||
# coding=utf-8
|
||
|
||
"""
|
||
@author: CNWei,ChenWei
|
||
@Software: PyCharm
|
||
@contact: t6g888@163.com
|
||
@file: creator
|
||
@date: 2026/3/6 10:40
|
||
@desc:
|
||
"""
|
||
import logging
|
||
|
||
import allure
|
||
from pathlib import Path
|
||
from dataclasses import dataclass
|
||
|
||
from conftest import execution_context
|
||
from core import settings
|
||
from core.executor import WorkflowExecutor
|
||
from pydantic import ValidationError
|
||
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__)
|
||
|
||
|
||
|
||
|
||
@dataclass
|
||
class CaseEntity:
|
||
"""用例执行实体:解耦模型数据与执行上下文"""
|
||
step_data: RawSchema
|
||
row_context: dict[str, Any]
|
||
|
||
|
||
class TestTemplateBase:
|
||
"""
|
||
具体的测试用例容器。
|
||
此映射类不包含任何逻辑方法,仅用于承载由 Loader 挂载的 test_* 方法。
|
||
"""
|
||
pass
|
||
|
||
|
||
class CaseDataLoader:
|
||
"""
|
||
测试用例加载器
|
||
职责:扫描文件系统 -> 载入 YAML -> 拆解参数化 -> 封装为 CaseInfo 模型
|
||
"""
|
||
|
||
@staticmethod
|
||
def fetch_yaml_files(cases_dir: Union[str, Path]) -> Generator[Path, None, None]:
|
||
"""扫描目录并迭代返回 (文件路径, 原始内容)"""
|
||
base_path = Path(cases_dir)
|
||
if not base_path.exists():
|
||
logger.error(f"📂 测试目录不存在: {base_path}")
|
||
return
|
||
# 匹配所有以 test_ 开头的 yaml 文件
|
||
yield from base_path.rglob("test_*.yaml")
|
||
|
||
@classmethod
|
||
def load_cases(cls, file_path: Path) -> List[CaseEntity]:
|
||
"""
|
||
加载单个 YAML 文件并转化为 CaseInfo 列表
|
||
包含参数化数据的自动拆解逻辑
|
||
"""
|
||
entities: List[CaseEntity] = []
|
||
try:
|
||
# 1. 使用重构后的 YamlProcessor 加载原始字典
|
||
processor = FileHandle(file_path)
|
||
raw_data = processor.load()
|
||
|
||
if not raw_data:
|
||
return []
|
||
|
||
entities = cls._parse_parametrize(raw_data)
|
||
|
||
except YamlLoadError:
|
||
# YamlProcessor 已经记录了 error 日志,这里直接跳过
|
||
pass
|
||
except ValidationError as e:
|
||
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[CaseEntity]:
|
||
"""
|
||
解析参数化逻辑:将 raw_data 中的 parametrize 展开为多个 CaseInfo 实例
|
||
"""
|
||
entities = []
|
||
parametrize_data = raw_data.pop("parametrize", None)
|
||
|
||
# 2. 实例化唯一的模板对象 (Pydantic 校验)
|
||
template_case = RawSchema.model_validate(raw_data)
|
||
# template_case = CaseTemplate(**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))
|
||
# 包装为实体,存入引用而非副本
|
||
# 修正: 使用 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={}))
|
||
|
||
return entities
|
||
|
||
@classmethod
|
||
def get_all_cases(cls, cases_dir: Union[str, Path]) -> List[CaseEntity]:
|
||
"""
|
||
全量获取接口:供 CaseGenerator 调用 frank
|
||
|
||
"""
|
||
all_cases = []
|
||
for file in cls.fetch_yaml_files(cases_dir):
|
||
all_cases.extend(cls.load_cases(file))
|
||
return all_cases
|
||
|
||
|
||
class CaseGenerator:
|
||
"""
|
||
职责 2: 用例构造工厂
|
||
负责将数据转化为 pytest 装饰的方法,并挂载到目标类
|
||
"""
|
||
|
||
@classmethod
|
||
def build_and_register(cls, target_cls: Type[TestTemplateBase], cases_dir: Union[str, Path]):
|
||
"""
|
||
构建测试用例并注册到目标测试类中。
|
||
|
||
遍历指定目录下的用例文件,解析数据,生成测试方法并动态绑定到 target_cls 上。
|
||
|
||
Args:
|
||
target_cls: 目标测试类(通常继承自 TestTemplateBase)。
|
||
cases_dir: 测试用例文件所在的目录路径。
|
||
"""
|
||
# 1. 通过 Loader 获取数据
|
||
all_cases = CaseDataLoader.get_all_cases(cases_dir)
|
||
for index, case_info in enumerate(all_cases):
|
||
case_title = case_info.row_context.get("title") or case_info.step_data.title
|
||
|
||
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(title, entity: CaseEntity):
|
||
"""
|
||
封装具体的 pytest 执行节点。
|
||
|
||
创建并返回一个闭包函数,该函数包含完整的测试执行逻辑(Allure 设置、日志、执行器调用)。
|
||
|
||
Args:
|
||
title: 测试用例标题。
|
||
entity: 包含用例数据和上下文的实体对象。
|
||
|
||
Returns:
|
||
function: 可被 pytest 识别和执行的测试方法。
|
||
"""
|
||
case_template = entity.step_data
|
||
context = entity.row_context
|
||
def build_actual_case(instance: TestTemplateBase, execution_context):
|
||
# --- 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: {title}")
|
||
try:
|
||
WorkflowExecutor.perform(case_template, execution_context, context=context)
|
||
|
||
except Exception as e:
|
||
# 可以在这里记录更详细的运行上下文快照
|
||
logger.error(f"Case 执行失败: {title} | 错误: {e}")
|
||
raise
|
||
|
||
return build_actual_case
|
||
|
||
|
||
if __name__ == '__main__':
|
||
from settings import TEST_CASE_DIR
|
||
|
||
# print(CaseDataLoader.get_all_cases(TEST_CASE_DIR))
|
||
# --- 引导执行 ---
|
||
CaseGenerator.build_and_register(TestTemplateBase, settings.TEST_CASE_DIR) |