yaml-test-core 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. yaml_test_core-1.0.0/LICENSE +17 -0
  2. yaml_test_core-1.0.0/PKG-INFO +55 -0
  3. yaml_test_core-1.0.0/README.md +33 -0
  4. yaml_test_core-1.0.0/pyproject.toml +35 -0
  5. yaml_test_core-1.0.0/setup.cfg +4 -0
  6. yaml_test_core-1.0.0/src/yaml_test_core/__init__.py +1 -0
  7. yaml_test_core-1.0.0/src/yaml_test_core/core/__init__.py +81 -0
  8. yaml_test_core-1.0.0/src/yaml_test_core/core/assertion/__init__.py +9 -0
  9. yaml_test_core-1.0.0/src/yaml_test_core/core/assertion/assert_excutor.py +40 -0
  10. yaml_test_core-1.0.0/src/yaml_test_core/core/assertion/assert_parser.py +193 -0
  11. yaml_test_core-1.0.0/src/yaml_test_core/core/assertion/assertor.py +123 -0
  12. yaml_test_core-1.0.0/src/yaml_test_core/core/base_api.py +134 -0
  13. yaml_test_core-1.0.0/src/yaml_test_core/core/context.py +112 -0
  14. yaml_test_core-1.0.0/src/yaml_test_core/core/exceptions.py +72 -0
  15. yaml_test_core-1.0.0/src/yaml_test_core/core/expression/__init__.py +6 -0
  16. yaml_test_core-1.0.0/src/yaml_test_core/core/expression/engine.py +96 -0
  17. yaml_test_core-1.0.0/src/yaml_test_core/core/expression/evaluator.py +33 -0
  18. yaml_test_core-1.0.0/src/yaml_test_core/core/expression/lexer.py +116 -0
  19. yaml_test_core-1.0.0/src/yaml_test_core/core/expression/mode.py +22 -0
  20. yaml_test_core-1.0.0/src/yaml_test_core/core/expression/nodes.py +40 -0
  21. yaml_test_core-1.0.0/src/yaml_test_core/core/expression/parser.py +75 -0
  22. yaml_test_core-1.0.0/src/yaml_test_core/core/extractor.py +81 -0
  23. yaml_test_core-1.0.0/src/yaml_test_core/core/model.py +149 -0
  24. yaml_test_core-1.0.0/src/yaml_test_core/core/protocol.py +26 -0
  25. yaml_test_core-1.0.0/src/yaml_test_core/core/step_handler/__init__.py +1 -0
  26. yaml_test_core-1.0.0/src/yaml_test_core/core/step_handler/base.py +90 -0
  27. yaml_test_core-1.0.0/src/yaml_test_core/core/step_handler/step_handler.py +376 -0
  28. yaml_test_core-1.0.0/src/yaml_test_core/core/step_runner.py +125 -0
  29. yaml_test_core-1.0.0/src/yaml_test_core/core/tool_registry.py +159 -0
  30. yaml_test_core-1.0.0/src/yaml_test_core.egg-info/PKG-INFO +55 -0
  31. yaml_test_core-1.0.0/src/yaml_test_core.egg-info/SOURCES.txt +32 -0
  32. yaml_test_core-1.0.0/src/yaml_test_core.egg-info/dependency_links.txt +1 -0
  33. yaml_test_core-1.0.0/src/yaml_test_core.egg-info/requires.txt +4 -0
  34. yaml_test_core-1.0.0/src/yaml_test_core.egg-info/top_level.txt +1 -0
