fix(exchange,case_validator),refactor(),feat(model): 解决 Pydantic 模型初始化与变量占位符的类型冲突,优化变量替换逻辑,重构 CaseInfo 模型并引入延迟校验机制

- 引入 SmartInt 和 SmartDict 类型,支持 YAML 占位符与业务类型的自动转换。
- 优化 CaseInfo 互斥校验逻辑,确保 request 与 api_action 二选一。
- 统一使用 Pydantic V2 的 model_config 规范。
- 将变量替换时机提前至模型实例化之前,支持占位符在校验前完成真实值注入,
保证了 int/bool 等字段的类型转换正确性。
- 优化断言渲染时机,支持响应提取值关联。
This commit is contained in:
2026-03-11 10:29:16 +08:00
parent 69a96a0060
commit 293b5160fe
39 changed files with 1359 additions and 1031 deletions

View File

@@ -10,14 +10,21 @@
@desc:
"""
import logging
from typing import Union
from typing import Union, Any
from pathlib import Path
import yaml
from commons.file_processors.base_processor import BaseFileProcessor
logger = logging.getLogger(__name__)
class YamlLoadError(Exception):
"""自定义 YAML 加载异常:当 YAML 语法错误或不符合业务结构时抛出"""
pass
class YamlProcessor(BaseFileProcessor):
"""
用于处理 YAML 文件的类,继承自 dict。
@@ -25,52 +32,101 @@ class YamlProcessor(BaseFileProcessor):
并可以直接像字典一样访问 YAML 数据。
"""
def __init__(self, filepath: Union[str, Path], **kwargs):
def __init__(self, filepath: Union[str, Path], data: Union[dict, None] = None):
"""
初始化 YamlFile 对象。
Args: filepath: YAML 文件的路径 (可以是字符串或 pathlib.Path 对象).
Args:
filepath: YAML 文件的路径 (可以是字符串或 pathlib.Path 对象).
data: 可选的初始数据字典。如果提供,则用该字典初始化 YamlFile。
如果不提供,则尝试从 filepath 加载数据。
"""
super().__init__(filepath, **kwargs)
# self.filepath: Path = Path(filepath) # 确保 filepath 是 Path 对象
def load(self) -> dict:
super().__init__(filepath=filepath)
self.filepath: Path = Path(filepath) # 确保 filepath 是 Path 对象
def load(self) -> dict[str, Any]:
"""
YAML 文件加载数据
:return:
加载 YAML 文件并返回字典。
Returns:
Dict: 加载后的数据字典。
Raises:
YamlLoadError: 文件读取或解析过程中出现异常。
"""
if not self.filepath.exists():
logger.warning(f"文件 {self.filepath} 不存在.")
raise FileNotFoundError(f"文件 {self.filepath} 不存在.")
logger.error(f"❌ 文件未找到: {self.filepath}")
return {}
try:
with open(self.filepath, "r", encoding="utf-8") as f:
loaded_data = yaml.safe_load(f)
if not isinstance(loaded_data, dict): # 确保加载的是字典
logger.error(f"YAML文件 {self.filepath} 的根节点不是一个字典/映射.")
raise ValueError(f"YAML文件 {self.filepath} 的根节点不是一个字典/映射.")
return loaded_data
content = yaml.safe_load(f)
# 情况1文件内容为空
if content is None:
return {}
# 情况2YAML 语法正确但不是字典(如单纯的字符串或列表)
if not isinstance(content, dict):
raise YamlLoadError(f"YAML 顶层格式错误:期望 dict实际为 {type(content).__name__}")
return content
except yaml.YAMLError as e:
logger.error(f"加载 YAML 文件 {self.filepath} 时出错: {e}")
raise e
msg = f" YAML 语法错误 [{self.filepath.name}]: {e}"
logger.error(msg)
raise YamlLoadError(msg) from e
except Exception as e:
logger.error(f"📂 读取文件系统异常: {e}")
raise
def save(self, data: dict, new_filepath: Union[str, Path, None] = None) -> None:
@staticmethod
def to_string(data: dict[str, Any]) -> str:
"""
将字典数据保存到 YAML 文件
:param data:
:param new_filepath: 可选参数,指定新的文件路径。如果为 None则覆盖原文件。
将字典 (自身) 转换为 YAML 格式的字符串
Returns:
YAML 格式的字符串。
"""
filepath = Path(new_filepath) if new_filepath else self.filepath
# 确保目标目录存在
filepath.parent.mkdir(parents=True, exist_ok=True)
try:
with open(filepath, "w", encoding="utf-8") as f:
return yaml.safe_dump(
data,
allow_unicode=True,
sort_keys=False,
default_flow_style=False
)
except TypeError as e:
logger.error(f"将数据转换为 YAML 字符串时出错: {e}")
return ""
except Exception as e:
logger.error(f"序列化 YAML 失败: {e}")
return ""
@staticmethod
def from_string(yaml_str: str) -> Union[None, dict]:
"""
将 YAML 格式的字符串转换为字典,并更新当前字典的内容.
Args:
yaml_str: YAML 格式的字符串。
"""
try:
data = yaml.safe_load(yaml_str)
return data if isinstance(data, dict) else {}
except yaml.YAMLError as e:
logger.error(f"YAML 字符串解析失败: {e}")
return {}
def save(self, data: dict[str, Any], new_filepath: Union[str, Path, None] = None):
"""
将字典数据保存为 YAML 文件。
Args:
data: 要保存的字典数据。
new_filepath: 可选,保存到新路径。
"""
target_path = Path(new_filepath) if new_filepath else self.filepath
try:
target_path.parent.mkdir(parents=True, exist_ok=True)
with open(target_path, "w", encoding="utf-8") as f:
yaml.safe_dump(
data,
stream=f,
@@ -78,20 +134,22 @@ class YamlProcessor(BaseFileProcessor):
sort_keys=False,
default_flow_style=False
)
logger.info(f"数据已成功保存 {filepath}")
except (TypeError, OSError, yaml.YAMLError) as e:
logger.error(f"保存 YAML 文件 {filepath} 时出错: {e}")
raise e
logger.debug(f"💾 数据已成功保存至: {target_path}")
except Exception as e:
logger.error(f"🚫 保存 YAML 失败: {e}")
raise
except (TypeError, OSError) as e:
logger.error(f"保存 YAML 文件 {self.filepath} 时出错: {e}")
# todo 需要将异常的情况返回给上层而不是默认处理为{}
if __name__ == '__main__':
from core.settings import TEST_CASE_DIR
# 示例用法
yaml_path = r'E:\PyP\InterfaceAutoTest\TestCases\answer\test_1_status.yaml' # 你的 YAML 文件路径
yaml_path = TEST_CASE_DIR / r'answer/test_1_status.yaml' # 你的 YAML 文件路径
yaml_file = YamlProcessor(yaml_path)
print(yaml_file.load())
print(yaml_file.to_string(yaml_file.load()))
print(type(yaml_file))
# # 直接像字典一样访问数据
@@ -134,4 +192,3 @@ if __name__ == '__main__':
# print("\n加载不存在的文件:", non_existent_file) # 应该打印空字典 {}
# non_existent_file['a'] = 1 # 可以直接添加
# print("\n加载不存在的文件:", non_existent_file)