pytest-dsl 0.11.1__py3-none-any.whl → 0.12.1__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/cli.py +89 -83
- pytest_dsl/core/http_client.py +91 -78
- pytest_dsl/core/http_request.py +135 -61
- pytest_dsl/examples/http/http_example.auto +1 -1
- pytest_dsl/examples/http/http_retry_assertions.auto +4 -4
- pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +2 -2
- pytest_dsl/examples/http/new_retry_test.auto +1 -1
- pytest_dsl/examples/http/retry_assertions_only.auto +3 -3
- pytest_dsl/examples/http/retry_config_only.auto +3 -3
- pytest_dsl/examples/http/retry_debug.auto +19 -8
- pytest_dsl/examples/http/retry_with_fix.auto +1 -1
- pytest_dsl/keywords/http_keywords.py +42 -14
- pytest_dsl/keywords/system_keywords.py +420 -1
- pytest_dsl/plugin.py +3 -2
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/METADATA +3 -3
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/RECORD +20 -20
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/top_level.txt +0 -0
pytest_dsl/core/http_request.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
import json
|
2
|
+
import logging
|
2
3
|
import re
|
3
|
-
import yaml
|
4
4
|
import jsonpath_ng.ext as jsonpath
|
5
|
-
from typing import Dict, List, Any,
|
5
|
+
from typing import Dict, List, Any, Tuple
|
6
6
|
import lxml.etree as etree
|
7
7
|
from requests import Response
|
8
8
|
import allure
|
@@ -10,6 +10,7 @@ import requests
|
|
10
10
|
|
11
11
|
from pytest_dsl.core.http_client import http_client_manager
|
12
12
|
|
13
|
+
logger = logging.getLogger(__name__)
|
13
14
|
|
14
15
|
# 定义支持的比较操作符
|
15
16
|
COMPARISON_OPERATORS = {
|
@@ -25,7 +26,8 @@ ASSERTION_TYPES = {
|
|
25
26
|
|
26
27
|
# 操作符和断言类型重叠的集合(可以作为二者使用的)
|
27
28
|
DUAL_PURPOSE = {
|
28
|
-
"contains", "not_contains", "matches", "in", "not_in", "startswith",
|
29
|
+
"contains", "not_contains", "matches", "in", "not_in", "startswith",
|
30
|
+
"endswith"
|
29
31
|
}
|
30
32
|
|
31
33
|
|
@@ -35,7 +37,8 @@ class HTTPRequest:
|
|
35
37
|
负责处理HTTP请求、响应捕获和断言
|
36
38
|
"""
|
37
39
|
|
38
|
-
def __init__(self, config: Dict[str, Any], client_name: str = "default",
|
40
|
+
def __init__(self, config: Dict[str, Any], client_name: str = "default",
|
41
|
+
session_name: str = None):
|
39
42
|
"""初始化HTTP请求
|
40
43
|
|
41
44
|
Args:
|
@@ -60,7 +63,8 @@ class HTTPRequest:
|
|
60
63
|
"""
|
61
64
|
# 获取HTTP客户端
|
62
65
|
if self.session_name:
|
63
|
-
client = http_client_manager.get_session(
|
66
|
+
client = http_client_manager.get_session(
|
67
|
+
self.session_name, self.client_name)
|
64
68
|
else:
|
65
69
|
client = http_client_manager.get_client(self.client_name)
|
66
70
|
|
@@ -91,7 +95,8 @@ class HTTPRequest:
|
|
91
95
|
'data': request_config.get('data'),
|
92
96
|
'files': request_config.get('files'),
|
93
97
|
'cookies': request_config.get('cookies'),
|
94
|
-
'auth': tuple(request_config.get('auth'))
|
98
|
+
'auth': (tuple(request_config.get('auth'))
|
99
|
+
if request_config.get('auth') else None),
|
95
100
|
'timeout': request_config.get('timeout'),
|
96
101
|
'allow_redirects': request_config.get('allow_redirects'),
|
97
102
|
'verify': request_config.get('verify'),
|
@@ -101,7 +106,8 @@ class HTTPRequest:
|
|
101
106
|
}
|
102
107
|
|
103
108
|
# 过滤掉None值
|
104
|
-
request_kwargs = {k: v for k,
|
109
|
+
request_kwargs = {k: v for k,
|
110
|
+
v in request_kwargs.items() if v is not None}
|
105
111
|
|
106
112
|
# 使用Allure记录请求信息
|
107
113
|
self._log_request_to_allure(method, url, request_kwargs)
|
@@ -113,8 +119,21 @@ class HTTPRequest:
|
|
113
119
|
# 使用Allure记录响应信息
|
114
120
|
self._log_response_to_allure(self.response)
|
115
121
|
|
116
|
-
# 处理捕获
|
117
|
-
|
122
|
+
# 处理捕获 - 确保即使后续断言失败,捕获的变量也能被正确设置
|
123
|
+
try:
|
124
|
+
self.process_captures()
|
125
|
+
except Exception as capture_error:
|
126
|
+
# 捕获处理失败不应该影响请求的执行,只记录警告
|
127
|
+
# 但确保captured_values至少是空字典
|
128
|
+
if (not hasattr(self, 'captured_values') or
|
129
|
+
self.captured_values is None):
|
130
|
+
self.captured_values = {}
|
131
|
+
logger.warning(f"变量捕获处理失败: {str(capture_error)}")
|
132
|
+
allure.attach(
|
133
|
+
f"变量捕获处理失败: {str(capture_error)}",
|
134
|
+
name="变量捕获警告",
|
135
|
+
attachment_type=allure.attachment_type.TEXT
|
136
|
+
)
|
118
137
|
|
119
138
|
return self.response
|
120
139
|
except requests.exceptions.RequestException as e:
|
@@ -149,13 +168,16 @@ class HTTPRequest:
|
|
149
168
|
Raises:
|
150
169
|
ValueError: 如果响应对象不存在
|
151
170
|
"""
|
152
|
-
if
|
171
|
+
if self.response is None:
|
153
172
|
error_message = f"需要先执行请求才能{operation}"
|
154
173
|
# 记录更详细的错误信息到Allure
|
174
|
+
config_json = json.dumps(
|
175
|
+
self.config, indent=2, ensure_ascii=False, default=str)
|
155
176
|
debug_info = (
|
156
177
|
f"错误详情: self.response 为 None\n"
|
157
|
-
f"配置信息: {
|
158
|
-
f"当前状态: 客户端名称={self.client_name},
|
178
|
+
f"配置信息: {config_json}\n"
|
179
|
+
f"当前状态: 客户端名称={self.client_name}, "
|
180
|
+
f"会话名称={self.session_name}"
|
159
181
|
)
|
160
182
|
allure.attach(
|
161
183
|
debug_info,
|
@@ -170,6 +192,7 @@ class HTTPRequest:
|
|
170
192
|
Returns:
|
171
193
|
捕获的值字典
|
172
194
|
"""
|
195
|
+
|
173
196
|
self._ensure_response_exists("捕获响应")
|
174
197
|
|
175
198
|
captures_config = self.config.get('captures', {})
|
@@ -181,18 +204,22 @@ class HTTPRequest:
|
|
181
204
|
# 提取捕获信息
|
182
205
|
try:
|
183
206
|
extractor_type = capture_spec[0]
|
184
|
-
extraction_path = capture_spec[1] if len(
|
207
|
+
extraction_path = capture_spec[1] if len(
|
208
|
+
capture_spec) > 1 else None
|
185
209
|
|
186
210
|
# 检查是否有length参数
|
187
211
|
is_length_capture = False
|
188
212
|
if len(capture_spec) > 2 and capture_spec[2] == "length":
|
189
213
|
is_length_capture = True
|
190
|
-
default_value = capture_spec[3] if len(
|
214
|
+
default_value = capture_spec[3] if len(
|
215
|
+
capture_spec) > 3 else None
|
191
216
|
else:
|
192
|
-
default_value = capture_spec[2] if len(
|
217
|
+
default_value = capture_spec[2] if len(
|
218
|
+
capture_spec) > 2 else None
|
193
219
|
|
194
220
|
# 提取值
|
195
|
-
captured_value = self._extract_value(
|
221
|
+
captured_value = self._extract_value(
|
222
|
+
extractor_type, extraction_path, default_value)
|
196
223
|
|
197
224
|
# 特殊处理length
|
198
225
|
if is_length_capture:
|
@@ -219,7 +246,8 @@ class HTTPRequest:
|
|
219
246
|
attachment_type=allure.attachment_type.TEXT
|
220
247
|
)
|
221
248
|
# 抛出更具体的异常,而不是用0替代
|
222
|
-
raise ValueError(
|
249
|
+
raise ValueError(
|
250
|
+
f"断言类型'length'无法应用于值 '{original_value}': {str(e)}")
|
223
251
|
else:
|
224
252
|
# 记录捕获到Allure
|
225
253
|
allure.attach(
|
@@ -246,15 +274,15 @@ class HTTPRequest:
|
|
246
274
|
return self.captured_values
|
247
275
|
|
248
276
|
def _format_error_details(self,
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
277
|
+
extractor_type: str,
|
278
|
+
extraction_path: str,
|
279
|
+
assertion_type: str,
|
280
|
+
compare_operator: str,
|
281
|
+
actual_value: Any,
|
282
|
+
expected_value: Any,
|
283
|
+
original_actual_value: Any = None,
|
284
|
+
error_message: str = None,
|
285
|
+
additional_context: str = None) -> str:
|
258
286
|
"""格式化断言错误的详细信息
|
259
287
|
|
260
288
|
Args:
|
@@ -287,7 +315,7 @@ class HTTPRequest:
|
|
287
315
|
|
288
316
|
# 添加断言类型和值信息
|
289
317
|
if assertion_type == "length":
|
290
|
-
error_details.append(
|
318
|
+
error_details.append("断言类型: 长度比较")
|
291
319
|
if original_actual_value is not None:
|
292
320
|
error_details.append(f"原始值: {original_actual_value}")
|
293
321
|
error_details.append(f"实际长度: {actual_value}")
|
@@ -324,6 +352,7 @@ class HTTPRequest:
|
|
324
352
|
Returns:
|
325
353
|
Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: 断言结果列表和失败需重试的断言列表
|
326
354
|
"""
|
355
|
+
|
327
356
|
self._ensure_response_exists("进行断言")
|
328
357
|
|
329
358
|
asserts_config = self.config.get('asserts', [])
|
@@ -340,15 +369,19 @@ class HTTPRequest:
|
|
340
369
|
global_retry_enabled = bool(retry_config)
|
341
370
|
|
342
371
|
# 3. 提取重试默认设置
|
343
|
-
global_retry_count = retry_assertions_config.get(
|
344
|
-
|
372
|
+
global_retry_count = retry_assertions_config.get(
|
373
|
+
'count', retry_config.get('count', 3))
|
374
|
+
global_retry_interval = retry_assertions_config.get(
|
375
|
+
'interval', retry_config.get('interval', 1))
|
345
376
|
|
346
377
|
# 4. 提取应该重试的断言索引列表
|
347
|
-
retry_all_assertions = retry_assertions_config.get(
|
378
|
+
retry_all_assertions = retry_assertions_config.get(
|
379
|
+
'all', global_retry_enabled)
|
348
380
|
retry_assertion_indices = retry_assertions_config.get('indices', [])
|
349
381
|
|
350
382
|
# 5. 提取特定断言的重试配置
|
351
|
-
specific_assertion_configs = retry_assertions_config.get(
|
383
|
+
specific_assertion_configs = retry_assertions_config.get(
|
384
|
+
'specific', {})
|
352
385
|
|
353
386
|
# 如果传入了specific_asserts,只处理指定的断言
|
354
387
|
process_asserts = specific_asserts if specific_asserts is not None else asserts_config
|
@@ -361,7 +394,8 @@ class HTTPRequest:
|
|
361
394
|
extractor_type = assertion[0]
|
362
395
|
|
363
396
|
# 获取原始索引(用于重试配置查找)
|
364
|
-
original_idx = index_mapping.get(
|
397
|
+
original_idx = index_mapping.get(
|
398
|
+
assertion_idx, assertion_idx) if index_mapping else assertion_idx
|
365
399
|
|
366
400
|
# 判断该断言是否应该重试(只使用retry_assertions配置)
|
367
401
|
is_retryable = False
|
@@ -371,7 +405,8 @@ class HTTPRequest:
|
|
371
405
|
# retry_assertions特定配置 - 使用原始索引和支持整数键
|
372
406
|
original_idx_str = str(original_idx)
|
373
407
|
if original_idx_str in specific_assertion_configs or original_idx in specific_assertion_configs:
|
374
|
-
spec_config = specific_assertion_configs.get(
|
408
|
+
spec_config = specific_assertion_configs.get(
|
409
|
+
original_idx_str) or specific_assertion_configs.get(original_idx)
|
375
410
|
is_retryable = True
|
376
411
|
if isinstance(spec_config, dict):
|
377
412
|
if 'count' in spec_config:
|
@@ -395,7 +430,8 @@ class HTTPRequest:
|
|
395
430
|
# 检查第二个元素是否是操作符或二者兼用的断言类型
|
396
431
|
if assertion[1] in COMPARISON_OPERATORS or assertion[1] in DUAL_PURPOSE:
|
397
432
|
# 这是操作符格式的断言 ["status", "eq", 200]
|
398
|
-
extraction_path = None if extractor_type in [
|
433
|
+
extraction_path = None if extractor_type in [
|
434
|
+
"status", "body", "response_time"] else assertion[1]
|
399
435
|
|
400
436
|
# 特殊处理二者兼用的断言类型
|
401
437
|
if assertion[1] in DUAL_PURPOSE:
|
@@ -443,20 +479,23 @@ class HTTPRequest:
|
|
443
479
|
else:
|
444
480
|
# 检查断言类型是否有效
|
445
481
|
if assertion_type not in ASSERTION_TYPES:
|
446
|
-
raise ValueError(
|
482
|
+
raise ValueError(
|
483
|
+
f"不支持的断言类型: {assertion_type} 在 {assertion}")
|
447
484
|
|
448
485
|
# 检查此断言类型是否需要预期值
|
449
486
|
if assertion_type not in ["exists", "not_exists"]:
|
450
487
|
# 特殊处理类型断言,它确实需要一个值但在这种格式中没有提供
|
451
488
|
if assertion_type == "type":
|
452
|
-
raise ValueError(
|
489
|
+
raise ValueError(
|
490
|
+
f"断言类型 'type' 需要预期值(string/number/boolean/array/object/null),但未提供: {assertion}")
|
453
491
|
# 断言类型作为操作符
|
454
492
|
expected_value = None
|
455
493
|
compare_operator = assertion_type # 使用断言类型作为操作符
|
456
494
|
else:
|
457
495
|
expected_value = None
|
458
496
|
compare_operator = assertion_type # 存在性断言的操作符就是断言类型本身
|
459
|
-
|
497
|
+
# 带操作符的断言 ["jsonpath", "$.id", "eq", 1] 或特殊断言 ["jsonpath", "$.type", "type", "string"] 或长度断言 ["body", "length", "gt", 50]
|
498
|
+
elif len(assertion) == 4:
|
460
499
|
extraction_path = assertion[1]
|
461
500
|
|
462
501
|
# 特殊处理长度断言:["body", "length", "gt", 50]
|
@@ -475,7 +514,8 @@ class HTTPRequest:
|
|
475
514
|
else:
|
476
515
|
# 检查断言类型是否有效
|
477
516
|
if assertion[2] not in ASSERTION_TYPES:
|
478
|
-
raise ValueError(
|
517
|
+
raise ValueError(
|
518
|
+
f"不支持的断言类型: {assertion[2]} 在 {assertion}")
|
479
519
|
|
480
520
|
# 其他类型的断言,比如特殊断言
|
481
521
|
assertion_type = assertion[2]
|
@@ -495,13 +535,15 @@ class HTTPRequest:
|
|
495
535
|
|
496
536
|
# 检查断言类型是否有效
|
497
537
|
if assertion_type not in ASSERTION_TYPES:
|
498
|
-
raise ValueError(
|
538
|
+
raise ValueError(
|
539
|
+
f"不支持的断言类型: {assertion_type} 在 {assertion}")
|
499
540
|
|
500
541
|
# 特殊处理长度断言
|
501
542
|
if assertion_type == "length":
|
502
543
|
# 验证第四个元素是有效的比较操作符
|
503
544
|
if assertion[3] not in COMPARISON_OPERATORS:
|
504
|
-
raise ValueError(
|
545
|
+
raise ValueError(
|
546
|
+
f"长度断言的比较操作符必须是 {', '.join(COMPARISON_OPERATORS)} 之一: {assertion}")
|
505
547
|
|
506
548
|
# 验证第五个元素是有效的长度值
|
507
549
|
try:
|
@@ -557,7 +599,8 @@ class HTTPRequest:
|
|
557
599
|
attachment_type=allure.attachment_type.TEXT
|
558
600
|
)
|
559
601
|
# 抛出更具体的异常,而不是用0替代
|
560
|
-
raise ValueError(
|
602
|
+
raise ValueError(
|
603
|
+
f"断言类型'length'无法应用于值 '{original_actual_value}': {str(e)}")
|
561
604
|
|
562
605
|
# 执行断言
|
563
606
|
assertion_result = {
|
@@ -576,7 +619,8 @@ class HTTPRequest:
|
|
576
619
|
|
577
620
|
try:
|
578
621
|
# 验证断言
|
579
|
-
result = self._perform_assertion(
|
622
|
+
result = self._perform_assertion(
|
623
|
+
assertion_type, compare_operator, actual_value, expected_value)
|
580
624
|
assertion_result['result'] = result
|
581
625
|
|
582
626
|
# 根据返回的布尔值确定断言是否通过
|
@@ -596,7 +640,8 @@ class HTTPRequest:
|
|
596
640
|
|
597
641
|
# 使用Allure记录断言失败
|
598
642
|
allure.attach(
|
599
|
-
self._format_assertion_details(
|
643
|
+
self._format_assertion_details(
|
644
|
+
assertion_result) + "\n\n错误: 断言结果为False",
|
600
645
|
name=f"断言失败: {extractor_type}",
|
601
646
|
attachment_type=allure.attachment_type.TEXT
|
602
647
|
)
|
@@ -643,7 +688,8 @@ class HTTPRequest:
|
|
643
688
|
|
644
689
|
# 使用Allure记录断言失败
|
645
690
|
allure.attach(
|
646
|
-
self._format_assertion_details(
|
691
|
+
self._format_assertion_details(
|
692
|
+
assertion_result) + f"\n\n错误: {str(e)}",
|
647
693
|
name=f"断言失败: {extractor_type}",
|
648
694
|
attachment_type=allure.attachment_type.TEXT
|
649
695
|
)
|
@@ -676,7 +722,8 @@ class HTTPRequest:
|
|
676
722
|
|
677
723
|
# 使用Allure记录断言错误
|
678
724
|
allure.attach(
|
679
|
-
self._format_assertion_details(
|
725
|
+
self._format_assertion_details(
|
726
|
+
assertion_result) + f"\n\n错误: {assertion_result['error']}",
|
680
727
|
name=f"断言执行错误: {extractor_type}",
|
681
728
|
attachment_type=allure.attachment_type.TEXT
|
682
729
|
)
|
@@ -689,7 +736,8 @@ class HTTPRequest:
|
|
689
736
|
# 执行完所有断言后,如果有失败的断言,抛出异常
|
690
737
|
if failed_assertions:
|
691
738
|
# 检查是否只收集失败而不抛出异常(由重试机制设置)
|
692
|
-
collect_only = self.config.get(
|
739
|
+
collect_only = self.config.get(
|
740
|
+
'_collect_failed_assertions_only', False)
|
693
741
|
|
694
742
|
if not collect_only:
|
695
743
|
if len(failed_assertions) == 1:
|
@@ -702,7 +750,8 @@ class HTTPRequest:
|
|
702
750
|
# 从错误消息中提取关键部分
|
703
751
|
if "[" in error_msg and "]" in error_msg:
|
704
752
|
# 尝试提取提取器类型
|
705
|
-
extractor_type = error_msg.split("[", 1)[1].split("]")[
|
753
|
+
extractor_type = error_msg.split("[", 1)[1].split("]")[
|
754
|
+
0] if "[" in error_msg else "未知"
|
706
755
|
else:
|
707
756
|
extractor_type = "未知"
|
708
757
|
|
@@ -759,7 +808,7 @@ class HTTPRequest:
|
|
759
808
|
Returns:
|
760
809
|
提取的值
|
761
810
|
"""
|
762
|
-
if
|
811
|
+
if self.response is None:
|
763
812
|
return default_value
|
764
813
|
|
765
814
|
try:
|
@@ -868,14 +917,33 @@ class HTTPRequest:
|
|
868
917
|
else:
|
869
918
|
text = self.response.text
|
870
919
|
|
871
|
-
|
920
|
+
# 检查正则表达式是否包含捕获组
|
921
|
+
compiled_pattern = re.compile(pattern)
|
922
|
+
has_groups = compiled_pattern.groups > 0
|
872
923
|
|
873
|
-
if
|
874
|
-
|
875
|
-
|
876
|
-
|
924
|
+
if has_groups:
|
925
|
+
# 如果有捕获组,只返回第一个匹配的捕获组内容
|
926
|
+
first_match = re.search(pattern, text)
|
927
|
+
if not first_match:
|
928
|
+
return default_value
|
929
|
+
|
930
|
+
# 获取第一个匹配的捕获组
|
931
|
+
if compiled_pattern.groups == 1:
|
932
|
+
# 只有一个捕获组,返回字符串
|
933
|
+
return first_match.group(1)
|
934
|
+
else:
|
935
|
+
# 多个捕获组,返回元组
|
936
|
+
return first_match.groups()
|
877
937
|
else:
|
878
|
-
|
938
|
+
# 如果没有捕获组,使用findall获取所有完整匹配
|
939
|
+
matches = re.findall(pattern, text)
|
940
|
+
|
941
|
+
if not matches:
|
942
|
+
return default_value
|
943
|
+
elif len(matches) == 1:
|
944
|
+
return matches[0]
|
945
|
+
else:
|
946
|
+
return matches
|
879
947
|
except Exception as e:
|
880
948
|
if default_value is not None:
|
881
949
|
return default_value
|
@@ -1013,7 +1081,8 @@ class HTTPRequest:
|
|
1013
1081
|
return isinstance(actual_value, str) and actual_value.endswith(str(expected_value))
|
1014
1082
|
elif assertion_type == "matches":
|
1015
1083
|
if not isinstance(actual_value, str):
|
1016
|
-
actual_value = str(
|
1084
|
+
actual_value = str(
|
1085
|
+
actual_value) if actual_value is not None else ""
|
1017
1086
|
try:
|
1018
1087
|
import re
|
1019
1088
|
pattern = str(expected_value)
|
@@ -1124,17 +1193,20 @@ class HTTPRequest:
|
|
1124
1193
|
elif operator == "startswith":
|
1125
1194
|
# startswith操作符检查actual是否以expected开头
|
1126
1195
|
if not isinstance(actual_value, str):
|
1127
|
-
actual_value = str(
|
1196
|
+
actual_value = str(
|
1197
|
+
actual_value) if actual_value is not None else ""
|
1128
1198
|
return actual_value.startswith(str(expected_value))
|
1129
1199
|
elif operator == "endswith":
|
1130
1200
|
# endswith操作符检查actual是否以expected结尾
|
1131
1201
|
if not isinstance(actual_value, str):
|
1132
|
-
actual_value = str(
|
1202
|
+
actual_value = str(
|
1203
|
+
actual_value) if actual_value is not None else ""
|
1133
1204
|
return actual_value.endswith(str(expected_value))
|
1134
1205
|
elif operator == "matches":
|
1135
1206
|
# matches操作符使用正则表达式进行匹配
|
1136
1207
|
if not isinstance(actual_value, str):
|
1137
|
-
actual_value = str(
|
1208
|
+
actual_value = str(
|
1209
|
+
actual_value) if actual_value is not None else ""
|
1138
1210
|
try:
|
1139
1211
|
import re
|
1140
1212
|
pattern = str(expected_value)
|
@@ -1198,7 +1270,8 @@ class HTTPRequest:
|
|
1198
1270
|
if "json" in request_kwargs and request_kwargs["json"]:
|
1199
1271
|
request_details.append("JSON Body:")
|
1200
1272
|
try:
|
1201
|
-
request_details.append(json.dumps(
|
1273
|
+
request_details.append(json.dumps(
|
1274
|
+
request_kwargs["json"], indent=2, ensure_ascii=False))
|
1202
1275
|
except:
|
1203
1276
|
request_details.append(str(request_kwargs["json"]))
|
1204
1277
|
elif "data" in request_kwargs and request_kwargs["data"]:
|
@@ -1243,7 +1316,8 @@ class HTTPRequest:
|
|
1243
1316
|
response_details.append("Body:")
|
1244
1317
|
try:
|
1245
1318
|
if 'application/json' in response.headers.get('Content-Type', ''):
|
1246
|
-
response_details.append(json.dumps(
|
1319
|
+
response_details.append(json.dumps(
|
1320
|
+
response.json(), indent=2, ensure_ascii=False))
|
1247
1321
|
elif len(response.content) < 10240: # 限制大小
|
1248
1322
|
response_details.append(response.text)
|
1249
1323
|
else:
|
@@ -1256,4 +1330,4 @@ class HTTPRequest:
|
|
1256
1330
|
"\n".join(response_details),
|
1257
1331
|
name=f"HTTP响应: {response_summary}",
|
1258
1332
|
attachment_type=allure.attachment_type.TEXT
|
1259
|
-
)
|
1333
|
+
)
|
@@ -110,7 +110,7 @@
|
|
110
110
|
asserts:
|
111
111
|
- ["status", "eq", 200]
|
112
112
|
- ["jsonpath", "$.args.task_id", "eq", "${task_id}"] # 这个断言一定会通过
|
113
|
-
- ["response_time", "lt",
|
113
|
+
- ["response_time", "lt", 4000] # 给2秒延迟留出足够的缓冲时间
|
114
114
|
''',断言重试次数: 3,断言重试间隔: 1
|
115
115
|
|
116
116
|
[打印],内容:'任务查询完成,URL: ${url}'
|
@@ -16,7 +16,7 @@
|
|
16
16
|
asserts:
|
17
17
|
- ["status", "eq", 200]
|
18
18
|
- ["jsonpath", "$.url", "contains", "httpbin.org"]
|
19
|
-
- ["response_time", "lt",
|
19
|
+
- ["response_time", "lt", 4000] # 给2秒延迟留出足够的缓冲时间
|
20
20
|
''',步骤名称: "旧版全局断言重试示例"
|
21
21
|
|
22
22
|
# 示例2: 使用indices指定重试特定断言
|
@@ -38,7 +38,7 @@
|
|
38
38
|
asserts:
|
39
39
|
- ["status", "eq", 200] # 索引0
|
40
40
|
- ["body", "matches", "^[0-9]+$"] # 索引1
|
41
|
-
- ["body", "lt", 6] # 索引2 -
|
41
|
+
- ["body", "lt", 6] # 索引2 - 需要重试,约50%概率失败
|
42
42
|
- ["response_time", "lt", 10000] # 索引3
|
43
43
|
retry_assertions:
|
44
44
|
count: 5
|
@@ -56,7 +56,7 @@
|
|
56
56
|
url: https://httpbin.org/delay/1
|
57
57
|
asserts:
|
58
58
|
- ["status", "eq", 200] # 索引0
|
59
|
-
- ["response_time", "lt",
|
59
|
+
- ["response_time", "lt", 3000] # 索引1 - 给1秒延迟留出缓冲
|
60
60
|
- ["jsonpath", "$.headers.Host", "eq", "httpbin.org"] # 索引2
|
61
61
|
- ["jsonpath", "$.url", "contains", "httpbin.org"] # 索引3
|
62
62
|
retry_assertions:
|
@@ -79,7 +79,7 @@
|
|
79
79
|
url: https://httpbin.org/status/200
|
80
80
|
asserts:
|
81
81
|
- ["status", "eq", 200]
|
82
|
-
- ["response_time", "lt",
|
82
|
+
- ["response_time", "lt", 5000] # 设置合理的响应时间限制
|
83
83
|
retry_assertions:
|
84
84
|
count: 3
|
85
85
|
interval: 1.5
|
@@ -13,7 +13,7 @@
|
|
13
13
|
asserts:
|
14
14
|
- ["status", "eq", 200]
|
15
15
|
- ["jsonpath", "$.url", "contains", "httpbin.org"]
|
16
|
-
- ["response_time", "lt",
|
16
|
+
- ["response_time", "lt", 4000] # 给2秒延迟留出足够的缓冲时间
|
17
17
|
retry_assertions:
|
18
18
|
count: 3
|
19
19
|
interval: 1
|
@@ -57,7 +57,7 @@
|
|
57
57
|
url: https://httpbin.org/delay/1
|
58
58
|
asserts:
|
59
59
|
- ["status", "eq", 200] # 索引0
|
60
|
-
- ["response_time", "lt",
|
60
|
+
- ["response_time", "lt", 3000] # 索引1 - 给1秒延迟留出缓冲
|
61
61
|
- ["jsonpath", "$.headers.Host", "eq", "httpbin.org"] # 索引2
|
62
62
|
retry_assertions:
|
63
63
|
# 全局设置
|
@@ -10,7 +10,7 @@
|
|
10
10
|
url: https://httpbin.org/delay/1
|
11
11
|
asserts:
|
12
12
|
- ["status", "eq", 200]
|
13
|
-
- ["response_time", "lt",
|
13
|
+
- ["response_time", "lt", 3000] # 给1秒延迟留出合理的缓冲时间
|
14
14
|
# 全局重试配置
|
15
15
|
retry_assertions:
|
16
16
|
count: 2
|
@@ -24,7 +24,7 @@
|
|
24
24
|
url: https://httpbin.org/delay/1
|
25
25
|
asserts:
|
26
26
|
- ["status", "eq", 200] # 索引0
|
27
|
-
- ["response_time", "lt",
|
27
|
+
- ["response_time", "lt", 3000] # 索引1,给1秒延迟留出缓冲
|
28
28
|
# 只重试指定索引的断言
|
29
29
|
retry_assertions:
|
30
30
|
count: 3
|
@@ -38,7 +38,7 @@
|
|
38
38
|
url: https://httpbin.org/delay/1
|
39
39
|
asserts:
|
40
40
|
- ["status", "eq", 200] # 索引0
|
41
|
-
- ["response_time", "lt",
|
41
|
+
- ["response_time", "lt", 3000] # 索引1,给1秒延迟留出缓冲
|
42
42
|
- ["body", "contains", "url"] # 索引2
|
43
43
|
# 为特定断言提供自定义配置
|
44
44
|
retry_assertions:
|
@@ -10,7 +10,7 @@
|
|
10
10
|
url: https://httpbin.org/delay/1
|
11
11
|
asserts:
|
12
12
|
- ["status", "eq", 200]
|
13
|
-
- ["response_time", "lt",
|
13
|
+
- ["response_time", "lt", 3000]
|
14
14
|
retry_assertions:
|
15
15
|
count: 2
|
16
16
|
interval: 1
|
@@ -23,7 +23,7 @@
|
|
23
23
|
url: https://httpbin.org/delay/1
|
24
24
|
asserts:
|
25
25
|
- ["status", "eq", 200]
|
26
|
-
- ["response_time", "lt",
|
26
|
+
- ["response_time", "lt", 3000]
|
27
27
|
retry_assertions:
|
28
28
|
count: 3
|
29
29
|
interval: 0.5
|
@@ -36,7 +36,7 @@
|
|
36
36
|
url: https://httpbin.org/delay/1
|
37
37
|
asserts:
|
38
38
|
- ["status", "eq", 200]
|
39
|
-
- ["response_time", "lt",
|
39
|
+
- ["response_time", "lt", 3000]
|
40
40
|
- ["body", "contains", "url"]
|
41
41
|
retry_assertions:
|
42
42
|
specific:
|
@@ -1,22 +1,33 @@
|
|
1
1
|
@name: 断言重试调试示例
|
2
|
-
@description:
|
2
|
+
@description: 使用随机数断言测试断言重试功能
|
3
3
|
@tags: [HTTP, API, 断言重试, 调试]
|
4
4
|
@author: Felix
|
5
5
|
@date: 2024-06-15
|
6
6
|
|
7
|
-
[打印],内容:'开始测试断言重试功能 -
|
7
|
+
[打印],内容:'开始测试断言重试功能 - 使用随机数模拟不确定结果'
|
8
8
|
|
9
|
-
#
|
9
|
+
# 使用随机数断言来模拟可能失败的断言,这比响应时间更可靠
|
10
10
|
[HTTP请求],客户端:'default',配置:'''
|
11
11
|
method: GET
|
12
|
-
url: https://
|
12
|
+
url: https://www.random.org/integers/
|
13
|
+
request:
|
14
|
+
params:
|
15
|
+
num: 1
|
16
|
+
min: 1
|
17
|
+
max: 10
|
18
|
+
col: 1
|
19
|
+
base: 10
|
20
|
+
format: plain
|
21
|
+
captures:
|
22
|
+
random_number: ["body"]
|
13
23
|
asserts:
|
14
24
|
- ["status", "eq", 200] # 这个会通过
|
15
|
-
- ["
|
25
|
+
- ["body", "lt", 5] # 约40%概率失败,用于测试重试逻辑
|
16
26
|
retry_assertions:
|
17
|
-
count:
|
18
|
-
interval:
|
27
|
+
count: 3
|
28
|
+
interval: 1
|
19
29
|
all: true
|
20
|
-
''',步骤名称:"
|
30
|
+
''',步骤名称:"使用随机数测试重试功能"
|
21
31
|
|
32
|
+
[打印],内容:'获取的随机数: ${random_number}'
|
22
33
|
[打印],内容:'测试完成 - 如果执行到这里,说明重试逻辑正常工作'
|