@@ -0,0 +1,17 @@
1
+ MIT License
2
+ Copyright (c) 2026 yaml-test-core
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+ The above copyright notice and this permission notice shall be included in all
10
+ copies or substantial portions of the Software.
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17
+ SOFTWARE.
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: yaml-test-core
3
+ Version: 1.0.0
4
+ Summary: YAML DSL test runtime core
5
+ Author-email: Your Name <xxx@xxx.com>
6
+ Project-URL: Repository, https://github.com/cuidingyong/yaml_test_core
7
+ Project-URL: Issues, https://github.com/cuidingyong/yaml_test_core/issues
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Software Development :: Testing
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: allure-pytest>=2.16.0
18
+ Requires-Dist: httpx>=0.28.1
19
+ Requires-Dist: jsonpath-ng>=1.8.0
20
+ Requires-Dist: pydantic>=2.13.4
21
+ Dynamic: license-file
22
+
23
+ # yaml-test-core
24
+
25
+ [![Python](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/)
26
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
27
+ [![Status](https://img.shields.io/badge/status-release-brightgreen)]()
28
+
29
+ YAML 自动化测试框架内核,轻量、高可扩展、纯异步接口测试运行时。
30
+
31
+ 专为**接口自动化测试、yaml 用例驱动、动态表达式、自定义工具、Allure 可视化报告**设计。
32
+
33
+ ## ✨ 核心特性
34
+
35
+ - 🚀 **全异步执行引擎**
36
+ 基于 asyncio 实现,接口请求、表达式渲染、工具调用全部异步化
37
+ - 📄 **纯 YAML 用例驱动**
38
+ 零代码编写接口自动化,用例结构清晰易维护
39
+ - 🧩 **强大表达式引擎**
40
+ 支持 `${var}` 变量渲染、JSONPath、自定义工具函数调用
41
+ - 🔧 **可注册自定义工具**
42
+ 支持同步/异步工具注册,业务高度可扩展
43
+ - 📊 **完整 Allure 报告**
44
+ 自动生成步骤层级、执行日志、断言详情、失败附件
45
+ - 🔁 **原生循环步骤支持**
46
+ 内置 Loop 步骤、break_loop、跳过机制
47
+ - ✅ **软硬断言体系**
48
+ 支持批量断言、软断言不中断、失败兜底策略
49
+ - 🧼 **完整上下文隔离**
50
+ 步骤上下文、循环子上下文自动克隆隔离
51
+
52
+ ## 📦 安装
53
+
54
+ ```bash
55
+ pip install yaml-test-core
@@ -0,0 +1,33 @@
1
+ # yaml-test-core
2
+
3
+ [![Python](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/)
4
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
+ [![Status](https://img.shields.io/badge/status-release-brightgreen)]()
6
+
7
+ YAML 自动化测试框架内核,轻量、高可扩展、纯异步接口测试运行时。
8
+
9
+ 专为**接口自动化测试、yaml 用例驱动、动态表达式、自定义工具、Allure 可视化报告**设计。
10
+
11
+ ## ✨ 核心特性
12
+
13
+ - 🚀 **全异步执行引擎**
14
+ 基于 asyncio 实现,接口请求、表达式渲染、工具调用全部异步化
15
+ - 📄 **纯 YAML 用例驱动**
16
+ 零代码编写接口自动化,用例结构清晰易维护
17
+ - 🧩 **强大表达式引擎**
18
+ 支持 `${var}` 变量渲染、JSONPath、自定义工具函数调用
19
+ - 🔧 **可注册自定义工具**
20
+ 支持同步/异步工具注册,业务高度可扩展
21
+ - 📊 **完整 Allure 报告**
22
+ 自动生成步骤层级、执行日志、断言详情、失败附件
23
+ - 🔁 **原生循环步骤支持**
24
+ 内置 Loop 步骤、break_loop、跳过机制
25
+ - ✅ **软硬断言体系**
26
+ 支持批量断言、软断言不中断、失败兜底策略
27
+ - 🧼 **完整上下文隔离**
28
+ 步骤上下文、循环子上下文自动克隆隔离
29
+
30
+ ## 📦 安装
31
+
32
+ ```bash
33
+ pip install yaml-test-core
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "yaml-test-core"
7
+ version = "1.0.0"
8
+ description = "YAML DSL test runtime core"
9
+ readme = "README.md"
10
+ license-files = [ "LICENSE" ]
11
+ authors = [
12
+ { name = "Your Name", email = "xxx@xxx.com" }
13
+ ]
14
+ requires-python = ">=3.12"
15
+ dependencies = [
16
+ "allure-pytest>=2.16.0",
17
+ "httpx>=0.28.1",
18
+ "jsonpath-ng>=1.8.0",
19
+ "pydantic>=2.13.4",
20
+ ]
21
+ classifiers = [
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Operating System :: OS Independent",
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
27
+ "Topic :: Software Development :: Testing",
28
+ ]
29
+
30
+ [project.urls]
31
+ "Repository" = "https://github.com/cuidingyong/yaml_test_core"
32
+ "Issues" = "https://github.com/cuidingyong/yaml_test_core/issues"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,81 @@
1
+ from .assertion import (
2
+ AssertionExecutor,
3
+ AssertParser,
4
+ SoftAssertor
5
+ )
6
+ from .context import (
7
+ ContextProtocol,
8
+ TestContext,
9
+ EvalContextProxy,
10
+ )
11
+ from .exceptions import (
12
+ FrameworkBaseError,
13
+ ContextIndexError,
14
+ ExtractorParseError,
15
+ SkipSignal,
16
+ BreakLoopSignal,
17
+ RenderError,
18
+ StepTypeError,
19
+ )
20
+ from .expression import ExpressionEngine, RenderMode
21
+ from .extractor import Extractor
22
+ from .model import (
23
+ TestCase,
24
+ BaseStepType,
25
+ Step,
26
+ LoopStep,
27
+ Fragment,
28
+ OnFailure,
29
+ BreakLoopMode,
30
+ ApiInstanceFactoryProtocol,
31
+ SkipMode,
32
+ StepData
33
+ )
34
+ from .protocol import ToolRegistryProtocol
35
+ from .step_runner import StepRunner
36
+
37
+ __all__ = [
38
+ "FrameworkBaseError",
39
+ "ContextIndexError",
40
+ "ExtractorParseError",
41
+ "SkipSignal",
42
+ "BreakLoopSignal",
43
+ "RenderError",
44
+ "StepTypeError",
45
+
46
+ # context
47
+ "ContextProtocol",
48
+ "TestContext",
49
+ "EvalContextProxy",
50
+
51
+ # assert
52
+ "SoftAssertor",
53
+ "AssertionExecutor",
54
+ "AssertParser",
55
+
56
+ # expression
57
+ "ExpressionEngine",
58
+ "RenderMode",
59
+
60
+ # extractor
61
+ "Extractor",
62
+
63
+ # enums
64
+ "SkipMode",
65
+ "BreakLoopMode",
66
+
67
+ # model & protocol
68
+ "TestCase",
69
+ "Step",
70
+ "LoopStep",
71
+ "OnFailure",
72
+ "StepData",
73
+ "ApiInstanceFactoryProtocol",
74
+
75
+ # runner entrypoint
76
+ "StepRunner",
77
+
78
+ "ToolRegistry",
79
+ ]
80
+
81
+ from .tool_registry import ToolRegistry
@@ -0,0 +1,9 @@
1
+ from .assert_excutor import AssertionExecutor
2
+ from .assert_parser import AssertParser
3
+ from .assertor import SoftAssertor
4
+
5
+ __all__ = [
6
+ 'AssertionExecutor',
7
+ 'AssertParser',
8
+ 'SoftAssertor',
9
+ ]
@@ -0,0 +1,40 @@
1
+ from typing import Optional
2
+
3
+ import allure
4
+
5
+ from .assert_parser import AssertParser
6
+ from .assertor import SoftAssertor
7
+
8
+
9
+ class AssertionExecutor:
10
+ """
11
+ 内核断言执行器
12
+ """
13
+
14
+ def __init__(self, soft_assertor: SoftAssertor, assert_parser: AssertParser):
15
+ self.soft: SoftAssertor = soft_assertor
16
+ self.parser: AssertParser = assert_parser
17
+
18
+ async def execute(self, response, base_expect, data_assert) -> tuple[Optional[str], Optional[str]]:
19
+ """
20
+ :return: (success_messages, fail_messages)
21
+ """
22
+ self.soft.clear_all()
23
+
24
+ if response and base_expect:
25
+ await self.parser.check_base_expect(response, base_expect)
26
+ if data_assert:
27
+ await self.parser.check_data_assert(response, data_assert)
28
+
29
+ success_msg = self.soft.get_all_msg(msg_type=1)
30
+ fail_msg = self.soft.get_all_msg(msg_type=2)
31
+
32
+ if success_msg:
33
+ allure.attach(success_msg, "成功断言明细", allure.attachment_type.TEXT)
34
+ if fail_msg:
35
+ allure.attach(fail_msg, "失败断言明细", allure.attachment_type.TEXT)
36
+
37
+ return success_msg, fail_msg
38
+
39
+ def clear(self) -> None:
40
+ self.soft.clear_all()
@@ -0,0 +1,193 @@
1
+ import re
2
+ from typing import Optional, Any
3
+
4
+ import jsonpath_ng.ext as jp
5
+
6
+ from yaml_test_core.core.context import TestContext
7
+ from yaml_test_core.core.model import ExpectConfig, DataAssert, BaseResponse
8
+ from yaml_test_core.core.expression.engine import ExpressionEngine
9
+ from .assertor import SoftAssertor
10
+
11
+
12
+ class AssertParser:
13
+ def __init__(
14
+ self,
15
+ soft_assertor: SoftAssertor,
16
+ engine: ExpressionEngine,
17
+ context: TestContext
18
+ ):
19
+ self.soft: SoftAssertor = soft_assertor
20
+ self.engine: ExpressionEngine = engine
21
+ self.context: TestContext = context
22
+
23
+ @staticmethod
24
+ def _get_json_value(resp_data: dict, json_path: str) -> Any:
25
+ """根据jsonpath获取返回值"""
26
+ expr = jp.parse(json_path)
27
+ match = expr.find(resp_data)
28
+ if not match:
29
+ return None
30
+ return [m.value for m in match] if len(match) > 1 else match[0].value
31
+
32
+ async def check_base_expect(self, resp: BaseResponse, expect: ExpectConfig):
33
+ """校验code、msg基础返回,异步"""
34
+ if expect.code is not None:
35
+ actual_code = resp.code
36
+ if isinstance(expect.code, int):
37
+ if actual_code != expect.code:
38
+ self.soft.add_fail(f"状态码断言失败 | 预期:{expect.code} | 实际:{actual_code}")
39
+ else:
40
+ self.soft.add_success(f"状态码断言通过 | 预期:{expect.code} | 实际:{actual_code}")
41
+ elif isinstance(expect.code, list):
42
+ if actual_code not in expect.code:
43
+ self.soft.add_fail(f"状态码断言失败 | 预期:{expect.code} | 实际:{actual_code}")
44
+ else:
45
+ self.soft.add_success(f"状态码断言通过 | 预期:{expect.code} | 实际:{actual_code}")
46
+
47
+ if expect.msg is not None:
48
+ actual_msg = resp.msg
49
+ if expect.msg.equals is not None:
50
+ if actual_msg != expect.msg.equals:
51
+ self.soft.add_fail(f"msg相等断言失败 | 预期:{expect.msg.equals} | 实际:{actual_msg}")
52
+ else:
53
+ self.soft.add_success(f"msg相等断言通过 | 预期:{expect.msg.equals} | 实际:{actual_msg}")
54
+ if expect.msg.contains is not None:
55
+ if expect.msg.contains not in actual_msg:
56
+ self.soft.add_fail(f"msg包含断言失败 | 预期包含:{expect.msg.contains} | 实际:{actual_msg}")
57
+ else:
58
+ self.soft.add_success(f"msg包含断言通过 | 预期包含:{expect.msg.contains} | 实际:{actual_msg}")
59
+
60
+ async def check_data_assert(self, resp: Optional[BaseResponse], data_assert: Optional[DataAssert] = None):
61
+ """执行全量深度数据断言,全部表达式渲染改为await"""
62
+ resp_data: Optional[dict] = None
63
+ if resp:
64
+ resp_data = resp.model_dump()
65
+ self.context.set("resp", resp_data)
66
+
67
+ if data_assert is None:
68
+ return
69
+
70
+ try:
71
+ if resp_data:
72
+ for path in data_assert.exists:
73
+ val = self._get_json_value(resp_data, path)
74
+ if val is None:
75
+ self.soft.add_fail(f"存在性断言失败 | 路径 {path} 不存在")
76
+ else:
77
+ self.soft.add_success(f"存在性断言成功 | 路径 {path} 存在")
78
+
79
+ for path in data_assert.not_exists:
80
+ val = self._get_json_value(resp_data, path)
81
+ if val is not None:
82
+ self.soft.add_fail(f"不存在断言失败 | 路径 {path} 不应存在")
83
+ else:
84
+ self.soft.add_success(f"不存在断言成功 | 路径 {path} 不应存在")
85
+
86
+ for path, compare in data_assert.array_length.items():
87
+ arr = self._get_json_value(resp_data, path)
88
+ if not isinstance(arr, list):
89
+ self.soft.add_fail(f"数组长度断言失败 | {path} 不是数组")
90
+ else:
91
+ compare_rendered = await self.engine.render(compare)
92
+ # 安全提示:eval存在字符串执行风险,仅限受控DSL内部使用
93
+ expr = f"{len(arr)} {compare_rendered}"
94
+ if not eval(expr):
95
+ self.soft.add_fail(f"数组长度断言失败 | {path} | 长度:{len(arr)} | 预期:{compare}")
96
+ else:
97
+ self.soft.add_success(f"数组长度断言成功| {path} | 长度:{len(arr)} | 预期:{compare}")
98
+
99
+ type_map = {
100
+ "str": str, "int": int, "array": list, "object": dict, "bool": bool
101
+ }
102
+ for path, target_type in data_assert.data_type.items():
103
+ val = self._get_json_value(resp_data, path)
104
+ target_cls = type_map[target_type]
105
+ if not isinstance(val, target_cls):
106
+ self.soft.add_fail(f"类型断言失败 | 路径:{path} | 预期:{target_cls.__name__} | 实际:{type(val).__name__}")
107
+ else:
108
+ self.soft.add_success(
109
+ f"类型断言成功 | 路径:{path} | 预期:{target_cls.__name__} | 实际:{type(val).__name__}")
110
+
111
+ for path, match_rule in data_assert.value_match.items():
112
+ if match_rule.equals is not None:
113
+ expect_val = await self.engine.render(match_rule.equals)
114
+ val = self._get_json_value(resp_data, path)
115
+ if val != expect_val:
116
+ self.soft.add_fail(f"等值断言失败 | 路径:{path} | 预期:{expect_val} | 实际:{val}")
117
+ else:
118
+ self.soft.add_success(f"等值断言成功 | 路径:{path} | 预期:{expect_val} | 实际:{val}")
119
+
120
+ if match_rule.contains is not None:
121
+ expect_contains = await self.engine.render(match_rule.contains)
122
+ val = self._get_json_value(resp_data, path)
123
+ if expect_contains not in str(val):
124
+ self.soft.add_fail(f"包含断言失败 | 路径:{path} | 预期包含:{expect_contains} | 实际:{val}")
125
+ else:
126
+ self.soft.add_success(f"包含断言成功 | 路径:{path} | 预期包含:{expect_contains} | 实际:{val}")
127
+
128
+ if match_rule.regex is not None:
129
+ expect_regex = await self.engine.render(match_rule.regex)
130
+ val = self._get_json_value(resp_data, path)
131
+ if not re.match(expect_regex, str(val)):
132
+ self.soft.add_fail(f"正则断言失败 | 路径:{path} | 正则:{expect_regex} | 实际:{val}")
133
+ else:
134
+ self.soft.add_success(f"正则断言成功 | 路径:{path} | 正则:{expect_regex} | 实际:{val}")
135
+
136
+ if match_rule.in_ is not None:
137
+ expect_in = await self.engine.render(match_rule.in_)
138
+ val = self._get_json_value(resp_data, path)
139
+ if val not in expect_in:
140
+ self.soft.add_fail(f"枚举断言失败 | 路径:{path} | 预期在:{expect_in} | 实际:{val}")
141
+ else:
142
+ self.soft.add_success(f"枚举断言成功 | 路径:{path} | 预期在:{expect_in} | 实际:{val}")
143
+
144
+ for path in data_assert.is_null:
145
+ val = self._get_json_value(resp_data, path)
146
+ if val is None:
147
+ self.soft.add_success(f"空值断言成功|路径 {path} 值为null")
148
+ else:
149
+ self.soft.add_fail(f"空值断言失败|路径 {path}|预期null | 实际:{val}")
150
+
151
+ for path in data_assert.not_null:
152
+ val = self._get_json_value(resp_data, path)
153
+ if val is not None:
154
+ self.soft.add_success(f"非空值断言成功|路径 {path}|预期:不为null | 实际:{val}")
155
+ else:
156
+ self.soft.add_fail(f"非空值断言失败|路径 {path}|预期:不为null | 实际:{val}")
157
+
158
+ for path in data_assert.is_empty:
159
+ val = self._get_json_value(resp_data, path)
160
+ empty_flag = val in ("", [], {})
161
+ if empty_flag:
162
+ self.soft.add_success(f"空集合断言成功|路径 {path}|值为空")
163
+ else:
164
+ self.soft.add_fail(f"空集合断言失败|路径 {path}|预期为空,实际:{val}")
165
+
166
+ for path in data_assert.not_empty:
167
+ val = self._get_json_value(resp_data, path)
168
+ empty_flag = val in ("", [], {})
169
+ if not empty_flag:
170
+ self.soft.add_success(f"非空集合断言成功|路径 {path}|不为空,实际:{val}")
171
+ else:
172
+ self.soft.add_fail(f"非空集合断言失败|路径 {path}|预期非空,实际为空:{val}")
173
+
174
+ for expr_template in data_assert.expr:
175
+ try:
176
+ result = await self.engine.render(expr_template)
177
+
178
+ if isinstance(result, list):
179
+ if len(result) > 0:
180
+ for msg in result:
181
+ self.soft.add_fail(f"表达式断言失败 | {expr_template} | {msg}")
182
+ else:
183
+ self.soft.add_success(f"表达式断言通过 | {expr_template}")
184
+ continue
185
+
186
+ if not result:
187
+ self.soft.add_fail(f"表达式断言异常 | {expr_template}")
188
+
189
+ except Exception:
190
+ self.soft.add_fail(f"表达式执行异常 | {expr_template}")
191
+ raise
192
+ finally:
193
+ self.context.pop("resp", None)
@@ -0,0 +1,123 @@
1
+ from typing import Any, TypeVar
2
+ from httpx import Response
3
+ from pydantic import BaseModel, ValidationError
4
+
5
+ from ..exceptions import AssertFailedError
6
+
7
+ T = TypeVar("T", bound=BaseModel)
8
+
9
+
10
+ class Assertor:
11
+ """统一硬断言引擎"""
12
+
13
+ @staticmethod
14
+ def assert_status_code(response: Response, expected: int = 200) -> None:
15
+ """校验HTTP状态码"""
16
+ actual = response.status_code
17
+ if actual != expected:
18
+ raise AssertFailedError(
19
+ f"HTTP状态码校验失败\n预期: {expected}\n实际: {actual}\n响应内容: {response.text}"
20
+ )
21
+
22
+ @staticmethod
23
+ def assert_schema(response: Response, model: type[T]) -> T:
24
+ """校验响应JSON结构符合Pydantic模型,并返回模型实例"""
25
+ try:
26
+ return model(**response.json())
27
+ except ValidationError as e:
28
+ raise AssertFailedError(f"响应Schema校验失败\n错误详情: {str(e)}")
29
+ except Exception as e:
30
+ raise AssertFailedError(
31
+ f"响应解析失败: {str(e)}\n响应内容: {response.text}"
32
+ )
33
+
34
+ @staticmethod
35
+ def assert_equal(actual: Any, expected: Any, msg: str = "") -> None:
36
+ """通用相等断言"""
37
+ if actual != expected:
38
+ raise AssertFailedError(f"{msg}校验失败\n预期: {expected}\n实际: {actual}")
39
+
40
+ @staticmethod
41
+ def assert_not_equal(actual: Any, expected: Any, msg: str = "") -> None:
42
+ """通用不等断言"""
43
+ if actual == expected:
44
+ raise AssertFailedError(
45
+ f"{msg} 校验失败\n预期不等于: {expected}\n实际: {actual}"
46
+ )
47
+
48
+ @staticmethod
49
+ def assert_contains(
50
+ container: str | list | dict, target: Any, msg: str = ""
51
+ ) -> None:
52
+ """包含断言"""
53
+ if target not in container:
54
+ raise AssertFailedError(
55
+ f"{msg} 包含校验失败\n目标: {target}\n容器: {container}"
56
+ )
57
+
58
+ @staticmethod
59
+ def assert_true(condition: bool, msg: str = "") -> None:
60
+ """真值断言"""
61
+ if not condition:
62
+ raise AssertFailedError(f"{msg} 真值校验失败,条件为False")
63
+
64
+
65
+ class SoftAssertor:
66
+ def __init__(self, record_success: bool = False):
67
+ """
68
+ :param record_success: 是否记录成功断言消息,由业务层传入,替代全局settings
69
+ """
70
+ self.fail_list: list[str] = []
71
+ self.success_list: list[str] = []
72
+ self.record_success = record_success
73
+
74
+ def add_fail(self, msg: str) -> None:
75
+ self.fail_list.append(msg)
76
+
77
+ def add_success(self, msg: str) -> None:
78
+ if self.record_success:
79
+ self.success_list.append(msg)
80
+
81
+ def clear_success(self) -> None:
82
+ self.success_list.clear()
83
+
84
+ def clear_fail(self) -> None:
85
+ self.fail_list.clear()
86
+
87
+ def clear_all(self) -> None:
88
+ self.clear_success()
89
+ self.clear_fail()
90
+
91
+ def assert_all(self) -> str | None:
92
+ """返回失败汇总文本,不主动抛异常;返回None代表无失败"""
93
+ if self.fail_list:
94
+ fail_msg = "\n".join([f"- {item}" for item in self.fail_list])
95
+ return f"存在{len(self.fail_list)}处断言失败:\n{fail_msg}"
96
+ return None
97
+
98
+ def get_all_fail_msg(self) -> str | None:
99
+ """仅返回失败文本,不主动抛错"""
100
+ if not self.fail_list:
101
+ return None
102
+ lines = [f"{idx + 1}. {item}" for idx, item in enumerate(self.fail_list)]
103
+ return "\n".join(lines)
104
+
105
+ def get_msg_list(self, msg_type: int) -> list[str]:
106
+ """
107
+ 获取原始消息列表,优先推荐使用;
108
+ :param msg_type: 1‑success 2‑fail
109
+ """
110
+ if msg_type == 1:
111
+ return self.success_list.copy()
112
+ return self.fail_list.copy()
113
+
114
+ def get_all_msg(self, msg_type: int) -> str | None:
115
+ """
116
+ 兼容旧接口:返回拼接后的字符串
117
+ :param msg_type: 类型 1‑success 2‑fail
118
+ """
119
+ lst = self.get_msg_list(msg_type)
120
+ if not lst:
121
+ return None
122
+ lines = [f"{idx + 1}. {item}" for idx, item in enumerate(lst)]
123
+ return "\n".join(lines)