pytest-dsl 0.5.0__py3-none-any.whl → 0.7.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/cli.py +28 -33
- pytest_dsl/core/auto_decorator.py +72 -53
- pytest_dsl/core/auto_directory.py +8 -5
- pytest_dsl/core/dsl_executor.py +91 -23
- pytest_dsl/core/http_request.py +272 -221
- pytest_dsl/core/lexer.py +17 -17
- pytest_dsl/core/parser.py +52 -4
- pytest_dsl/core/parsetab.py +81 -70
- pytest_dsl/core/plugin_discovery.py +1 -8
- pytest_dsl/core/utils.py +43 -23
- pytest_dsl/core/variable_utils.py +215 -70
- pytest_dsl/core/yaml_loader.py +96 -19
- pytest_dsl/examples/assert/assertion_example.auto +1 -1
- pytest_dsl/examples/assert/boolean_test.auto +2 -2
- pytest_dsl/examples/assert/expression_test.auto +1 -1
- pytest_dsl/examples/custom/test_advanced_keywords.auto +2 -2
- pytest_dsl/examples/custom/test_custom_keywords.auto +2 -2
- pytest_dsl/examples/custom/test_default_values.auto +2 -2
- pytest_dsl/examples/http/file_reference_test.auto +1 -1
- pytest_dsl/examples/http/http_advanced.auto +1 -1
- pytest_dsl/examples/http/http_example.auto +1 -1
- pytest_dsl/examples/http/http_length_test.auto +1 -1
- pytest_dsl/examples/http/http_retry_assertions.auto +1 -1
- pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +2 -2
- pytest_dsl/examples/http/http_with_yaml.auto +1 -1
- pytest_dsl/examples/quickstart/api_basics.auto +1 -1
- pytest_dsl/examples/quickstart/assertions.auto +1 -1
- pytest_dsl/examples/quickstart/loops.auto +2 -2
- pytest_dsl/keywords/assertion_keywords.py +76 -62
- pytest_dsl/keywords/global_keywords.py +43 -4
- pytest_dsl/keywords/http_keywords.py +58 -56
- pytest_dsl-0.7.0.dist-info/METADATA +1040 -0
- pytest_dsl-0.7.0.dist-info/RECORD +67 -0
- {pytest_dsl-0.5.0.dist-info → pytest_dsl-0.7.0.dist-info}/WHEEL +1 -1
- pytest_dsl/parsetab.py +0 -69
- pytest_dsl-0.5.0.dist-info/METADATA +0 -596
- pytest_dsl-0.5.0.dist-info/RECORD +0 -68
- {pytest_dsl-0.5.0.dist-info → pytest_dsl-0.7.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.5.0.dist-info → pytest_dsl-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.5.0.dist-info → pytest_dsl-0.7.0.dist-info}/top_level.txt +0 -0
pytest_dsl/core/http_request.py
CHANGED
@@ -13,30 +13,31 @@ from pytest_dsl.core.http_client import http_client_manager
|
|
13
13
|
|
14
14
|
# 定义支持的比较操作符
|
15
15
|
COMPARISON_OPERATORS = {
|
16
|
-
"eq", "neq", "lt", "lte", "gt", "gte", "in", "not_in", "matches"
|
16
|
+
"eq", "neq", "lt", "lte", "gt", "gte", "in", "not_in", "matches",
|
17
|
+
"contains", "not_contains", "startswith", "endswith"
|
17
18
|
}
|
18
19
|
|
19
20
|
# 定义支持的断言类型
|
20
21
|
ASSERTION_TYPES = {
|
21
|
-
"exists", "not_exists", "contains", "not_contains", "startswith",
|
22
|
+
"exists", "not_exists", "contains", "not_contains", "startswith",
|
22
23
|
"endswith", "matches", "type", "length", "schema", "value"
|
23
24
|
}
|
24
25
|
|
25
26
|
# 操作符和断言类型重叠的集合(可以作为二者使用的)
|
26
27
|
DUAL_PURPOSE = {
|
27
|
-
"contains", "not_contains", "matches", "in", "not_in"
|
28
|
+
"contains", "not_contains", "matches", "in", "not_in", "startswith", "endswith"
|
28
29
|
}
|
29
30
|
|
30
31
|
|
31
32
|
class HTTPRequest:
|
32
33
|
"""HTTP请求处理类
|
33
|
-
|
34
|
+
|
34
35
|
负责处理HTTP请求、响应捕获和断言
|
35
36
|
"""
|
36
|
-
|
37
|
+
|
37
38
|
def __init__(self, config: Dict[str, Any], client_name: str = "default", session_name: str = None):
|
38
39
|
"""初始化HTTP请求
|
39
|
-
|
40
|
+
|
40
41
|
Args:
|
41
42
|
config: 请求配置
|
42
43
|
client_name: 客户端名称
|
@@ -47,13 +48,13 @@ class HTTPRequest:
|
|
47
48
|
self.session_name = session_name
|
48
49
|
self.response = None
|
49
50
|
self.captured_values = {}
|
50
|
-
|
51
|
+
|
51
52
|
def execute(self, disable_auth: bool = False) -> Response:
|
52
53
|
"""执行HTTP请求
|
53
|
-
|
54
|
+
|
54
55
|
Args:
|
55
56
|
disable_auth: 是否禁用认证
|
56
|
-
|
57
|
+
|
57
58
|
Returns:
|
58
59
|
Response对象
|
59
60
|
"""
|
@@ -62,7 +63,7 @@ class HTTPRequest:
|
|
62
63
|
client = http_client_manager.get_session(self.session_name, self.client_name)
|
63
64
|
else:
|
64
65
|
client = http_client_manager.get_client(self.client_name)
|
65
|
-
|
66
|
+
|
66
67
|
# 验证客户端有效性
|
67
68
|
if client is None:
|
68
69
|
error_message = f"无法获取HTTP客户端: {self.client_name}"
|
@@ -72,16 +73,16 @@ class HTTPRequest:
|
|
72
73
|
attachment_type=allure.attachment_type.TEXT
|
73
74
|
)
|
74
75
|
raise ValueError(error_message)
|
75
|
-
|
76
|
+
|
76
77
|
# 提取请求参数
|
77
78
|
method = self.config.get('method', 'GET').upper()
|
78
79
|
url = self.config.get('url', '')
|
79
|
-
|
80
|
+
|
80
81
|
# 配置中是否禁用认证
|
81
82
|
disable_auth = disable_auth or self.config.get('disable_auth', False)
|
82
|
-
|
83
|
+
|
83
84
|
request_config = self.config.get('request', {})
|
84
|
-
|
85
|
+
|
85
86
|
# 构建请求参数
|
86
87
|
request_kwargs = {
|
87
88
|
'params': request_config.get('params'),
|
@@ -98,23 +99,23 @@ class HTTPRequest:
|
|
98
99
|
'proxies': request_config.get('proxies'),
|
99
100
|
'disable_auth': disable_auth # 传递禁用认证标志
|
100
101
|
}
|
101
|
-
|
102
|
+
|
102
103
|
# 过滤掉None值
|
103
104
|
request_kwargs = {k: v for k, v in request_kwargs.items() if v is not None}
|
104
|
-
|
105
|
+
|
105
106
|
# 使用Allure记录请求信息
|
106
107
|
self._log_request_to_allure(method, url, request_kwargs)
|
107
|
-
|
108
|
+
|
108
109
|
try:
|
109
110
|
# 发送请求
|
110
111
|
self.response = client.make_request(method, url, **request_kwargs)
|
111
|
-
|
112
|
+
|
112
113
|
# 使用Allure记录响应信息
|
113
114
|
self._log_response_to_allure(self.response)
|
114
|
-
|
115
|
+
|
115
116
|
# 处理捕获
|
116
117
|
self.process_captures()
|
117
|
-
|
118
|
+
|
118
119
|
return self.response
|
119
120
|
except requests.exceptions.RequestException as e:
|
120
121
|
# 记录请求异常到Allure
|
@@ -124,7 +125,7 @@ class HTTPRequest:
|
|
124
125
|
name=f"HTTP请求失败: {method} {url}",
|
125
126
|
attachment_type=allure.attachment_type.TEXT
|
126
127
|
)
|
127
|
-
|
128
|
+
|
128
129
|
# 重新抛出更有意义的异常
|
129
130
|
raise ValueError(f"HTTP请求失败: {str(e)}") from e
|
130
131
|
except Exception as e:
|
@@ -135,16 +136,16 @@ class HTTPRequest:
|
|
135
136
|
name=f"HTTP请求执行错误: {method} {url}",
|
136
137
|
attachment_type=allure.attachment_type.TEXT
|
137
138
|
)
|
138
|
-
|
139
|
+
|
139
140
|
# 重新抛出异常
|
140
141
|
raise ValueError(f"HTTP请求执行错误: {str(e)}") from e
|
141
|
-
|
142
|
+
|
142
143
|
def _ensure_response_exists(self, operation: str = "处理"):
|
143
144
|
"""确保响应对象存在
|
144
|
-
|
145
|
+
|
145
146
|
Args:
|
146
147
|
operation: 操作名称,用于错误消息
|
147
|
-
|
148
|
+
|
148
149
|
Raises:
|
149
150
|
ValueError: 如果响应对象不存在
|
150
151
|
"""
|
@@ -162,26 +163,26 @@ class HTTPRequest:
|
|
162
163
|
attachment_type=allure.attachment_type.TEXT
|
163
164
|
)
|
164
165
|
raise ValueError(error_message)
|
165
|
-
|
166
|
+
|
166
167
|
def process_captures(self) -> Dict[str, Any]:
|
167
168
|
"""处理响应捕获
|
168
|
-
|
169
|
+
|
169
170
|
Returns:
|
170
171
|
捕获的值字典
|
171
172
|
"""
|
172
173
|
self._ensure_response_exists("捕获响应")
|
173
|
-
|
174
|
+
|
174
175
|
captures_config = self.config.get('captures', {})
|
175
|
-
|
176
|
+
|
176
177
|
for var_name, capture_spec in captures_config.items():
|
177
178
|
if not isinstance(capture_spec, list):
|
178
179
|
raise ValueError(f"无效的捕获规格: {var_name}: {capture_spec}")
|
179
|
-
|
180
|
+
|
180
181
|
# 提取捕获信息
|
181
182
|
try:
|
182
183
|
extractor_type = capture_spec[0]
|
183
184
|
extraction_path = capture_spec[1] if len(capture_spec) > 1 else None
|
184
|
-
|
185
|
+
|
185
186
|
# 检查是否有length参数
|
186
187
|
is_length_capture = False
|
187
188
|
if len(capture_spec) > 2 and capture_spec[2] == "length":
|
@@ -189,16 +190,16 @@ class HTTPRequest:
|
|
189
190
|
default_value = capture_spec[3] if len(capture_spec) > 3 else None
|
190
191
|
else:
|
191
192
|
default_value = capture_spec[2] if len(capture_spec) > 2 else None
|
192
|
-
|
193
|
+
|
193
194
|
# 提取值
|
194
195
|
captured_value = self._extract_value(extractor_type, extraction_path, default_value)
|
195
|
-
|
196
|
+
|
196
197
|
# 特殊处理length
|
197
198
|
if is_length_capture:
|
198
199
|
try:
|
199
200
|
original_value = captured_value
|
200
201
|
captured_value = len(captured_value)
|
201
|
-
|
202
|
+
|
202
203
|
# 记录长度到Allure
|
203
204
|
allure.attach(
|
204
205
|
f"变量名: {var_name}\n提取器: {extractor_type}\n路径: {extraction_path}\n原始值: {str(original_value)}\n长度: {captured_value}",
|
@@ -226,7 +227,7 @@ class HTTPRequest:
|
|
226
227
|
name=f"捕获变量: {var_name}",
|
227
228
|
attachment_type=allure.attachment_type.TEXT
|
228
229
|
)
|
229
|
-
|
230
|
+
|
230
231
|
self.captured_values[var_name] = captured_value
|
231
232
|
except Exception as e:
|
232
233
|
error_msg = (
|
@@ -241,12 +242,12 @@ class HTTPRequest:
|
|
241
242
|
)
|
242
243
|
# 设置默认值
|
243
244
|
self.captured_values[var_name] = None
|
244
|
-
|
245
|
+
|
245
246
|
return self.captured_values
|
246
|
-
|
247
|
-
def _format_error_details(self,
|
248
|
-
extractor_type: str,
|
249
|
-
extraction_path: str,
|
247
|
+
|
248
|
+
def _format_error_details(self,
|
249
|
+
extractor_type: str,
|
250
|
+
extraction_path: str,
|
250
251
|
assertion_type: str,
|
251
252
|
compare_operator: str,
|
252
253
|
actual_value: Any,
|
@@ -255,7 +256,7 @@ class HTTPRequest:
|
|
255
256
|
error_message: str = None,
|
256
257
|
additional_context: str = None) -> str:
|
257
258
|
"""格式化断言错误的详细信息
|
258
|
-
|
259
|
+
|
259
260
|
Args:
|
260
261
|
extractor_type: 提取器类型
|
261
262
|
extraction_path: 提取路径
|
@@ -266,24 +267,24 @@ class HTTPRequest:
|
|
266
267
|
original_actual_value: 原始值(用于length断言)
|
267
268
|
error_message: 错误消息
|
268
269
|
additional_context: 附加上下文信息
|
269
|
-
|
270
|
+
|
270
271
|
Returns:
|
271
272
|
格式化的错误详情字符串
|
272
273
|
"""
|
273
274
|
error_details = []
|
274
|
-
|
275
|
+
|
275
276
|
# 添加基本错误信息
|
276
277
|
prefix = "断言执行错误" if additional_context else "断言失败"
|
277
278
|
error_details.append(f"{prefix} [{extractor_type}]")
|
278
|
-
|
279
|
+
|
279
280
|
# 添加异常类型信息(如果存在)
|
280
281
|
if additional_context:
|
281
282
|
error_details.append(f"异常类型: {additional_context}")
|
282
|
-
|
283
|
+
|
283
284
|
# 添加路径信息
|
284
285
|
if extraction_path:
|
285
286
|
error_details.append(f"路径: {extraction_path}")
|
286
|
-
|
287
|
+
|
287
288
|
# 添加断言类型和值信息
|
288
289
|
if assertion_type == "length":
|
289
290
|
error_details.append(f"断言类型: 长度比较")
|
@@ -293,93 +294,97 @@ class HTTPRequest:
|
|
293
294
|
else:
|
294
295
|
error_details.append(f"断言类型: {assertion_type}")
|
295
296
|
error_details.append(f"实际值: {actual_value}")
|
296
|
-
|
297
|
+
|
297
298
|
# 添加比较操作符
|
298
299
|
if compare_operator:
|
299
300
|
error_details.append(f"比较操作符: {compare_operator}")
|
300
|
-
|
301
|
+
|
301
302
|
# 添加预期值
|
302
303
|
if expected_value is not None:
|
303
304
|
error_details.append(f"预期值: {expected_value}")
|
304
|
-
|
305
|
+
|
305
306
|
# 添加类型信息
|
306
307
|
error_details.append(f"实际类型: {type(actual_value).__name__}")
|
307
308
|
if expected_value is not None:
|
308
309
|
error_details.append(f"预期类型: {type(expected_value).__name__}")
|
309
|
-
|
310
|
+
|
310
311
|
# 添加错误消息
|
311
312
|
if error_message:
|
312
313
|
error_details.append(f"错误信息: {error_message}")
|
313
|
-
|
314
|
+
|
314
315
|
return "\n".join(error_details)
|
315
316
|
|
316
|
-
def process_asserts(self, specific_asserts=None) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
317
|
+
def process_asserts(self, specific_asserts=None, index_mapping=None) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
317
318
|
"""处理响应断言
|
318
|
-
|
319
|
+
|
319
320
|
Args:
|
320
321
|
specific_asserts: 指定要处理的断言列表,如果为None则处理所有断言
|
321
|
-
|
322
|
+
index_mapping: 索引映射字典,用于将新索引映射到原始索引(用于重试场景)
|
323
|
+
|
322
324
|
Returns:
|
323
325
|
Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: 断言结果列表和失败需重试的断言列表
|
324
326
|
"""
|
325
327
|
self._ensure_response_exists("进行断言")
|
326
|
-
|
328
|
+
|
327
329
|
asserts_config = self.config.get('asserts', [])
|
328
330
|
assert_results = []
|
329
331
|
failed_retryable_assertions = []
|
330
332
|
failed_assertions = [] # 收集所有失败的断言
|
331
|
-
|
333
|
+
|
332
334
|
# 处理断言重试配置
|
333
335
|
# 1. 只使用独立的retry_assertions配置
|
334
336
|
retry_assertions_config = self.config.get('retry_assertions', {})
|
335
|
-
|
336
|
-
|
337
|
+
|
337
338
|
# 2. 向后兼容: 检查全局retry配置(仅作为默认值使用)
|
338
339
|
retry_config = self.config.get('retry', {})
|
339
340
|
global_retry_enabled = bool(retry_config)
|
340
|
-
|
341
|
+
|
341
342
|
# 3. 提取重试默认设置
|
342
343
|
global_retry_count = retry_assertions_config.get('count', retry_config.get('count', 3))
|
343
344
|
global_retry_interval = retry_assertions_config.get('interval', retry_config.get('interval', 1))
|
344
|
-
|
345
|
+
|
345
346
|
# 4. 提取应该重试的断言索引列表
|
346
347
|
retry_all_assertions = retry_assertions_config.get('all', global_retry_enabled)
|
347
348
|
retry_assertion_indices = retry_assertions_config.get('indices', [])
|
348
|
-
|
349
|
+
|
349
350
|
# 5. 提取特定断言的重试配置
|
350
351
|
specific_assertion_configs = retry_assertions_config.get('specific', {})
|
351
|
-
|
352
|
+
|
352
353
|
# 如果传入了specific_asserts,只处理指定的断言
|
353
354
|
process_asserts = specific_asserts if specific_asserts is not None else asserts_config
|
354
|
-
|
355
|
+
|
355
356
|
for assertion_idx, assertion in enumerate(process_asserts):
|
356
357
|
if not isinstance(assertion, list) or len(assertion) < 2:
|
357
358
|
raise ValueError(f"无效的断言配置: {assertion}")
|
358
|
-
|
359
|
+
|
359
360
|
# 提取断言参数
|
360
361
|
extractor_type = assertion[0]
|
361
|
-
|
362
|
+
|
363
|
+
# 获取原始索引(用于重试配置查找)
|
364
|
+
original_idx = index_mapping.get(assertion_idx, assertion_idx) if index_mapping else assertion_idx
|
365
|
+
|
362
366
|
# 判断该断言是否应该重试(只使用retry_assertions配置)
|
363
367
|
is_retryable = False
|
364
368
|
assertion_retry_count = global_retry_count
|
365
369
|
assertion_retry_interval = global_retry_interval
|
366
|
-
|
367
|
-
# retry_assertions特定配置
|
368
|
-
|
369
|
-
|
370
|
+
|
371
|
+
# retry_assertions特定配置 - 使用原始索引和支持整数键
|
372
|
+
original_idx_str = str(original_idx)
|
373
|
+
if original_idx_str in specific_assertion_configs or original_idx in specific_assertion_configs:
|
374
|
+
spec_config = specific_assertion_configs.get(original_idx_str) or specific_assertion_configs.get(original_idx)
|
370
375
|
is_retryable = True
|
371
376
|
if isinstance(spec_config, dict):
|
372
377
|
if 'count' in spec_config:
|
373
378
|
assertion_retry_count = spec_config['count']
|
374
379
|
if 'interval' in spec_config:
|
375
380
|
assertion_retry_interval = spec_config['interval']
|
376
|
-
# retry_assertions索引列表
|
377
|
-
elif
|
381
|
+
# retry_assertions索引列表 - 使用原始索引
|
382
|
+
elif original_idx in retry_assertion_indices:
|
378
383
|
is_retryable = True
|
379
384
|
# retry_assertions全局配置
|
380
385
|
elif retry_all_assertions:
|
381
386
|
is_retryable = True
|
382
|
-
|
387
|
+
|
383
388
|
# 处理断言参数
|
384
389
|
if len(assertion) == 2: # 简单存在性断言 ["header", "Location"]
|
385
390
|
extraction_path = assertion[1]
|
@@ -391,7 +396,7 @@ class HTTPRequest:
|
|
391
396
|
if assertion[1] in COMPARISON_OPERATORS or assertion[1] in DUAL_PURPOSE:
|
392
397
|
# 这是操作符格式的断言 ["status", "eq", 200]
|
393
398
|
extraction_path = None if extractor_type in ["status", "body", "response_time"] else assertion[1]
|
394
|
-
|
399
|
+
|
395
400
|
# 特殊处理二者兼用的断言类型
|
396
401
|
if assertion[1] in DUAL_PURPOSE:
|
397
402
|
if extractor_type in ["jsonpath", "body", "header"]:
|
@@ -405,33 +410,64 @@ class HTTPRequest:
|
|
405
410
|
else:
|
406
411
|
assertion_type = "value" # 标记为值比较
|
407
412
|
compare_operator = assertion[1]
|
408
|
-
|
413
|
+
|
409
414
|
expected_value = assertion[2] # 预期值
|
410
415
|
else:
|
411
|
-
#
|
416
|
+
# 这可能是断言类型格式 ["jsonpath", "$.id", "exists"] 或者是简化的长度断言 ["body", "length", 10]
|
412
417
|
extraction_path = assertion[1]
|
413
|
-
|
414
|
-
|
415
|
-
#
|
416
|
-
if
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
if assertion_type == "type":
|
423
|
-
raise ValueError(f"断言类型 'type' 需要预期值(string/number/boolean/array/object/null),但未提供: {assertion}")
|
424
|
-
# 断言类型作为操作符
|
425
|
-
expected_value = None
|
426
|
-
compare_operator = assertion_type # 使用断言类型作为操作符
|
418
|
+
potential_assertion_type = assertion[2]
|
419
|
+
|
420
|
+
# 特殊处理:如果第二个参数是"length",第三个参数是数字,这是简化的长度断言
|
421
|
+
if extraction_path == "length" and isinstance(potential_assertion_type, (int, float)):
|
422
|
+
# 这是 ["body", "length", 10] 格式的长度断言
|
423
|
+
extraction_path = None # body提取器不需要路径
|
424
|
+
assertion_type = "length"
|
425
|
+
compare_operator = "eq"
|
426
|
+
expected_value = potential_assertion_type
|
427
427
|
else:
|
428
|
-
|
429
|
-
|
430
|
-
|
428
|
+
# 这是标准的断言类型格式 ["jsonpath", "$.id", "exists"]
|
429
|
+
assertion_type = potential_assertion_type
|
430
|
+
|
431
|
+
# 特殊处理schema断言,因为schema值可能是字典
|
432
|
+
if assertion_type == "schema" or isinstance(assertion_type, dict):
|
433
|
+
if isinstance(assertion_type, dict):
|
434
|
+
# 这是 ["body", "schema", {schema_dict}] 格式
|
435
|
+
assertion_type = "schema"
|
436
|
+
expected_value = potential_assertion_type
|
437
|
+
compare_operator = "schema"
|
438
|
+
extraction_path = None # body提取器不需要路径
|
439
|
+
else:
|
440
|
+
# 这是 ["jsonpath", "$.data", "schema"] 格式,需要在4参数中处理
|
441
|
+
expected_value = None
|
442
|
+
compare_operator = assertion_type
|
443
|
+
else:
|
444
|
+
# 检查断言类型是否有效
|
445
|
+
if assertion_type not in ASSERTION_TYPES:
|
446
|
+
raise ValueError(f"不支持的断言类型: {assertion_type} 在 {assertion}")
|
447
|
+
|
448
|
+
# 检查此断言类型是否需要预期值
|
449
|
+
if assertion_type not in ["exists", "not_exists"]:
|
450
|
+
# 特殊处理类型断言,它确实需要一个值但在这种格式中没有提供
|
451
|
+
if assertion_type == "type":
|
452
|
+
raise ValueError(f"断言类型 'type' 需要预期值(string/number/boolean/array/object/null),但未提供: {assertion}")
|
453
|
+
# 断言类型作为操作符
|
454
|
+
expected_value = None
|
455
|
+
compare_operator = assertion_type # 使用断言类型作为操作符
|
456
|
+
else:
|
457
|
+
expected_value = None
|
458
|
+
compare_operator = assertion_type # 存在性断言的操作符就是断言类型本身
|
459
|
+
elif len(assertion) == 4: # 带操作符的断言 ["jsonpath", "$.id", "eq", 1] 或特殊断言 ["jsonpath", "$.type", "type", "string"] 或长度断言 ["body", "length", "gt", 50]
|
431
460
|
extraction_path = assertion[1]
|
432
|
-
|
461
|
+
|
462
|
+
# 特殊处理长度断言:["body", "length", "gt", 50]
|
463
|
+
if extraction_path == "length" and assertion[2] in COMPARISON_OPERATORS:
|
464
|
+
# 这是4参数的长度断言格式
|
465
|
+
extraction_path = None # body提取器不需要路径
|
466
|
+
assertion_type = "length"
|
467
|
+
compare_operator = assertion[2] # 比较操作符
|
468
|
+
expected_value = assertion[3] # 预期长度值
|
433
469
|
# 检查第三个元素是否是操作符
|
434
|
-
|
470
|
+
elif assertion[2] in COMPARISON_OPERATORS or assertion[2] in DUAL_PURPOSE:
|
435
471
|
# 这是操作符形式的断言
|
436
472
|
assertion_type = "value" # 标记为值比较
|
437
473
|
compare_operator = assertion[2] # 比较操作符
|
@@ -440,15 +476,13 @@ class HTTPRequest:
|
|
440
476
|
# 检查断言类型是否有效
|
441
477
|
if assertion[2] not in ASSERTION_TYPES:
|
442
478
|
raise ValueError(f"不支持的断言类型: {assertion[2]} 在 {assertion}")
|
443
|
-
|
479
|
+
|
444
480
|
# 其他类型的断言,比如特殊断言
|
445
481
|
assertion_type = assertion[2]
|
446
|
-
|
482
|
+
|
447
483
|
# 根据断言类型决定如何处理操作符和期望值
|
448
484
|
if assertion_type == "length":
|
449
|
-
#
|
450
|
-
if len(assertion) < 4:
|
451
|
-
raise ValueError(f"长度断言需要提供预期值: {assertion}")
|
485
|
+
# 对于4参数的长度断言,第4个参数是期望值,默认使用eq比较
|
452
486
|
compare_operator = "eq" # 默认使用相等比较
|
453
487
|
expected_value = assertion[3] # 预期长度值
|
454
488
|
else:
|
@@ -458,17 +492,17 @@ class HTTPRequest:
|
|
458
492
|
else: # 5个参数,例如 ["jsonpath", "$", "length", "eq", 10]
|
459
493
|
extraction_path = assertion[1]
|
460
494
|
assertion_type = assertion[2]
|
461
|
-
|
495
|
+
|
462
496
|
# 检查断言类型是否有效
|
463
497
|
if assertion_type not in ASSERTION_TYPES:
|
464
498
|
raise ValueError(f"不支持的断言类型: {assertion_type} 在 {assertion}")
|
465
|
-
|
499
|
+
|
466
500
|
# 特殊处理长度断言
|
467
501
|
if assertion_type == "length":
|
468
502
|
# 验证第四个元素是有效的比较操作符
|
469
503
|
if assertion[3] not in COMPARISON_OPERATORS:
|
470
504
|
raise ValueError(f"长度断言的比较操作符必须是 {', '.join(COMPARISON_OPERATORS)} 之一: {assertion}")
|
471
|
-
|
505
|
+
|
472
506
|
# 验证第五个元素是有效的长度值
|
473
507
|
try:
|
474
508
|
# 尝试将预期长度转换为整数
|
@@ -477,47 +511,54 @@ class HTTPRequest:
|
|
477
511
|
raise ValueError(f"长度断言的预期值必须是非负整数: {assertion}")
|
478
512
|
except (ValueError, TypeError):
|
479
513
|
raise ValueError(f"长度断言的预期值必须是有效的整数: {assertion}")
|
480
|
-
|
514
|
+
|
481
515
|
compare_operator = assertion[3]
|
482
516
|
expected_value = assertion[4]
|
483
|
-
|
517
|
+
|
484
518
|
# 提取实际值
|
485
519
|
actual_value = self._extract_value(extractor_type, extraction_path)
|
486
|
-
|
520
|
+
|
487
521
|
# 特殊处理"length"断言类型
|
488
522
|
original_actual_value = actual_value
|
489
|
-
if assertion_type == "length"
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
523
|
+
if assertion_type == "length":
|
524
|
+
# 只对不支持长度计算的提取器类型跳过长度计算
|
525
|
+
# response_time和status返回数值,对数值计算长度没有意义
|
526
|
+
if extractor_type in ["response_time", "status"]:
|
527
|
+
# 对于这些类型,直接使用数值本身,不计算长度
|
528
|
+
pass
|
529
|
+
else:
|
530
|
+
# 对于其他类型(包括body、jsonpath、header等),尝试计算长度
|
531
|
+
try:
|
532
|
+
# 记录长度断言的原始值信息
|
533
|
+
allure.attach(
|
534
|
+
f"提取器: {extractor_type}\n路径: {extraction_path}\n原始值: {original_actual_value}\n类型: {type(original_actual_value).__name__}",
|
535
|
+
name=f"长度断言原始值: {extractor_type}",
|
536
|
+
attachment_type=allure.attachment_type.TEXT
|
537
|
+
)
|
538
|
+
|
539
|
+
actual_value = len(actual_value)
|
540
|
+
|
541
|
+
# 记录计算结果
|
542
|
+
allure.attach(
|
543
|
+
f"提取器: {extractor_type}\n路径: {extraction_path}\n长度: {actual_value}",
|
544
|
+
name=f"长度断言计算结果: {extractor_type}",
|
545
|
+
attachment_type=allure.attachment_type.TEXT
|
546
|
+
)
|
547
|
+
except Exception as e:
|
548
|
+
# 记录长度计算失败的详细信息
|
549
|
+
error_msg = (
|
550
|
+
f"无法计算长度: {type(e).__name__}: {str(e)}\n"
|
551
|
+
f"类型: {type(original_actual_value).__name__}\n"
|
552
|
+
f"值: {original_actual_value}"
|
553
|
+
)
|
554
|
+
allure.attach(
|
555
|
+
error_msg,
|
556
|
+
name=f"长度计算失败: {extractor_type} {extraction_path}",
|
557
|
+
attachment_type=allure.attachment_type.TEXT
|
558
|
+
)
|
559
|
+
# 抛出更具体的异常,而不是用0替代
|
560
|
+
raise ValueError(f"断言类型'length'无法应用于值 '{original_actual_value}': {str(e)}")
|
561
|
+
|
521
562
|
# 执行断言
|
522
563
|
assertion_result = {
|
523
564
|
'type': extractor_type,
|
@@ -530,18 +571,18 @@ class HTTPRequest:
|
|
530
571
|
'retryable': is_retryable,
|
531
572
|
'retry_count': assertion_retry_count,
|
532
573
|
'retry_interval': assertion_retry_interval,
|
533
|
-
'index':
|
574
|
+
'index': original_idx # 记录断言在原始列表中的索引
|
534
575
|
}
|
535
|
-
|
576
|
+
|
536
577
|
try:
|
537
578
|
# 验证断言
|
538
579
|
result = self._perform_assertion(assertion_type, compare_operator, actual_value, expected_value)
|
539
580
|
assertion_result['result'] = result
|
540
|
-
|
581
|
+
|
541
582
|
# 根据返回的布尔值确定断言是否通过
|
542
583
|
if result:
|
543
584
|
assertion_result['passed'] = True
|
544
|
-
|
585
|
+
|
545
586
|
# 使用Allure记录断言成功
|
546
587
|
allure.attach(
|
547
588
|
self._format_assertion_details(assertion_result),
|
@@ -552,18 +593,18 @@ class HTTPRequest:
|
|
552
593
|
# 断言失败但没有抛出异常
|
553
594
|
assertion_result['passed'] = False
|
554
595
|
assertion_result['error'] = "断言失败"
|
555
|
-
|
596
|
+
|
556
597
|
# 使用Allure记录断言失败
|
557
598
|
allure.attach(
|
558
599
|
self._format_assertion_details(assertion_result) + "\n\n错误: 断言结果为False",
|
559
600
|
name=f"断言失败: {extractor_type}",
|
560
601
|
attachment_type=allure.attachment_type.TEXT
|
561
602
|
)
|
562
|
-
|
603
|
+
|
563
604
|
# 如果断言可重试,添加到失败且需要重试的断言列表
|
564
605
|
if is_retryable:
|
565
606
|
failed_retryable_assertions.append(assertion_result)
|
566
|
-
|
607
|
+
|
567
608
|
# 构建格式化的错误消息
|
568
609
|
formatted_error = self._format_error_details(
|
569
610
|
extractor_type=extractor_type,
|
@@ -574,16 +615,16 @@ class HTTPRequest:
|
|
574
615
|
expected_value=expected_value,
|
575
616
|
original_actual_value=original_actual_value
|
576
617
|
)
|
577
|
-
|
618
|
+
|
578
619
|
# 收集断言失败,而不是立即抛出异常
|
579
620
|
failed_assertions.append((assertion_idx, formatted_error))
|
580
|
-
|
621
|
+
|
581
622
|
except AssertionError as e:
|
582
623
|
# 处理上面可能抛出的断言错误,或者其他断言错误
|
583
624
|
assertion_result['result'] = False
|
584
625
|
assertion_result['passed'] = False
|
585
626
|
assertion_result['error'] = str(e)
|
586
|
-
|
627
|
+
|
587
628
|
# 如果异常已经是格式化的多行消息,直接使用
|
588
629
|
if "\n" in str(e):
|
589
630
|
formatted_error = str(e)
|
@@ -599,27 +640,27 @@ class HTTPRequest:
|
|
599
640
|
original_actual_value=original_actual_value,
|
600
641
|
error_message=str(e)
|
601
642
|
)
|
602
|
-
|
643
|
+
|
603
644
|
# 使用Allure记录断言失败
|
604
645
|
allure.attach(
|
605
646
|
self._format_assertion_details(assertion_result) + f"\n\n错误: {str(e)}",
|
606
647
|
name=f"断言失败: {extractor_type}",
|
607
648
|
attachment_type=allure.attachment_type.TEXT
|
608
649
|
)
|
609
|
-
|
650
|
+
|
610
651
|
# 如果断言可重试,添加到失败且需要重试的断言列表
|
611
652
|
if is_retryable:
|
612
653
|
failed_retryable_assertions.append(assertion_result)
|
613
|
-
|
654
|
+
|
614
655
|
# 收集断言失败,而不是立即抛出异常
|
615
656
|
failed_assertions.append((assertion_idx, formatted_error))
|
616
|
-
|
657
|
+
|
617
658
|
except Exception as e:
|
618
659
|
# 处理其他类型的异常
|
619
660
|
assertion_result['result'] = False
|
620
661
|
assertion_result['passed'] = False
|
621
662
|
assertion_result['error'] = f"未预期的异常: {type(e).__name__}: {str(e)}"
|
622
|
-
|
663
|
+
|
623
664
|
# 使用辅助方法构建详细的错误消息
|
624
665
|
formatted_error = self._format_error_details(
|
625
666
|
extractor_type=extractor_type,
|
@@ -632,24 +673,24 @@ class HTTPRequest:
|
|
632
673
|
error_message=str(e),
|
633
674
|
additional_context=type(e).__name__
|
634
675
|
)
|
635
|
-
|
676
|
+
|
636
677
|
# 使用Allure记录断言错误
|
637
678
|
allure.attach(
|
638
679
|
self._format_assertion_details(assertion_result) + f"\n\n错误: {assertion_result['error']}",
|
639
680
|
name=f"断言执行错误: {extractor_type}",
|
640
681
|
attachment_type=allure.attachment_type.TEXT
|
641
682
|
)
|
642
|
-
|
683
|
+
|
643
684
|
# 收集断言失败,而不是立即抛出异常
|
644
685
|
failed_assertions.append((assertion_idx, formatted_error))
|
645
|
-
|
686
|
+
|
646
687
|
assert_results.append(assertion_result)
|
647
|
-
|
688
|
+
|
648
689
|
# 执行完所有断言后,如果有失败的断言,抛出异常
|
649
690
|
if failed_assertions:
|
650
691
|
# 检查是否只收集失败而不抛出异常(由重试机制设置)
|
651
692
|
collect_only = self.config.get('_collect_failed_assertions_only', False)
|
652
|
-
|
693
|
+
|
653
694
|
if not collect_only:
|
654
695
|
if len(failed_assertions) == 1:
|
655
696
|
# 只有一个断言失败时,直接使用该断言的错误消息
|
@@ -664,63 +705,63 @@ class HTTPRequest:
|
|
664
705
|
extractor_type = error_msg.split("[", 1)[1].split("]")[0] if "[" in error_msg else "未知"
|
665
706
|
else:
|
666
707
|
extractor_type = "未知"
|
667
|
-
|
708
|
+
|
668
709
|
# 生成简短的断言标题
|
669
710
|
assertion_title = f"断言 #{assertion_idx+1} [{extractor_type}]"
|
670
|
-
|
711
|
+
|
671
712
|
# 添加分隔线使错误更容易辨别
|
672
713
|
error_summary += f"\n{'-' * 30}\n{idx}. {assertion_title}:\n{'-' * 30}\n{error_msg}"
|
673
|
-
|
714
|
+
|
674
715
|
# 添加底部分隔线
|
675
716
|
error_summary += f"\n{'-' * 50}"
|
676
|
-
|
717
|
+
|
677
718
|
raise AssertionError(error_summary)
|
678
|
-
|
719
|
+
|
679
720
|
# 返回断言结果和需要重试的断言
|
680
721
|
return assert_results, failed_retryable_assertions
|
681
|
-
|
722
|
+
|
682
723
|
def _format_assertion_details(self, assertion_result: Dict[str, Any]) -> str:
|
683
724
|
"""格式化断言详情,用于Allure报告
|
684
|
-
|
725
|
+
|
685
726
|
Args:
|
686
727
|
assertion_result: 断言结果字典
|
687
|
-
|
728
|
+
|
688
729
|
Returns:
|
689
730
|
格式化的断言详情字符串
|
690
731
|
"""
|
691
732
|
details = f"类型: {assertion_result['type']}\n"
|
692
733
|
if assertion_result['path']:
|
693
734
|
details += f"路径: {assertion_result['path']}\n"
|
694
|
-
|
735
|
+
|
695
736
|
if assertion_result['assertion_type'] == 'length':
|
696
737
|
details += f"原始值: {assertion_result['original_value']}\n"
|
697
738
|
details += f"长度: {assertion_result['actual_value']}\n"
|
698
739
|
else:
|
699
740
|
details += f"实际值: {assertion_result['actual_value']}\n"
|
700
|
-
|
741
|
+
|
701
742
|
details += f"操作符: {assertion_result['operator']}\n"
|
702
|
-
|
743
|
+
|
703
744
|
if assertion_result['expected_value'] is not None:
|
704
745
|
details += f"预期值: {assertion_result['expected_value']}\n"
|
705
|
-
|
746
|
+
|
706
747
|
details += f"结果: {'通过' if assertion_result['passed'] else '失败'}"
|
707
|
-
|
748
|
+
|
708
749
|
return details
|
709
|
-
|
750
|
+
|
710
751
|
def _extract_value(self, extractor_type: str, extraction_path: str = None, default_value: Any = None) -> Any:
|
711
752
|
"""从响应提取值
|
712
|
-
|
753
|
+
|
713
754
|
Args:
|
714
755
|
extractor_type: 提取器类型
|
715
756
|
extraction_path: 提取路径
|
716
757
|
default_value: 默认值
|
717
|
-
|
758
|
+
|
718
759
|
Returns:
|
719
760
|
提取的值
|
720
761
|
"""
|
721
762
|
if not self.response:
|
722
763
|
return default_value
|
723
|
-
|
764
|
+
|
724
765
|
try:
|
725
766
|
if extractor_type == "jsonpath":
|
726
767
|
return self._extract_jsonpath(extraction_path, default_value)
|
@@ -753,23 +794,23 @@ class HTTPRequest:
|
|
753
794
|
if default_value is not None:
|
754
795
|
return default_value
|
755
796
|
raise ValueError(error_message)
|
756
|
-
|
797
|
+
|
757
798
|
def _extract_jsonpath(self, path: str, default_value: Any = None) -> Any:
|
758
799
|
"""使用JSONPath从JSON响应提取值
|
759
|
-
|
800
|
+
|
760
801
|
Args:
|
761
802
|
path: JSONPath表达式
|
762
803
|
default_value: 默认值
|
763
|
-
|
804
|
+
|
764
805
|
Returns:
|
765
806
|
提取的值
|
766
807
|
"""
|
767
808
|
try:
|
768
809
|
json_data = self.response.json()
|
769
|
-
|
810
|
+
|
770
811
|
jsonpath_expr = jsonpath.parse(path)
|
771
812
|
matches = [match.value for match in jsonpath_expr.find(json_data)]
|
772
|
-
|
813
|
+
|
773
814
|
if not matches:
|
774
815
|
return default_value
|
775
816
|
elif len(matches) == 1:
|
@@ -780,14 +821,14 @@ class HTTPRequest:
|
|
780
821
|
if default_value is not None:
|
781
822
|
return default_value
|
782
823
|
raise ValueError(f"JSONPath提取失败: {str(e)}")
|
783
|
-
|
824
|
+
|
784
825
|
def _extract_xpath(self, path: str, default_value: Any = None) -> Any:
|
785
826
|
"""使用XPath从HTML/XML响应提取值
|
786
|
-
|
827
|
+
|
787
828
|
Args:
|
788
829
|
path: XPath表达式
|
789
830
|
default_value: 默认值
|
790
|
-
|
831
|
+
|
791
832
|
Returns:
|
792
833
|
提取的值
|
793
834
|
"""
|
@@ -795,10 +836,10 @@ class HTTPRequest:
|
|
795
836
|
# 尝试解析响应内容
|
796
837
|
parser = etree.HTMLParser()
|
797
838
|
tree = etree.fromstring(self.response.content, parser)
|
798
|
-
|
839
|
+
|
799
840
|
# 执行XPath
|
800
841
|
result = tree.xpath(path)
|
801
|
-
|
842
|
+
|
802
843
|
if not result:
|
803
844
|
return default_value
|
804
845
|
elif len(result) == 1:
|
@@ -809,14 +850,14 @@ class HTTPRequest:
|
|
809
850
|
if default_value is not None:
|
810
851
|
return default_value
|
811
852
|
raise ValueError(f"XPath提取失败: {str(e)}")
|
812
|
-
|
853
|
+
|
813
854
|
def _extract_regex(self, pattern: str, default_value: Any = None) -> Any:
|
814
855
|
"""使用正则表达式从响应提取值
|
815
|
-
|
856
|
+
|
816
857
|
Args:
|
817
858
|
pattern: 正则表达式模式
|
818
859
|
default_value: 默认值
|
819
|
-
|
860
|
+
|
820
861
|
Returns:
|
821
862
|
提取的值
|
822
863
|
"""
|
@@ -826,9 +867,9 @@ class HTTPRequest:
|
|
826
867
|
text = json.dumps(self.response.json())
|
827
868
|
else:
|
828
869
|
text = self.response.text
|
829
|
-
|
870
|
+
|
830
871
|
matches = re.findall(pattern, text)
|
831
|
-
|
872
|
+
|
832
873
|
if not matches:
|
833
874
|
return default_value
|
834
875
|
elif len(matches) == 1:
|
@@ -839,42 +880,42 @@ class HTTPRequest:
|
|
839
880
|
if default_value is not None:
|
840
881
|
return default_value
|
841
882
|
raise ValueError(f"正则表达式提取失败: {str(e)}")
|
842
|
-
|
883
|
+
|
843
884
|
def _extract_header(self, header_name: str, default_value: Any = None) -> Any:
|
844
885
|
"""从响应头提取值
|
845
|
-
|
886
|
+
|
846
887
|
Args:
|
847
888
|
header_name: 响应头名称
|
848
889
|
default_value: 默认值
|
849
|
-
|
890
|
+
|
850
891
|
Returns:
|
851
892
|
提取的值
|
852
893
|
"""
|
853
894
|
header_value = self.response.headers.get(header_name)
|
854
895
|
return header_value if header_value is not None else default_value
|
855
|
-
|
896
|
+
|
856
897
|
def _extract_cookie(self, cookie_name: str, default_value: Any = None) -> Any:
|
857
898
|
"""从响应Cookie提取值
|
858
|
-
|
899
|
+
|
859
900
|
Args:
|
860
901
|
cookie_name: Cookie名称
|
861
902
|
default_value: 默认值
|
862
|
-
|
903
|
+
|
863
904
|
Returns:
|
864
905
|
提取的值
|
865
906
|
"""
|
866
907
|
cookie = self.response.cookies.get(cookie_name)
|
867
908
|
return cookie if cookie is not None else default_value
|
868
|
-
|
909
|
+
|
869
910
|
def _perform_assertion(self, assertion_type: str, operator: str, actual_value: Any, expected_value: Any = None) -> bool:
|
870
911
|
"""执行断言
|
871
|
-
|
912
|
+
|
872
913
|
Args:
|
873
914
|
assertion_type: 断言类型
|
874
915
|
operator: 比较操作符
|
875
916
|
actual_value: 实际值
|
876
917
|
expected_value: 预期值
|
877
|
-
|
918
|
+
|
878
919
|
Returns:
|
879
920
|
断言结果
|
880
921
|
"""
|
@@ -886,7 +927,7 @@ class HTTPRequest:
|
|
886
927
|
if isinstance(expected_value, str):
|
887
928
|
# 去除空白字符和换行符
|
888
929
|
clean_expected = expected_value.strip()
|
889
|
-
|
930
|
+
|
890
931
|
# 判断是否是整数
|
891
932
|
if clean_expected.isdigit() or (clean_expected.startswith('-') and clean_expected[1:].isdigit()):
|
892
933
|
expected_value = int(clean_expected)
|
@@ -904,12 +945,12 @@ class HTTPRequest:
|
|
904
945
|
clean_expected[1:].replace('e', '').replace('E', '').replace('+', '', 1).replace('-', '', 1).replace('.', '', 1).isdigit())
|
905
946
|
):
|
906
947
|
expected_value = float(clean_expected)
|
907
|
-
|
948
|
+
|
908
949
|
# 将实际值转换为适当的类型
|
909
950
|
if isinstance(actual_value, str):
|
910
951
|
# 去除空白字符和换行符
|
911
952
|
clean_actual = actual_value.strip()
|
912
|
-
|
953
|
+
|
913
954
|
# 判断是否是整数
|
914
955
|
if clean_actual.isdigit() or (clean_actual.startswith('-') and clean_actual[1:].isdigit()):
|
915
956
|
actual_value = int(clean_actual)
|
@@ -934,7 +975,7 @@ class HTTPRequest:
|
|
934
975
|
name="断言类型转换警告",
|
935
976
|
attachment_type=allure.attachment_type.TEXT
|
936
977
|
)
|
937
|
-
|
978
|
+
|
938
979
|
# 记录断言参数
|
939
980
|
allure.attach(
|
940
981
|
f"断言类型: {assertion_type}\n"
|
@@ -944,7 +985,7 @@ class HTTPRequest:
|
|
944
985
|
name="断言参数",
|
945
986
|
attachment_type=allure.attachment_type.TEXT
|
946
987
|
)
|
947
|
-
|
988
|
+
|
948
989
|
# 基于断言类型执行断言
|
949
990
|
if assertion_type == "value":
|
950
991
|
# 直接使用操作符进行比较
|
@@ -1036,15 +1077,15 @@ class HTTPRequest:
|
|
1036
1077
|
return False
|
1037
1078
|
else:
|
1038
1079
|
raise ValueError(f"不支持的断言类型: {assertion_type}")
|
1039
|
-
|
1080
|
+
|
1040
1081
|
def _compare_values(self, actual_value: Any, expected_value: Any, operator: str) -> bool:
|
1041
1082
|
"""比较两个值
|
1042
|
-
|
1083
|
+
|
1043
1084
|
Args:
|
1044
1085
|
actual_value: 实际值
|
1045
1086
|
expected_value: 预期值
|
1046
1087
|
operator: 比较操作符
|
1047
|
-
|
1088
|
+
|
1048
1089
|
Returns:
|
1049
1090
|
比较结果
|
1050
1091
|
"""
|
@@ -1080,6 +1121,16 @@ class HTTPRequest:
|
|
1080
1121
|
elif isinstance(actual_value, (list, tuple, dict)):
|
1081
1122
|
return expected_value not in actual_value
|
1082
1123
|
return True
|
1124
|
+
elif operator == "startswith":
|
1125
|
+
# startswith操作符检查actual是否以expected开头
|
1126
|
+
if not isinstance(actual_value, str):
|
1127
|
+
actual_value = str(actual_value) if actual_value is not None else ""
|
1128
|
+
return actual_value.startswith(str(expected_value))
|
1129
|
+
elif operator == "endswith":
|
1130
|
+
# endswith操作符检查actual是否以expected结尾
|
1131
|
+
if not isinstance(actual_value, str):
|
1132
|
+
actual_value = str(actual_value) if actual_value is not None else ""
|
1133
|
+
return actual_value.endswith(str(expected_value))
|
1083
1134
|
elif operator == "matches":
|
1084
1135
|
# matches操作符使用正则表达式进行匹配
|
1085
1136
|
if not isinstance(actual_value, str):
|
@@ -1109,10 +1160,10 @@ class HTTPRequest:
|
|
1109
1160
|
return False
|
1110
1161
|
else:
|
1111
1162
|
raise ValueError(f"不支持的比较操作符: {operator}")
|
1112
|
-
|
1163
|
+
|
1113
1164
|
def _log_request_to_allure(self, method: str, url: str, request_kwargs: Dict[str, Any]) -> None:
|
1114
1165
|
"""使用Allure记录请求信息
|
1115
|
-
|
1166
|
+
|
1116
1167
|
Args:
|
1117
1168
|
method: HTTP方法
|
1118
1169
|
url: 请求URL
|
@@ -1120,10 +1171,10 @@ class HTTPRequest:
|
|
1120
1171
|
"""
|
1121
1172
|
# 创建请求信息摘要
|
1122
1173
|
request_summary = f"{method} {url}"
|
1123
|
-
|
1174
|
+
|
1124
1175
|
# 创建详细请求信息
|
1125
1176
|
request_details = [f"Method: {method}", f"URL: {url}"]
|
1126
|
-
|
1177
|
+
|
1127
1178
|
# 添加请求头
|
1128
1179
|
if "headers" in request_kwargs and request_kwargs["headers"]:
|
1129
1180
|
# 隐藏敏感信息
|
@@ -1136,13 +1187,13 @@ class HTTPRequest:
|
|
1136
1187
|
request_details.append("Headers:")
|
1137
1188
|
for key, value in safe_headers.items():
|
1138
1189
|
request_details.append(f" {key}: {value}")
|
1139
|
-
|
1190
|
+
|
1140
1191
|
# 添加查询参数
|
1141
1192
|
if "params" in request_kwargs and request_kwargs["params"]:
|
1142
1193
|
request_details.append("Query Parameters:")
|
1143
1194
|
for key, value in request_kwargs["params"].items():
|
1144
1195
|
request_details.append(f" {key}: {value}")
|
1145
|
-
|
1196
|
+
|
1146
1197
|
# 添加请求体
|
1147
1198
|
if "json" in request_kwargs and request_kwargs["json"]:
|
1148
1199
|
request_details.append("JSON Body:")
|
@@ -1154,40 +1205,40 @@ class HTTPRequest:
|
|
1154
1205
|
request_details.append("Form Data:")
|
1155
1206
|
for key, value in request_kwargs["data"].items():
|
1156
1207
|
request_details.append(f" {key}: {value}")
|
1157
|
-
|
1208
|
+
|
1158
1209
|
# 添加文件信息
|
1159
1210
|
if "files" in request_kwargs and request_kwargs["files"]:
|
1160
1211
|
request_details.append("Files:")
|
1161
1212
|
for key, value in request_kwargs["files"].items():
|
1162
1213
|
request_details.append(f" {key}: <File object>")
|
1163
|
-
|
1214
|
+
|
1164
1215
|
# 记录到Allure
|
1165
1216
|
allure.attach(
|
1166
1217
|
"\n".join(request_details),
|
1167
1218
|
name=f"HTTP请求: {request_summary}",
|
1168
1219
|
attachment_type=allure.attachment_type.TEXT
|
1169
1220
|
)
|
1170
|
-
|
1221
|
+
|
1171
1222
|
def _log_response_to_allure(self, response: Response) -> None:
|
1172
1223
|
"""使用Allure记录响应信息
|
1173
|
-
|
1224
|
+
|
1174
1225
|
Args:
|
1175
1226
|
response: 响应对象
|
1176
1227
|
"""
|
1177
1228
|
# 创建响应信息摘要
|
1178
1229
|
response_summary = f"{response.status_code} {response.reason} ({response.elapsed.total_seconds() * 1000:.2f}ms)"
|
1179
|
-
|
1230
|
+
|
1180
1231
|
# 创建详细响应信息
|
1181
1232
|
response_details = [
|
1182
1233
|
f"Status: {response.status_code} {response.reason}",
|
1183
1234
|
f"Response Time: {response.elapsed.total_seconds() * 1000:.2f}ms"
|
1184
1235
|
]
|
1185
|
-
|
1236
|
+
|
1186
1237
|
# 添加响应头
|
1187
1238
|
response_details.append("Headers:")
|
1188
1239
|
for key, value in response.headers.items():
|
1189
1240
|
response_details.append(f" {key}: {value}")
|
1190
|
-
|
1241
|
+
|
1191
1242
|
# 添加响应体
|
1192
1243
|
response_details.append("Body:")
|
1193
1244
|
try:
|
@@ -1199,10 +1250,10 @@ class HTTPRequest:
|
|
1199
1250
|
response_details.append(f"<{len(response.content)} bytes>")
|
1200
1251
|
except Exception as e:
|
1201
1252
|
response_details.append(f"<Error parsing body: {str(e)}>")
|
1202
|
-
|
1253
|
+
|
1203
1254
|
# 记录到Allure
|
1204
1255
|
allure.attach(
|
1205
1256
|
"\n".join(response_details),
|
1206
1257
|
name=f"HTTP响应: {response_summary}",
|
1207
1258
|
attachment_type=allure.attachment_type.TEXT
|
1208
|
-
)
|
1259
|
+
)
|