pytest-dsl 0.1.0__py3-none-any.whl → 0.2.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/core/__init__.py +7 -0
- pytest_dsl/core/auth_provider.py +50 -10
- pytest_dsl/core/custom_keyword_manager.py +213 -0
- pytest_dsl/core/dsl_executor.py +39 -2
- pytest_dsl/core/http_client.py +11 -6
- pytest_dsl/core/http_request.py +517 -119
- pytest_dsl/core/lexer.py +14 -1
- pytest_dsl/core/parser.py +45 -2
- pytest_dsl/core/parsetab.py +50 -38
- pytest_dsl/core/variable_utils.py +1 -1
- pytest_dsl/examples/custom/test_advanced_keywords.auto +31 -0
- pytest_dsl/examples/custom/test_custom_keywords.auto +37 -0
- pytest_dsl/examples/custom/test_default_values.auto +34 -0
- pytest_dsl/examples/http/http_retry_assertions.auto +2 -2
- pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +2 -2
- pytest_dsl/examples/quickstart/api_basics.auto +55 -0
- pytest_dsl/examples/quickstart/assertions.auto +31 -0
- pytest_dsl/examples/quickstart/loops.auto +24 -0
- pytest_dsl/examples/test_custom_keyword.py +9 -0
- pytest_dsl/examples/test_http.py +0 -139
- pytest_dsl/examples/test_quickstart.py +14 -0
- pytest_dsl/keywords/http_keywords.py +290 -102
- pytest_dsl/parsetab.py +69 -0
- pytest_dsl-0.2.0.dist-info/METADATA +504 -0
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.2.0.dist-info}/RECORD +29 -24
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.2.0.dist-info}/WHEEL +1 -1
- pytest_dsl/core/custom_auth_example.py +0 -425
- pytest_dsl/examples/http/csrf_auth_test.auto +0 -64
- pytest_dsl/examples/http/custom_auth_test.auto +0 -76
- pytest_dsl/examples/http_clients.yaml +0 -48
- pytest_dsl/examples/keyword_example.py +0 -70
- pytest_dsl-0.1.0.dist-info/METADATA +0 -537
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.2.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.2.0.dist-info}/top_level.txt +0 -0
pytest_dsl/examples/test_http.py
CHANGED
@@ -27,142 +27,3 @@ class TestHttp:
|
|
27
27
|
"""
|
28
28
|
pass
|
29
29
|
|
30
|
-
# 定义自定义认证提供者
|
31
|
-
class HMACAuthProvider(CustomAuthProvider):
|
32
|
-
def apply_auth(self, request_kwargs):
|
33
|
-
if "headers" not in request_kwargs:
|
34
|
-
request_kwargs["headers"] = {}
|
35
|
-
request_kwargs["headers"]["Authorization"] = "HMAC-SHA256 test_signature"
|
36
|
-
request_kwargs["headers"]["X-Amz-Date"] = "20240501T120000Z"
|
37
|
-
return request_kwargs
|
38
|
-
|
39
|
-
class JWTAuthProvider(CustomAuthProvider):
|
40
|
-
def apply_auth(self, request_kwargs):
|
41
|
-
if "headers" not in request_kwargs:
|
42
|
-
request_kwargs["headers"] = {}
|
43
|
-
request_kwargs["headers"]["Authorization"] = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example_token"
|
44
|
-
return request_kwargs
|
45
|
-
|
46
|
-
class WeChatAuthProvider(CustomAuthProvider):
|
47
|
-
def apply_auth(self, request_kwargs):
|
48
|
-
if "headers" not in request_kwargs:
|
49
|
-
request_kwargs["headers"] = {}
|
50
|
-
request_kwargs["headers"]["X-Wx-Openid"] = "test_openid"
|
51
|
-
request_kwargs["headers"]["X-Wx-Session-Key"] = "test_session_key"
|
52
|
-
return request_kwargs
|
53
|
-
|
54
|
-
class MultiStepAuthProvider(CustomAuthProvider):
|
55
|
-
def apply_auth(self, request_kwargs):
|
56
|
-
if "headers" not in request_kwargs:
|
57
|
-
request_kwargs["headers"] = {}
|
58
|
-
request_kwargs["headers"]["Authorization"] = "Bearer multi_step_token"
|
59
|
-
return request_kwargs
|
60
|
-
|
61
|
-
class CSRFLoginAuthProvider(CustomAuthProvider):
|
62
|
-
"""CSRF登录认证提供者
|
63
|
-
|
64
|
-
该提供者实现了一个需要先登录获取CSRF令牌,然后在后续请求中使用该令牌的认证流程。
|
65
|
-
为了适应httpbin.org的测试,这里模拟了CSRF认证流程。
|
66
|
-
"""
|
67
|
-
def __init__(self):
|
68
|
-
self._csrf_token = None
|
69
|
-
self._session = requests.Session()
|
70
|
-
logger.setLevel(logging.DEBUG) # 设置日志级别为DEBUG
|
71
|
-
|
72
|
-
def apply_auth(self, request_kwargs):
|
73
|
-
# 确保headers存在
|
74
|
-
if "headers" not in request_kwargs:
|
75
|
-
request_kwargs["headers"] = {}
|
76
|
-
|
77
|
-
# 如果还没有CSRF令牌,先登录获取
|
78
|
-
if not self._csrf_token:
|
79
|
-
self._login()
|
80
|
-
|
81
|
-
# 添加CSRF令牌到请求头
|
82
|
-
request_kwargs["headers"]["X-Csrf-Token"] = self._csrf_token
|
83
|
-
|
84
|
-
# 设置Content-Type头
|
85
|
-
# 如果请求中有JSON数据
|
86
|
-
if "json" in request_kwargs:
|
87
|
-
request_kwargs["headers"]["Content-Type"] = "application/json"
|
88
|
-
logger.debug(f"请求体 (JSON): {json.dumps(request_kwargs['json'])}")
|
89
|
-
# 如果请求中有表单数据
|
90
|
-
elif "data" in request_kwargs:
|
91
|
-
# 如果data是字典,默认为表单数据
|
92
|
-
if isinstance(request_kwargs["data"], dict):
|
93
|
-
if "Content-Type" not in request_kwargs["headers"]:
|
94
|
-
request_kwargs["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
|
95
|
-
logger.debug(f"请求体 (form): {request_kwargs['data']}")
|
96
|
-
|
97
|
-
# 调试信息:打印请求信息
|
98
|
-
method = request_kwargs.get('method', 'GET')
|
99
|
-
url = request_kwargs.get('url', '')
|
100
|
-
logger.debug(f"发送请求: {method} {url}")
|
101
|
-
logger.debug(f"请求头: {json.dumps(request_kwargs.get('headers', {}))}")
|
102
|
-
|
103
|
-
return request_kwargs
|
104
|
-
|
105
|
-
def clean_auth_state(self, request_kwargs=None):
|
106
|
-
"""清理CSRF认证状态
|
107
|
-
|
108
|
-
清理CSRF认证相关的状态,包括令牌和会话。
|
109
|
-
|
110
|
-
Args:
|
111
|
-
request_kwargs: 请求参数
|
112
|
-
|
113
|
-
Returns:
|
114
|
-
更新后的请求参数
|
115
|
-
"""
|
116
|
-
# 重置CSRF令牌
|
117
|
-
logger.debug("清理CSRF认证状态")
|
118
|
-
self._csrf_token = None
|
119
|
-
|
120
|
-
# 如果有会话,清理会话
|
121
|
-
if self._session:
|
122
|
-
self._session.cookies.clear()
|
123
|
-
logger.debug("已清理CSRF会话cookie")
|
124
|
-
|
125
|
-
# 处理请求参数
|
126
|
-
if request_kwargs:
|
127
|
-
if "headers" not in request_kwargs:
|
128
|
-
request_kwargs["headers"] = {}
|
129
|
-
|
130
|
-
# 移除CSRF相关头
|
131
|
-
csrf_headers = ['X-Csrf-Token', 'X-CSRF-Token', 'csrf-token', 'CSRF-Token']
|
132
|
-
for header in csrf_headers:
|
133
|
-
if header in request_kwargs["headers"]:
|
134
|
-
request_kwargs["headers"].pop(header)
|
135
|
-
logger.debug(f"已移除请求头: {header}")
|
136
|
-
|
137
|
-
# 标记会话已管理
|
138
|
-
self.manage_session = True
|
139
|
-
|
140
|
-
return request_kwargs if request_kwargs else {}
|
141
|
-
|
142
|
-
def _login(self):
|
143
|
-
"""执行登录流程获取CSRF令牌
|
144
|
-
|
145
|
-
由于httpbin.org没有实际的登录系统,这里模拟一个登录流程
|
146
|
-
"""
|
147
|
-
# 对于测试目的,生成一个模拟的CSRF令牌
|
148
|
-
self._csrf_token = "csrf_token_12345678"
|
149
|
-
logger.debug(f"生成CSRF令牌: {self._csrf_token}")
|
150
|
-
|
151
|
-
# 如果使用真实API,可以使用类似下面的代码
|
152
|
-
# 1. 获取登录页面,提取CSRF令牌
|
153
|
-
# login_page = self._session.get("https://httpbin.org/headers")
|
154
|
-
# login_page.raise_for_status()
|
155
|
-
|
156
|
-
# 2. 执行登录请求
|
157
|
-
# login_response = self._session.post(
|
158
|
-
# "https://httpbin.org/anything",
|
159
|
-
# json={"username": "test_user", "password": "test_password"}
|
160
|
-
# )
|
161
|
-
# login_response.raise_for_status()
|
162
|
-
|
163
|
-
# 注册自定义认证提供者
|
164
|
-
register_auth_provider("hmac_aws_auth", HMACAuthProvider)
|
165
|
-
register_auth_provider("jwt_refresh_auth", JWTAuthProvider)
|
166
|
-
register_auth_provider("wechat_miniapp_auth", WeChatAuthProvider)
|
167
|
-
register_auth_provider("multi_step_auth", MultiStepAuthProvider)
|
168
|
-
register_auth_provider("csrf_login_auth", CSRFLoginAuthProvider)
|
@@ -149,6 +149,75 @@ def _process_request_config(config: Dict[str, Any], test_context: TestContext =
|
|
149
149
|
return config
|
150
150
|
|
151
151
|
|
152
|
+
def _normalize_retry_config(config, assert_retry_count=None, assert_retry_interval=None):
|
153
|
+
"""标准化断言重试配置
|
154
|
+
|
155
|
+
将不同来源的重试配置(命令行参数、retry配置、retry_assertions配置)
|
156
|
+
统一转换为标准化的重试配置对象。
|
157
|
+
|
158
|
+
Args:
|
159
|
+
config: 原始配置字典
|
160
|
+
assert_retry_count: 命令行级别的重试次数参数
|
161
|
+
assert_retry_interval: 命令行级别的重试间隔参数
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
标准化的重试配置字典,格式为:
|
165
|
+
{
|
166
|
+
'enabled': 是否启用重试,
|
167
|
+
'count': 重试次数,
|
168
|
+
'interval': 重试间隔,
|
169
|
+
'all': 是否重试所有断言,
|
170
|
+
'indices': 要重试的断言索引列表,
|
171
|
+
'specific': 特定断言的重试配置
|
172
|
+
}
|
173
|
+
"""
|
174
|
+
# 初始化标准重试配置
|
175
|
+
standard_retry_config = {
|
176
|
+
'enabled': False,
|
177
|
+
'count': 3, # 默认重试3次
|
178
|
+
'interval': 1.0, # 默认间隔1秒
|
179
|
+
'all': False, # 默认不重试所有断言
|
180
|
+
'indices': [], # 默认不指定要重试的断言索引
|
181
|
+
'specific': {} # 默认不指定特定断言的重试配置
|
182
|
+
}
|
183
|
+
|
184
|
+
# 处理命令行参数
|
185
|
+
if assert_retry_count and int(assert_retry_count) > 0:
|
186
|
+
standard_retry_config['enabled'] = True
|
187
|
+
standard_retry_config['count'] = int(assert_retry_count)
|
188
|
+
standard_retry_config['all'] = True # 命令行参数会重试所有断言
|
189
|
+
if assert_retry_interval:
|
190
|
+
standard_retry_config['interval'] = float(assert_retry_interval)
|
191
|
+
|
192
|
+
# 处理专用retry_assertions配置
|
193
|
+
if 'retry_assertions' in config and config['retry_assertions']:
|
194
|
+
retry_assertions = config['retry_assertions']
|
195
|
+
standard_retry_config['enabled'] = True
|
196
|
+
|
197
|
+
if 'count' in retry_assertions:
|
198
|
+
standard_retry_config['count'] = retry_assertions['count']
|
199
|
+
if 'interval' in retry_assertions:
|
200
|
+
standard_retry_config['interval'] = retry_assertions['interval']
|
201
|
+
if 'all' in retry_assertions:
|
202
|
+
standard_retry_config['all'] = retry_assertions['all']
|
203
|
+
if 'indices' in retry_assertions:
|
204
|
+
standard_retry_config['indices'] = retry_assertions['indices']
|
205
|
+
if 'specific' in retry_assertions:
|
206
|
+
standard_retry_config['specific'] = retry_assertions['specific']
|
207
|
+
|
208
|
+
# 处理传统retry配置(如果专用配置不存在)
|
209
|
+
elif 'retry' in config and config['retry']:
|
210
|
+
retry_config = config['retry']
|
211
|
+
if 'count' in retry_config and retry_config['count'] > 0:
|
212
|
+
standard_retry_config['enabled'] = True
|
213
|
+
standard_retry_config['count'] = retry_config['count']
|
214
|
+
standard_retry_config['all'] = True # 传统配置会重试所有断言
|
215
|
+
if 'interval' in retry_config:
|
216
|
+
standard_retry_config['interval'] = retry_config['interval']
|
217
|
+
|
218
|
+
return standard_retry_config
|
219
|
+
|
220
|
+
|
152
221
|
@keyword_manager.register('HTTP请求', [
|
153
222
|
{'name': '客户端', 'mapping': 'client', 'description': '客户端名称,对应YAML变量文件中的客户端配置'},
|
154
223
|
{'name': '配置', 'mapping': 'config', 'description': '包含请求、捕获和断言的YAML配置'},
|
@@ -228,24 +297,18 @@ def http_request(context, **kwargs):
|
|
228
297
|
except yaml.YAMLError as e:
|
229
298
|
raise ValueError(f"无效的YAML配置: {str(e)}")
|
230
299
|
|
231
|
-
#
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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)
|
300
|
+
# 统一处理重试配置
|
301
|
+
retry_config = _normalize_retry_config(config, assert_retry_count, assert_retry_interval)
|
302
|
+
|
303
|
+
# 为了兼容性,将标准化后的重试配置写回到配置中
|
304
|
+
if retry_config['enabled']:
|
305
|
+
config['retry_assertions'] = {
|
306
|
+
'count': retry_config['count'],
|
307
|
+
'interval': retry_config['interval'],
|
308
|
+
'all': retry_config['all'],
|
309
|
+
'indices': retry_config['indices'],
|
310
|
+
'specific': retry_config['specific']
|
311
|
+
}
|
249
312
|
|
250
313
|
config = _process_request_config(config, test_context=context)
|
251
314
|
|
@@ -266,22 +329,14 @@ def http_request(context, **kwargs):
|
|
266
329
|
if save_response:
|
267
330
|
context.set(save_response, response)
|
268
331
|
|
269
|
-
#
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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()
|
332
|
+
# 统一处理断言逻辑
|
333
|
+
with allure.step("执行断言验证"):
|
334
|
+
if retry_config['enabled']:
|
335
|
+
# 使用统一的重试处理函数
|
336
|
+
_process_assertions_with_unified_retry(http_req, retry_config)
|
337
|
+
else:
|
338
|
+
# 不需要重试,直接断言
|
339
|
+
http_req.process_asserts()
|
285
340
|
|
286
341
|
# 返回捕获的变量
|
287
342
|
return captured_values
|
@@ -305,82 +360,129 @@ def _deep_merge(dict1, dict2):
|
|
305
360
|
return dict1
|
306
361
|
|
307
362
|
|
308
|
-
def
|
309
|
-
"""
|
363
|
+
def _process_assertions_with_unified_retry(http_req, retry_config):
|
364
|
+
"""使用统一的重试配置处理断言
|
310
365
|
|
311
366
|
Args:
|
312
367
|
http_req: HTTP请求对象
|
313
|
-
|
314
|
-
retry_interval: 重试间隔(秒)
|
368
|
+
retry_config: 标准化的重试配置
|
315
369
|
"""
|
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
|
-
# 尝试执行所有断言
|
370
|
+
# 初始尝试执行所有断言
|
357
371
|
try:
|
358
372
|
results, failed_retryable_assertions = http_req.process_asserts()
|
359
|
-
|
360
|
-
|
361
|
-
|
373
|
+
# 如果没有失败的断言,直接返回
|
374
|
+
return results
|
375
|
+
except AssertionError as e:
|
376
|
+
# 记录初始断言失败的详细错误信息
|
377
|
+
allure.attach(
|
378
|
+
str(e),
|
379
|
+
name="断言验证失败详情",
|
380
|
+
attachment_type=allure.attachment_type.TEXT
|
381
|
+
)
|
382
|
+
|
383
|
+
# 添加一个特殊的标记到配置中,表示我们只想收集失败的断言而不抛出异常
|
384
|
+
original_config = http_req.config.copy() if isinstance(http_req.config, dict) else {}
|
385
|
+
|
386
|
+
# 创建一个临时副本
|
387
|
+
temp_config = original_config.copy()
|
388
|
+
|
389
|
+
# 添加特殊标记,用于指示http_request.py中的process_asserts在处理fail时不抛出异常
|
390
|
+
# 注意:这需要对应修改HTTPRequest.process_asserts方法
|
391
|
+
temp_config['_collect_failed_assertions_only'] = True
|
392
|
+
|
393
|
+
try:
|
394
|
+
# 临时替换配置
|
395
|
+
http_req.config = temp_config
|
396
|
+
|
397
|
+
# 重新运行断言,这次只收集失败的断言而不抛出异常
|
398
|
+
_, failed_retryable_assertions = http_req.process_asserts()
|
399
|
+
except Exception as collect_err:
|
400
|
+
# 出现意外错误时记录
|
401
|
+
allure.attach(
|
402
|
+
f"收集失败断言时出错: {type(collect_err).__name__}: {str(collect_err)}",
|
403
|
+
name="断言收集错误",
|
404
|
+
attachment_type=allure.attachment_type.TEXT
|
405
|
+
)
|
406
|
+
failed_retryable_assertions = []
|
407
|
+
finally:
|
408
|
+
# 恢复原始配置
|
409
|
+
http_req.config = original_config
|
410
|
+
|
411
|
+
# 有断言失败,判断是否有需要重试的断言
|
362
412
|
if not failed_retryable_assertions:
|
363
|
-
#
|
413
|
+
# 没有可重试的断言,重新抛出原始异常
|
364
414
|
raise
|
365
415
|
|
366
|
-
#
|
367
|
-
|
416
|
+
# 过滤需要重试的断言
|
417
|
+
retryable_assertions = []
|
368
418
|
|
369
|
-
# 找出所有断言中最大的重试次数
|
370
419
|
for failed_assertion in failed_retryable_assertions:
|
371
|
-
|
420
|
+
assertion_idx = failed_assertion['index']
|
372
421
|
|
373
|
-
|
422
|
+
# 判断该断言是否应该重试
|
423
|
+
should_retry = False
|
424
|
+
specific_retry_count = retry_config['count']
|
425
|
+
specific_retry_interval = retry_config['interval']
|
426
|
+
|
427
|
+
# 检查特定断言配置
|
428
|
+
if str(assertion_idx) in retry_config['specific']:
|
429
|
+
should_retry = True
|
430
|
+
spec_config = retry_config['specific'][str(assertion_idx)]
|
431
|
+
if isinstance(spec_config, dict):
|
432
|
+
if 'count' in spec_config:
|
433
|
+
specific_retry_count = spec_config['count']
|
434
|
+
if 'interval' in spec_config:
|
435
|
+
specific_retry_interval = spec_config['interval']
|
436
|
+
# 检查索引列表
|
437
|
+
elif assertion_idx in retry_config['indices']:
|
438
|
+
should_retry = True
|
439
|
+
# 检查是否重试所有
|
440
|
+
elif retry_config['all']:
|
441
|
+
should_retry = True
|
442
|
+
|
443
|
+
# 如果应该重试,添加到可重试断言列表
|
444
|
+
if should_retry:
|
445
|
+
# 添加重试配置到断言对象
|
446
|
+
failed_assertion['retry_count'] = specific_retry_count
|
447
|
+
failed_assertion['retry_interval'] = specific_retry_interval
|
448
|
+
retryable_assertions.append(failed_assertion)
|
449
|
+
|
450
|
+
# 如果没有可重试的断言,重新抛出异常
|
451
|
+
if not retryable_assertions:
|
452
|
+
raise
|
453
|
+
|
454
|
+
# 记录哪些断言会被重试
|
455
|
+
retry_info = "\n".join([
|
456
|
+
f"{i+1}. {a['type']} " +
|
457
|
+
(f"[{a['path']}]" if a['path'] else "") +
|
458
|
+
f": 重试 {a['retry_count']} 次,间隔 {a['retry_interval']} 秒"
|
459
|
+
for i, a in enumerate(retryable_assertions)
|
460
|
+
])
|
461
|
+
|
462
|
+
allure.attach(
|
463
|
+
f"找到 {len(retryable_assertions)} 个可重试的断言:\n\n{retry_info}",
|
464
|
+
name="重试断言列表",
|
465
|
+
attachment_type=allure.attachment_type.TEXT
|
466
|
+
)
|
467
|
+
|
468
|
+
# 开始重试循环
|
469
|
+
max_retry_count = retry_config['count']
|
470
|
+
|
471
|
+
# 找出所有断言中最大的重试次数
|
472
|
+
for retryable_assertion in retryable_assertions:
|
473
|
+
max_retry_count = max(max_retry_count, retryable_assertion.get('retry_count', 3))
|
474
|
+
|
475
|
+
# 进行断言重试
|
374
476
|
for attempt in range(1, max_retry_count + 1): # 从1开始,因为第0次已经尝试过了
|
375
477
|
# 等待重试间隔
|
376
|
-
with allure.step(f"断言重试 (尝试 {attempt
|
478
|
+
with allure.step(f"断言重试 (尝试 {attempt}/{max_retry_count})"):
|
377
479
|
# 确定本次重试的间隔时间(使用每个断言中最长的间隔时间)
|
378
|
-
retry_interval =
|
379
|
-
for
|
380
|
-
retry_interval = max(retry_interval,
|
480
|
+
retry_interval = retry_config['interval']
|
481
|
+
for assertion in retryable_assertions:
|
482
|
+
retry_interval = max(retry_interval, assertion.get('retry_interval', 1.0))
|
381
483
|
|
382
484
|
allure.attach(
|
383
|
-
f"重试 {len(
|
485
|
+
f"重试 {len(retryable_assertions)} 个断言\n"
|
384
486
|
f"等待间隔: {retry_interval}秒",
|
385
487
|
name="断言重试信息",
|
386
488
|
attachment_type=allure.attachment_type.TEXT
|
@@ -393,12 +495,12 @@ def _process_config_based_assertions_with_retry(http_req):
|
|
393
495
|
|
394
496
|
# 过滤出仍在重试范围内的断言
|
395
497
|
still_retryable_assertions = []
|
396
|
-
for
|
397
|
-
assertion_retry_count =
|
498
|
+
for assertion in retryable_assertions:
|
499
|
+
assertion_retry_count = assertion.get('retry_count', 3)
|
398
500
|
|
399
501
|
# 如果断言的重试次数大于当前尝试次数,继续重试该断言
|
400
502
|
if attempt < assertion_retry_count:
|
401
|
-
still_retryable_assertions.append(
|
503
|
+
still_retryable_assertions.append(assertion)
|
402
504
|
|
403
505
|
# 如果没有可以继续重试的断言,跳出循环
|
404
506
|
if not still_retryable_assertions:
|
@@ -413,18 +515,104 @@ def _process_config_based_assertions_with_retry(http_req):
|
|
413
515
|
# 只处理需要重试的断言
|
414
516
|
results, new_failed_assertions = http_req.process_asserts(specific_asserts=retry_assertions)
|
415
517
|
|
416
|
-
#
|
518
|
+
# 如果所有断言都通过了,检查全部断言
|
417
519
|
if not new_failed_assertions:
|
418
520
|
# 执行一次完整的断言检查,确保所有断言都通过
|
419
|
-
|
521
|
+
try:
|
522
|
+
results, _ = http_req.process_asserts()
|
523
|
+
allure.attach(
|
524
|
+
"所有断言重试后验证通过",
|
525
|
+
name="重试成功",
|
526
|
+
attachment_type=allure.attachment_type.TEXT
|
527
|
+
)
|
528
|
+
return results
|
529
|
+
except AssertionError as final_err:
|
530
|
+
# 记录最终错误,然后继续重试
|
531
|
+
allure.attach(
|
532
|
+
f"重试后的完整断言验证仍有失败: {str(final_err)}",
|
533
|
+
name="完整断言仍失败",
|
534
|
+
attachment_type=allure.attachment_type.TEXT
|
535
|
+
)
|
536
|
+
continue
|
420
537
|
|
421
538
|
# 更新失败的可重试断言列表
|
422
|
-
|
539
|
+
retryable_assertions = new_failed_assertions
|
423
540
|
|
424
|
-
except AssertionError:
|
425
|
-
#
|
541
|
+
except AssertionError as retry_err:
|
542
|
+
# 重试时断言失败,记录后继续重试
|
543
|
+
allure.attach(
|
544
|
+
f"第 {attempt} 次重试断言失败: {str(retry_err)}",
|
545
|
+
name=f"重试断言失败 #{attempt}",
|
546
|
+
attachment_type=allure.attachment_type.TEXT
|
547
|
+
)
|
426
548
|
continue
|
427
549
|
|
428
550
|
# 重试次数用完,执行一次完整的断言以获取最终结果和错误
|
429
551
|
# 这会抛出异常,如果仍然有断言失败
|
430
|
-
|
552
|
+
allure.attach(
|
553
|
+
"所有重试次数已用完,执行最终断言验证",
|
554
|
+
name="重试完成",
|
555
|
+
attachment_type=allure.attachment_type.TEXT
|
556
|
+
)
|
557
|
+
|
558
|
+
try:
|
559
|
+
results, _ = http_req.process_asserts()
|
560
|
+
return results
|
561
|
+
except AssertionError as final_err:
|
562
|
+
# 重新格式化错误消息,添加重试信息
|
563
|
+
enhanced_error = (
|
564
|
+
f"断言验证失败 (已重试 {max_retry_count} 次):\n\n{str(final_err)}"
|
565
|
+
)
|
566
|
+
allure.attach(
|
567
|
+
enhanced_error,
|
568
|
+
name="重试后仍失败的断言",
|
569
|
+
attachment_type=allure.attachment_type.TEXT
|
570
|
+
)
|
571
|
+
raise AssertionError(enhanced_error) from final_err
|
572
|
+
|
573
|
+
|
574
|
+
# 移除旧的重试函数,使用统一的重试机制
|
575
|
+
# 以下函数保留用于向后兼容,但内部逻辑已更改为调用统一重试函数
|
576
|
+
def _process_assertions_with_retry(http_req, retry_count, retry_interval):
|
577
|
+
"""处理断言并支持重试(向后兼容函数)
|
578
|
+
|
579
|
+
Args:
|
580
|
+
http_req: HTTP请求对象
|
581
|
+
retry_count: 重试次数
|
582
|
+
retry_interval: 重试间隔(秒)
|
583
|
+
"""
|
584
|
+
# 创建统一的重试配置
|
585
|
+
retry_config = {
|
586
|
+
'enabled': True,
|
587
|
+
'count': retry_count,
|
588
|
+
'interval': retry_interval,
|
589
|
+
'all': True,
|
590
|
+
'indices': [],
|
591
|
+
'specific': {}
|
592
|
+
}
|
593
|
+
|
594
|
+
# 使用统一的重试处理函数
|
595
|
+
return _process_assertions_with_unified_retry(http_req, retry_config)
|
596
|
+
|
597
|
+
|
598
|
+
def _process_config_based_assertions_with_retry(http_req):
|
599
|
+
"""基于配置处理断言重试(向后兼容函数)
|
600
|
+
|
601
|
+
Args:
|
602
|
+
http_req: HTTP请求对象
|
603
|
+
"""
|
604
|
+
# 从配置中获取重试信息
|
605
|
+
retry_assertions_config = http_req.config.get('retry_assertions', {})
|
606
|
+
|
607
|
+
# 创建统一的重试配置
|
608
|
+
retry_config = {
|
609
|
+
'enabled': True,
|
610
|
+
'count': retry_assertions_config.get('count', 3),
|
611
|
+
'interval': retry_assertions_config.get('interval', 1.0),
|
612
|
+
'all': retry_assertions_config.get('all', False),
|
613
|
+
'indices': retry_assertions_config.get('indices', []),
|
614
|
+
'specific': retry_assertions_config.get('specific', {})
|
615
|
+
}
|
616
|
+
|
617
|
+
# 使用统一的重试处理函数
|
618
|
+
return _process_assertions_with_unified_retry(http_req, retry_config)
|