pytest-dsl 0.1.0__py3-none-any.whl
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.
- pytest_dsl/__init__.py +10 -0
- pytest_dsl/cli.py +44 -0
- pytest_dsl/conftest_adapter.py +4 -0
- pytest_dsl/core/__init__.py +0 -0
- pytest_dsl/core/auth_provider.py +409 -0
- pytest_dsl/core/auto_decorator.py +181 -0
- pytest_dsl/core/auto_directory.py +81 -0
- pytest_dsl/core/context.py +23 -0
- pytest_dsl/core/custom_auth_example.py +425 -0
- pytest_dsl/core/dsl_executor.py +329 -0
- pytest_dsl/core/dsl_executor_utils.py +84 -0
- pytest_dsl/core/global_context.py +103 -0
- pytest_dsl/core/http_client.py +411 -0
- pytest_dsl/core/http_request.py +810 -0
- pytest_dsl/core/keyword_manager.py +109 -0
- pytest_dsl/core/lexer.py +139 -0
- pytest_dsl/core/parser.py +197 -0
- pytest_dsl/core/parsetab.py +76 -0
- pytest_dsl/core/plugin_discovery.py +187 -0
- pytest_dsl/core/utils.py +146 -0
- pytest_dsl/core/variable_utils.py +267 -0
- pytest_dsl/core/yaml_loader.py +62 -0
- pytest_dsl/core/yaml_vars.py +75 -0
- pytest_dsl/docs/custom_keywords.md +140 -0
- pytest_dsl/examples/__init__.py +5 -0
- pytest_dsl/examples/assert/assertion_example.auto +44 -0
- pytest_dsl/examples/assert/boolean_test.auto +34 -0
- pytest_dsl/examples/assert/expression_test.auto +49 -0
- pytest_dsl/examples/http/__init__.py +3 -0
- pytest_dsl/examples/http/builtin_auth_test.auto +79 -0
- pytest_dsl/examples/http/csrf_auth_test.auto +64 -0
- pytest_dsl/examples/http/custom_auth_test.auto +76 -0
- pytest_dsl/examples/http/file_reference_test.auto +111 -0
- pytest_dsl/examples/http/http_advanced.auto +91 -0
- pytest_dsl/examples/http/http_example.auto +147 -0
- pytest_dsl/examples/http/http_length_test.auto +55 -0
- pytest_dsl/examples/http/http_retry_assertions.auto +91 -0
- pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +94 -0
- pytest_dsl/examples/http/http_with_yaml.auto +58 -0
- pytest_dsl/examples/http/new_retry_test.auto +22 -0
- pytest_dsl/examples/http/retry_assertions_only.auto +52 -0
- pytest_dsl/examples/http/retry_config_only.auto +49 -0
- pytest_dsl/examples/http/retry_debug.auto +22 -0
- pytest_dsl/examples/http/retry_with_fix.auto +21 -0
- pytest_dsl/examples/http/simple_retry.auto +20 -0
- pytest_dsl/examples/http/vars.yaml +55 -0
- pytest_dsl/examples/http_clients.yaml +48 -0
- pytest_dsl/examples/keyword_example.py +70 -0
- pytest_dsl/examples/test_assert.py +16 -0
- pytest_dsl/examples/test_http.py +168 -0
- pytest_dsl/keywords/__init__.py +10 -0
- pytest_dsl/keywords/assertion_keywords.py +610 -0
- pytest_dsl/keywords/global_keywords.py +51 -0
- pytest_dsl/keywords/http_keywords.py +430 -0
- pytest_dsl/keywords/system_keywords.py +17 -0
- pytest_dsl/main_adapter.py +7 -0
- pytest_dsl/plugin.py +44 -0
- pytest_dsl-0.1.0.dist-info/METADATA +537 -0
- pytest_dsl-0.1.0.dist-info/RECORD +63 -0
- pytest_dsl-0.1.0.dist-info/WHEEL +5 -0
- pytest_dsl-0.1.0.dist-info/entry_points.txt +5 -0
- pytest_dsl-0.1.0.dist-info/licenses/LICENSE +21 -0
- pytest_dsl-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,430 @@
|
|
1
|
+
"""HTTP请求关键字模块
|
2
|
+
|
3
|
+
该模块提供了用于发送HTTP请求、捕获响应和断言的关键字。
|
4
|
+
"""
|
5
|
+
|
6
|
+
import allure
|
7
|
+
import re
|
8
|
+
import yaml
|
9
|
+
import json
|
10
|
+
import os
|
11
|
+
import time
|
12
|
+
from typing import Dict, Any, Union
|
13
|
+
|
14
|
+
from pytest_dsl.core.keyword_manager import keyword_manager
|
15
|
+
from pytest_dsl.core.http_request import HTTPRequest
|
16
|
+
from pytest_dsl.core.yaml_vars import yaml_vars
|
17
|
+
from pytest_dsl.core.context import TestContext
|
18
|
+
|
19
|
+
def _process_file_reference(reference: Union[str, Dict[str, Any]], allow_vars: bool = True, test_context: TestContext = None) -> Any:
|
20
|
+
"""处理文件引用,加载外部文件内容
|
21
|
+
|
22
|
+
支持两种语法:
|
23
|
+
1. 简单语法: "@file:/path/to/file.json" 或 "@file_template:/path/to/file.json"
|
24
|
+
2. 详细语法: 使用file_ref结构提供更多的配置选项
|
25
|
+
|
26
|
+
Args:
|
27
|
+
reference: 文件引用字符串或配置字典
|
28
|
+
allow_vars: 是否允许在文件内容中替换变量
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
加载并处理后的文件内容
|
32
|
+
"""
|
33
|
+
# 处理简单语法
|
34
|
+
if isinstance(reference, str):
|
35
|
+
# 匹配简单文件引用语法
|
36
|
+
file_ref_pattern = r'^@file(?:_template)?:(.+)$'
|
37
|
+
match = re.match(file_ref_pattern, reference.strip())
|
38
|
+
|
39
|
+
if match:
|
40
|
+
file_path = match.group(1).strip()
|
41
|
+
is_template = '_template' in reference[:15] # 检查是否为模板
|
42
|
+
return _load_file_content(file_path, is_template, 'auto', 'utf-8', test_context)
|
43
|
+
|
44
|
+
# 处理详细语法
|
45
|
+
elif isinstance(reference, dict) and 'file_ref' in reference:
|
46
|
+
file_ref = reference['file_ref']
|
47
|
+
|
48
|
+
if isinstance(file_ref, str):
|
49
|
+
# 如果file_ref是字符串,使用默认配置
|
50
|
+
return _load_file_content(file_ref, allow_vars, 'auto', 'utf-8', test_context)
|
51
|
+
elif isinstance(file_ref, dict):
|
52
|
+
# 如果file_ref是字典,使用自定义配置
|
53
|
+
file_path = file_ref.get('path')
|
54
|
+
if not file_path:
|
55
|
+
raise ValueError("file_ref必须包含path字段")
|
56
|
+
|
57
|
+
template = file_ref.get('template', allow_vars)
|
58
|
+
file_type = file_ref.get('type', 'auto')
|
59
|
+
encoding = file_ref.get('encoding', 'utf-8')
|
60
|
+
|
61
|
+
return _load_file_content(file_path, template, file_type, encoding, test_context)
|
62
|
+
|
63
|
+
# 如果不是文件引用,返回原始值
|
64
|
+
return reference
|
65
|
+
|
66
|
+
|
67
|
+
def _load_file_content(file_path: str, is_template: bool = False,
|
68
|
+
file_type: str = 'auto', encoding: str = 'utf-8', test_context: TestContext = None) -> Any:
|
69
|
+
"""加载文件内容
|
70
|
+
|
71
|
+
Args:
|
72
|
+
file_path: 文件路径
|
73
|
+
is_template: 是否作为模板处理(替换变量引用)
|
74
|
+
file_type: 文件类型 (auto, json, yaml, text)
|
75
|
+
encoding: 文件编码
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
加载并处理后的文件内容
|
79
|
+
"""
|
80
|
+
# 验证文件存在
|
81
|
+
if not os.path.exists(file_path):
|
82
|
+
raise FileNotFoundError(f"找不到引用的文件: {file_path}")
|
83
|
+
|
84
|
+
# 读取文件内容
|
85
|
+
with open(file_path, 'r', encoding=encoding) as f:
|
86
|
+
content = f.read()
|
87
|
+
|
88
|
+
# 如果是模板,处理变量替换
|
89
|
+
if is_template:
|
90
|
+
from pytest_dsl.core.variable_utils import VariableReplacer
|
91
|
+
replacer = VariableReplacer(test_context=test_context)
|
92
|
+
content = replacer.replace_in_string(content)
|
93
|
+
|
94
|
+
# 根据文件类型处理内容
|
95
|
+
if file_type == 'auto':
|
96
|
+
# 根据文件扩展名自动检测类型
|
97
|
+
file_ext = os.path.splitext(file_path)[1].lower()
|
98
|
+
if file_ext in ['.json']:
|
99
|
+
file_type = 'json'
|
100
|
+
elif file_ext in ['.yaml', '.yml']:
|
101
|
+
file_type = 'yaml'
|
102
|
+
else:
|
103
|
+
file_type = 'text'
|
104
|
+
|
105
|
+
# 处理不同类型的文件
|
106
|
+
if file_type == 'json':
|
107
|
+
try:
|
108
|
+
return json.loads(content)
|
109
|
+
except json.JSONDecodeError as e:
|
110
|
+
raise ValueError(f"无效的JSON文件 {file_path}: {str(e)}")
|
111
|
+
elif file_type == 'yaml':
|
112
|
+
try:
|
113
|
+
return yaml.safe_load(content)
|
114
|
+
except yaml.YAMLError as e:
|
115
|
+
raise ValueError(f"无效的YAML文件 {file_path}: {str(e)}")
|
116
|
+
else:
|
117
|
+
# 文本文件直接返回内容
|
118
|
+
return content
|
119
|
+
|
120
|
+
|
121
|
+
def _process_request_config(config: Dict[str, Any], test_context: TestContext = None) -> Dict[str, Any]:
|
122
|
+
"""处理请求配置,检查并处理文件引用
|
123
|
+
|
124
|
+
Args:
|
125
|
+
config: 请求配置
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
处理后的请求配置
|
129
|
+
"""
|
130
|
+
if not isinstance(config, dict):
|
131
|
+
return config
|
132
|
+
|
133
|
+
# 处理request部分
|
134
|
+
if 'request' in config and isinstance(config['request'], dict):
|
135
|
+
request = config['request']
|
136
|
+
|
137
|
+
# 处理json字段
|
138
|
+
if 'json' in request:
|
139
|
+
request['json'] = _process_file_reference(request['json'], test_context=test_context)
|
140
|
+
|
141
|
+
# 处理data字段
|
142
|
+
if 'data' in request:
|
143
|
+
request['data'] = _process_file_reference(request['data'], test_context=test_context)
|
144
|
+
|
145
|
+
# 处理headers字段
|
146
|
+
if 'headers' in request:
|
147
|
+
request['headers'] = _process_file_reference(request['headers'], test_context=test_context)
|
148
|
+
|
149
|
+
return config
|
150
|
+
|
151
|
+
|
152
|
+
@keyword_manager.register('HTTP请求', [
|
153
|
+
{'name': '客户端', 'mapping': 'client', 'description': '客户端名称,对应YAML变量文件中的客户端配置'},
|
154
|
+
{'name': '配置', 'mapping': 'config', 'description': '包含请求、捕获和断言的YAML配置'},
|
155
|
+
{'name': '会话', 'mapping': 'session', 'description': '会话名称,用于在多个请求间保持会话状态'},
|
156
|
+
{'name': '保存响应', 'mapping': 'save_response', 'description': '将完整响应保存到指定变量名中'},
|
157
|
+
{'name': '重试次数', 'mapping': 'retry_count', 'description': '请求失败时的重试次数'},
|
158
|
+
{'name': '重试间隔', 'mapping': 'retry_interval', 'description': '重试间隔时间(秒)'},
|
159
|
+
{'name': '模板', 'mapping': 'template', 'description': '使用YAML变量文件中定义的请求模板'},
|
160
|
+
{'name': '断言重试次数', 'mapping': 'assert_retry_count', 'description': '断言失败时的重试次数'},
|
161
|
+
{'name': '断言重试间隔', 'mapping': 'assert_retry_interval', 'description': '断言重试间隔时间(秒)'}
|
162
|
+
])
|
163
|
+
def http_request(context, **kwargs):
|
164
|
+
"""执行HTTP请求
|
165
|
+
|
166
|
+
根据YAML配置发送HTTP请求,支持客户端配置、会话管理、响应捕获和断言。
|
167
|
+
|
168
|
+
Args:
|
169
|
+
context: 测试上下文
|
170
|
+
client: 客户端名称
|
171
|
+
config: YAML配置
|
172
|
+
session: 会话名称
|
173
|
+
save_response: 保存响应的变量名
|
174
|
+
retry_count: 重试次数
|
175
|
+
retry_interval: 重试间隔
|
176
|
+
template: 模板名称
|
177
|
+
assert_retry_count: 断言失败时的重试次数
|
178
|
+
assert_retry_interval: 断言重试间隔时间(秒)
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
捕获的变量字典或响应对象
|
182
|
+
"""
|
183
|
+
client_name = kwargs.get('client', 'default')
|
184
|
+
config = kwargs.get('config', '{}')
|
185
|
+
session_name = kwargs.get('session')
|
186
|
+
save_response = kwargs.get('save_response')
|
187
|
+
retry_count = kwargs.get('retry_count')
|
188
|
+
retry_interval = kwargs.get('retry_interval')
|
189
|
+
template_name = kwargs.get('template')
|
190
|
+
assert_retry_count = kwargs.get('assert_retry_count')
|
191
|
+
assert_retry_interval = kwargs.get('assert_retry_interval')
|
192
|
+
|
193
|
+
with allure.step(f"发送HTTP请求 (客户端: {client_name}{', 会话: ' + session_name if session_name else ''})"):
|
194
|
+
# 处理模板
|
195
|
+
if template_name:
|
196
|
+
# 从YAML变量中获取模板
|
197
|
+
http_templates = yaml_vars.get_variable("http_templates") or {}
|
198
|
+
template = http_templates.get(template_name)
|
199
|
+
|
200
|
+
if not template:
|
201
|
+
raise ValueError(f"未找到名为 '{template_name}' 的HTTP请求模板")
|
202
|
+
|
203
|
+
# 解析配置并合并模板
|
204
|
+
if isinstance(config, str):
|
205
|
+
# 先进行变量替换,再解析YAML
|
206
|
+
from pytest_dsl.core.variable_utils import VariableReplacer
|
207
|
+
replacer = VariableReplacer(test_context=context)
|
208
|
+
config = replacer.replace_in_string(config)
|
209
|
+
try:
|
210
|
+
user_config = yaml.safe_load(config) if config else {}
|
211
|
+
|
212
|
+
# 深度合并
|
213
|
+
merged_config = _deep_merge(template.copy(), user_config)
|
214
|
+
config = merged_config
|
215
|
+
except yaml.YAMLError as e:
|
216
|
+
raise ValueError(f"无效的YAML配置: {str(e)}")
|
217
|
+
else:
|
218
|
+
# 如果没有使用模板,直接对配置字符串进行变量替换
|
219
|
+
if isinstance(config, str):
|
220
|
+
from pytest_dsl.core.variable_utils import VariableReplacer
|
221
|
+
replacer = VariableReplacer(test_context=context)
|
222
|
+
config = replacer.replace_in_string(config)
|
223
|
+
|
224
|
+
# 解析YAML配置
|
225
|
+
if isinstance(config, str):
|
226
|
+
try:
|
227
|
+
config = yaml.safe_load(config)
|
228
|
+
except yaml.YAMLError as e:
|
229
|
+
raise ValueError(f"无效的YAML配置: {str(e)}")
|
230
|
+
|
231
|
+
# 如果提供了命令行级别的断言重试参数,将其添加到新的retry_assertions配置中
|
232
|
+
if assert_retry_count and int(assert_retry_count) > 0:
|
233
|
+
# 检查配置中是否已经有retry_assertions配置
|
234
|
+
if 'retry_assertions' not in config:
|
235
|
+
config['retry_assertions'] = {}
|
236
|
+
|
237
|
+
# 设置全局重试次数和间隔
|
238
|
+
config['retry_assertions']['count'] = int(assert_retry_count)
|
239
|
+
config['retry_assertions']['all'] = True
|
240
|
+
if assert_retry_interval:
|
241
|
+
config['retry_assertions']['interval'] = float(assert_retry_interval)
|
242
|
+
|
243
|
+
# 向后兼容:同时设置旧格式的retry配置
|
244
|
+
if 'retry' not in config:
|
245
|
+
config['retry'] = {}
|
246
|
+
config['retry']['count'] = int(assert_retry_count)
|
247
|
+
if assert_retry_interval:
|
248
|
+
config['retry']['interval'] = float(assert_retry_interval)
|
249
|
+
|
250
|
+
config = _process_request_config(config, test_context=context)
|
251
|
+
|
252
|
+
# 创建HTTP请求对象
|
253
|
+
http_req = HTTPRequest(config, client_name, session_name)
|
254
|
+
|
255
|
+
# 执行请求
|
256
|
+
response = http_req.execute()
|
257
|
+
|
258
|
+
# 处理捕获
|
259
|
+
captured_values = http_req.captured_values
|
260
|
+
|
261
|
+
# 将捕获的变量注册到上下文
|
262
|
+
for var_name, value in captured_values.items():
|
263
|
+
context.set(var_name, value)
|
264
|
+
|
265
|
+
# 保存完整响应(如果需要)
|
266
|
+
if save_response:
|
267
|
+
context.set(save_response, response)
|
268
|
+
|
269
|
+
# 检查是否有配置中的断言重试设置
|
270
|
+
has_retry_assertions = 'retry_assertions' in config
|
271
|
+
has_legacy_retry = 'retry' in config
|
272
|
+
|
273
|
+
# 处理断言(支持配置中的重试设置)
|
274
|
+
if has_retry_assertions or has_legacy_retry:
|
275
|
+
# 使用配置式断言重试
|
276
|
+
with allure.step("执行配置式断言验证(支持选择性重试)"):
|
277
|
+
_process_config_based_assertions_with_retry(http_req)
|
278
|
+
elif assert_retry_count and int(assert_retry_count) > 0:
|
279
|
+
# 向后兼容:使用传统的断言重试
|
280
|
+
_process_assertions_with_retry(http_req, int(assert_retry_count),
|
281
|
+
float(assert_retry_interval) if assert_retry_interval else 1.0)
|
282
|
+
else:
|
283
|
+
# 不需要重试,直接断言
|
284
|
+
http_req.process_asserts()
|
285
|
+
|
286
|
+
# 返回捕获的变量
|
287
|
+
return captured_values
|
288
|
+
|
289
|
+
|
290
|
+
def _deep_merge(dict1, dict2):
|
291
|
+
"""深度合并两个字典
|
292
|
+
|
293
|
+
Args:
|
294
|
+
dict1: 基础字典(会被修改)
|
295
|
+
dict2: 要合并的字典(优先级更高)
|
296
|
+
|
297
|
+
Returns:
|
298
|
+
合并后的字典
|
299
|
+
"""
|
300
|
+
for key in dict2:
|
301
|
+
if key in dict1 and isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
302
|
+
_deep_merge(dict1[key], dict2[key])
|
303
|
+
else:
|
304
|
+
dict1[key] = dict2[key]
|
305
|
+
return dict1
|
306
|
+
|
307
|
+
|
308
|
+
def _process_assertions_with_retry(http_req, retry_count, retry_interval):
|
309
|
+
"""处理断言并支持重试
|
310
|
+
|
311
|
+
Args:
|
312
|
+
http_req: HTTP请求对象
|
313
|
+
retry_count: 重试次数
|
314
|
+
retry_interval: 重试间隔(秒)
|
315
|
+
"""
|
316
|
+
|
317
|
+
for attempt in range(retry_count + 1):
|
318
|
+
try:
|
319
|
+
# 尝试执行断言
|
320
|
+
with allure.step(f"断言验证 (尝试 {attempt + 1}/{retry_count + 1})"):
|
321
|
+
# 修改为获取断言结果和失败的可重试断言
|
322
|
+
results, failed_retryable_assertions = http_req.process_asserts()
|
323
|
+
# 断言成功,直接返回
|
324
|
+
return results
|
325
|
+
except AssertionError as e:
|
326
|
+
# 如果还有重试机会,等待后重试
|
327
|
+
if attempt < retry_count:
|
328
|
+
with allure.step(f"断言失败,等待 {retry_interval} 秒后重试"):
|
329
|
+
allure.attach(
|
330
|
+
str(e),
|
331
|
+
name="断言失败详情",
|
332
|
+
attachment_type=allure.attachment_type.TEXT
|
333
|
+
)
|
334
|
+
time.sleep(retry_interval)
|
335
|
+
# 重新发送请求
|
336
|
+
http_req.execute()
|
337
|
+
else:
|
338
|
+
# 重试次数用完,重新抛出异常
|
339
|
+
raise
|
340
|
+
|
341
|
+
|
342
|
+
def _process_config_based_assertions_with_retry(http_req):
|
343
|
+
"""基于配置处理断言重试
|
344
|
+
|
345
|
+
支持以下重试配置格式:
|
346
|
+
1. 关键字级别参数: assert_retry_count, assert_retry_interval
|
347
|
+
2. 全局配置: retry: {count: 3, interval: 1}
|
348
|
+
3. 独立重试配置: retry_assertions: {...}
|
349
|
+
|
350
|
+
Args:
|
351
|
+
http_req: HTTP请求对象
|
352
|
+
|
353
|
+
Returns:
|
354
|
+
断言结果列表
|
355
|
+
"""
|
356
|
+
# 尝试执行所有断言
|
357
|
+
try:
|
358
|
+
results, failed_retryable_assertions = http_req.process_asserts()
|
359
|
+
return results # 如果所有断言都通过,直接返回结果
|
360
|
+
except AssertionError:
|
361
|
+
# 有断言失败,需要进行重试
|
362
|
+
if not failed_retryable_assertions:
|
363
|
+
# 没有可重试的断言,重新抛出异常
|
364
|
+
raise
|
365
|
+
|
366
|
+
# 开始重试循环
|
367
|
+
max_retry_count = 3 # 默认重试次数
|
368
|
+
|
369
|
+
# 找出所有断言中最大的重试次数
|
370
|
+
for failed_assertion in failed_retryable_assertions:
|
371
|
+
max_retry_count = max(max_retry_count, failed_assertion.get('retry_count', 3))
|
372
|
+
|
373
|
+
# 断言重试
|
374
|
+
for attempt in range(1, max_retry_count + 1): # 从1开始,因为第0次已经尝试过了
|
375
|
+
# 等待重试间隔
|
376
|
+
with allure.step(f"断言重试 (尝试 {attempt + 1}/{max_retry_count + 1})"):
|
377
|
+
# 确定本次重试的间隔时间(使用每个断言中最长的间隔时间)
|
378
|
+
retry_interval = 1.0 # 默认间隔时间
|
379
|
+
for failed_assertion in failed_retryable_assertions:
|
380
|
+
retry_interval = max(retry_interval, failed_assertion.get('retry_interval', 1.0))
|
381
|
+
|
382
|
+
allure.attach(
|
383
|
+
f"重试 {len(failed_retryable_assertions)} 个可重试断言\n"
|
384
|
+
f"等待间隔: {retry_interval}秒",
|
385
|
+
name="断言重试信息",
|
386
|
+
attachment_type=allure.attachment_type.TEXT
|
387
|
+
)
|
388
|
+
|
389
|
+
time.sleep(retry_interval)
|
390
|
+
|
391
|
+
# 重新发送请求
|
392
|
+
http_req.execute()
|
393
|
+
|
394
|
+
# 过滤出仍在重试范围内的断言
|
395
|
+
still_retryable_assertions = []
|
396
|
+
for failed_assertion in failed_retryable_assertions:
|
397
|
+
assertion_retry_count = failed_assertion.get('retry_count', 3)
|
398
|
+
|
399
|
+
# 如果断言的重试次数大于当前尝试次数,继续重试该断言
|
400
|
+
if attempt < assertion_retry_count:
|
401
|
+
still_retryable_assertions.append(failed_assertion)
|
402
|
+
|
403
|
+
# 如果没有可以继续重试的断言,跳出循环
|
404
|
+
if not still_retryable_assertions:
|
405
|
+
break
|
406
|
+
|
407
|
+
# 只重试那些仍在重试范围内的断言
|
408
|
+
try:
|
409
|
+
# 从原始断言配置中提取出需要重试的断言
|
410
|
+
retry_assertion_indexes = [a['index'] for a in still_retryable_assertions]
|
411
|
+
retry_assertions = [http_req.config.get('asserts', [])[idx] for idx in retry_assertion_indexes]
|
412
|
+
|
413
|
+
# 只处理需要重试的断言
|
414
|
+
results, new_failed_assertions = http_req.process_asserts(specific_asserts=retry_assertions)
|
415
|
+
|
416
|
+
# 如果所有断言都通过了,返回结果
|
417
|
+
if not new_failed_assertions:
|
418
|
+
# 执行一次完整的断言检查,确保所有断言都通过
|
419
|
+
return http_req.process_asserts()[0]
|
420
|
+
|
421
|
+
# 更新失败的可重试断言列表
|
422
|
+
failed_retryable_assertions = new_failed_assertions
|
423
|
+
|
424
|
+
except AssertionError:
|
425
|
+
# 断言仍然失败,继续重试
|
426
|
+
continue
|
427
|
+
|
428
|
+
# 重试次数用完,执行一次完整的断言以获取最终结果和错误
|
429
|
+
# 这会抛出异常,如果仍然有断言失败
|
430
|
+
return http_req.process_asserts()[0]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import allure
|
2
|
+
from pytest_dsl.core.keyword_manager import keyword_manager
|
3
|
+
|
4
|
+
|
5
|
+
@keyword_manager.register('打印', [
|
6
|
+
{'name': '内容', 'mapping': 'content', 'description': '要打印的文本内容'}
|
7
|
+
])
|
8
|
+
def print_content(**kwargs):
|
9
|
+
content = kwargs.get('content')
|
10
|
+
print(f"内容: {content}")
|
11
|
+
|
12
|
+
|
13
|
+
@keyword_manager.register('返回结果', [
|
14
|
+
{'name': '结果', 'mapping': 'result', 'description': '要返回的结果值'}
|
15
|
+
])
|
16
|
+
def return_result(**kwargs):
|
17
|
+
return kwargs.get('result')
|
pytest_dsl/plugin.py
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
"""pytest-dsl插件的主要入口文件
|
2
|
+
|
3
|
+
该文件负责将DSL功能集成到pytest框架中,包括命令行参数处理、YAML变量加载、
|
4
|
+
自定义目录收集器等功能。
|
5
|
+
"""
|
6
|
+
import pytest
|
7
|
+
import os
|
8
|
+
from pathlib import Path
|
9
|
+
|
10
|
+
# 导入模块化组件
|
11
|
+
from pytest_dsl.core.yaml_loader import add_yaml_options, load_yaml_variables
|
12
|
+
from pytest_dsl.core.plugin_discovery import load_all_plugins, scan_local_keywords
|
13
|
+
from pytest_dsl.core.global_context import global_context
|
14
|
+
|
15
|
+
|
16
|
+
def pytest_addoption(parser):
|
17
|
+
"""添加命令行参数选项
|
18
|
+
|
19
|
+
Args:
|
20
|
+
parser: pytest命令行参数解析器
|
21
|
+
"""
|
22
|
+
# 使用yaml_loader模块添加YAML相关选项
|
23
|
+
add_yaml_options(parser)
|
24
|
+
|
25
|
+
|
26
|
+
@pytest.hookimpl
|
27
|
+
def pytest_configure(config):
|
28
|
+
"""配置测试会话,加载已执行的setup/teardown信息和YAML变量
|
29
|
+
|
30
|
+
Args:
|
31
|
+
config: pytest配置对象
|
32
|
+
"""
|
33
|
+
|
34
|
+
# 加载YAML变量文件
|
35
|
+
load_yaml_variables(config)
|
36
|
+
|
37
|
+
# 确保全局变量存储目录存在
|
38
|
+
os.makedirs(global_context._storage_dir, exist_ok=True)
|
39
|
+
|
40
|
+
# 加载所有已安装的关键字插件
|
41
|
+
load_all_plugins()
|
42
|
+
|
43
|
+
# 加载本地关键字(向后兼容)
|
44
|
+
scan_local_keywords()
|