pytest-dsl 0.1.0__py3-none-any.whl → 0.1.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/core/auth_provider.py +50 -10
- pytest_dsl/core/http_client.py +11 -6
- pytest_dsl/core/http_request.py +399 -110
- pytest_dsl/examples/csrf_auth_provider.py +232 -0
- 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_quickstart.py +14 -0
- pytest_dsl-0.1.1.dist-info/METADATA +504 -0
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/RECORD +14 -9
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/WHEEL +1 -1
- pytest_dsl-0.1.0.dist-info/METADATA +0 -537
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/top_level.txt +0 -0
pytest_dsl/core/http_request.py
CHANGED
@@ -2,7 +2,7 @@ import json
|
|
2
2
|
import re
|
3
3
|
import yaml
|
4
4
|
import jsonpath_ng.ext as jsonpath
|
5
|
-
from typing import Dict, List, Any, Union, Optional, Tuple
|
5
|
+
from typing import Dict, List, Any, Union, Optional, Tuple, Set
|
6
6
|
import lxml.etree as etree
|
7
7
|
from requests import Response
|
8
8
|
import allure
|
@@ -11,6 +11,23 @@ import requests
|
|
11
11
|
from pytest_dsl.core.http_client import http_client_manager
|
12
12
|
|
13
13
|
|
14
|
+
# 定义支持的比较操作符
|
15
|
+
COMPARISON_OPERATORS = {
|
16
|
+
"eq", "neq", "lt", "lte", "gt", "gte", "in", "not_in"
|
17
|
+
}
|
18
|
+
|
19
|
+
# 定义支持的断言类型
|
20
|
+
ASSERTION_TYPES = {
|
21
|
+
"exists", "not_exists", "contains", "not_contains", "startswith",
|
22
|
+
"endswith", "matches", "type", "length", "schema", "value"
|
23
|
+
}
|
24
|
+
|
25
|
+
# 操作符和断言类型重叠的集合(可以作为二者使用的)
|
26
|
+
DUAL_PURPOSE = {
|
27
|
+
"contains", "not_contains", "matches", "in", "not_in"
|
28
|
+
}
|
29
|
+
|
30
|
+
|
14
31
|
class HTTPRequest:
|
15
32
|
"""HTTP请求处理类
|
16
33
|
|
@@ -122,14 +139,17 @@ class HTTPRequest:
|
|
122
139
|
# 重新抛出异常
|
123
140
|
raise ValueError(f"HTTP请求执行错误: {str(e)}") from e
|
124
141
|
|
125
|
-
def
|
126
|
-
"""
|
142
|
+
def _ensure_response_exists(self, operation: str = "处理"):
|
143
|
+
"""确保响应对象存在
|
127
144
|
|
128
|
-
|
129
|
-
|
145
|
+
Args:
|
146
|
+
operation: 操作名称,用于错误消息
|
147
|
+
|
148
|
+
Raises:
|
149
|
+
ValueError: 如果响应对象不存在
|
130
150
|
"""
|
131
151
|
if not self.response:
|
132
|
-
error_message = "
|
152
|
+
error_message = f"需要先执行请求才能{operation}"
|
133
153
|
# 记录更详细的错误信息到Allure
|
134
154
|
debug_info = (
|
135
155
|
f"错误详情: self.response 为 None\n"
|
@@ -138,11 +158,19 @@ class HTTPRequest:
|
|
138
158
|
)
|
139
159
|
allure.attach(
|
140
160
|
debug_info,
|
141
|
-
name="
|
161
|
+
name=f"{operation}失败详情",
|
142
162
|
attachment_type=allure.attachment_type.TEXT
|
143
163
|
)
|
144
164
|
raise ValueError(error_message)
|
145
165
|
|
166
|
+
def process_captures(self) -> Dict[str, Any]:
|
167
|
+
"""处理响应捕获
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
捕获的值字典
|
171
|
+
"""
|
172
|
+
self._ensure_response_exists("捕获响应")
|
173
|
+
|
146
174
|
captures_config = self.config.get('captures', {})
|
147
175
|
|
148
176
|
for var_name, capture_spec in captures_config.items():
|
@@ -178,38 +206,19 @@ class HTTPRequest:
|
|
178
206
|
attachment_type=allure.attachment_type.TEXT
|
179
207
|
)
|
180
208
|
except Exception as e:
|
181
|
-
#
|
182
|
-
error_msg =
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
error_msg += f"\nURL: {self.config.get('url', '')}"
|
188
|
-
if 'headers' in self.config.get('request', {}):
|
189
|
-
error_msg += "\nHeaders: " + str(self.config.get('request', {}).get('headers', {}))
|
190
|
-
if 'params' in self.config.get('request', {}):
|
191
|
-
error_msg += "\nParams: " + str(self.config.get('request', {}).get('params', {}))
|
192
|
-
if 'json' in self.config.get('request', {}):
|
193
|
-
error_msg += "\nJSON Body: " + str(self.config.get('request', {}).get('json', {}))
|
194
|
-
|
195
|
-
# 添加响应信息
|
196
|
-
error_msg += "\n\n=== 响应信息 ==="
|
197
|
-
error_msg += f"\nStatus: {self.response.status_code} {self.response.reason}"
|
198
|
-
error_msg += f"\nHeaders: {dict(self.response.headers)}"
|
199
|
-
try:
|
200
|
-
if 'application/json' in self.response.headers.get('Content-Type', ''):
|
201
|
-
error_msg += f"\nBody: {json.dumps(self.response.json(), ensure_ascii=False)}"
|
202
|
-
else:
|
203
|
-
error_msg += f"\nBody: {self.response.text}"
|
204
|
-
except:
|
205
|
-
error_msg += "\nBody: <无法解析响应体>"
|
206
|
-
|
209
|
+
# 记录长度计算失败的详细信息
|
210
|
+
error_msg = (
|
211
|
+
f"无法计算长度: {type(e).__name__}: {str(e)}\n"
|
212
|
+
f"类型: {type(original_value).__name__}\n"
|
213
|
+
f"值: {original_value}"
|
214
|
+
)
|
207
215
|
allure.attach(
|
208
216
|
error_msg,
|
209
|
-
name=f"
|
217
|
+
name=f"长度计算失败: {extractor_type} {extraction_path}",
|
210
218
|
attachment_type=allure.attachment_type.TEXT
|
211
219
|
)
|
212
|
-
|
220
|
+
# 抛出更具体的异常,而不是用0替代
|
221
|
+
raise ValueError(f"断言类型'length'无法应用于值 '{original_value}': {str(e)}")
|
213
222
|
else:
|
214
223
|
# 记录捕获到Allure
|
215
224
|
allure.attach(
|
@@ -235,21 +244,90 @@ class HTTPRequest:
|
|
235
244
|
|
236
245
|
return self.captured_values
|
237
246
|
|
238
|
-
def
|
247
|
+
def _format_error_details(self,
|
248
|
+
extractor_type: str,
|
249
|
+
extraction_path: str,
|
250
|
+
assertion_type: str,
|
251
|
+
compare_operator: str,
|
252
|
+
actual_value: Any,
|
253
|
+
expected_value: Any,
|
254
|
+
original_actual_value: Any = None,
|
255
|
+
error_message: str = None,
|
256
|
+
additional_context: str = None) -> str:
|
257
|
+
"""格式化断言错误的详细信息
|
258
|
+
|
259
|
+
Args:
|
260
|
+
extractor_type: 提取器类型
|
261
|
+
extraction_path: 提取路径
|
262
|
+
assertion_type: 断言类型
|
263
|
+
compare_operator: 比较操作符
|
264
|
+
actual_value: 实际值
|
265
|
+
expected_value: 预期值
|
266
|
+
original_actual_value: 原始值(用于length断言)
|
267
|
+
error_message: 错误消息
|
268
|
+
additional_context: 附加上下文信息
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
格式化的错误详情字符串
|
272
|
+
"""
|
273
|
+
error_details = []
|
274
|
+
|
275
|
+
# 添加基本错误信息
|
276
|
+
prefix = "断言执行错误" if additional_context else "断言失败"
|
277
|
+
error_details.append(f"{prefix} [{extractor_type}]")
|
278
|
+
|
279
|
+
# 添加异常类型信息(如果存在)
|
280
|
+
if additional_context:
|
281
|
+
error_details.append(f"异常类型: {additional_context}")
|
282
|
+
|
283
|
+
# 添加路径信息
|
284
|
+
if extraction_path:
|
285
|
+
error_details.append(f"路径: {extraction_path}")
|
286
|
+
|
287
|
+
# 添加断言类型和值信息
|
288
|
+
if assertion_type == "length":
|
289
|
+
error_details.append(f"断言类型: 长度比较")
|
290
|
+
if original_actual_value is not None:
|
291
|
+
error_details.append(f"原始值: {original_actual_value}")
|
292
|
+
error_details.append(f"实际长度: {actual_value}")
|
293
|
+
else:
|
294
|
+
error_details.append(f"断言类型: {assertion_type}")
|
295
|
+
error_details.append(f"实际值: {actual_value}")
|
296
|
+
|
297
|
+
# 添加比较操作符
|
298
|
+
if compare_operator:
|
299
|
+
error_details.append(f"比较操作符: {compare_operator}")
|
300
|
+
|
301
|
+
# 添加预期值
|
302
|
+
if expected_value is not None:
|
303
|
+
error_details.append(f"预期值: {expected_value}")
|
304
|
+
|
305
|
+
# 添加类型信息
|
306
|
+
error_details.append(f"实际类型: {type(actual_value).__name__}")
|
307
|
+
if expected_value is not None:
|
308
|
+
error_details.append(f"预期类型: {type(expected_value).__name__}")
|
309
|
+
|
310
|
+
# 添加错误消息
|
311
|
+
if error_message:
|
312
|
+
error_details.append(f"错误信息: {error_message}")
|
313
|
+
|
314
|
+
return "\n".join(error_details)
|
315
|
+
|
316
|
+
def process_asserts(self, specific_asserts=None) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
239
317
|
"""处理响应断言
|
240
318
|
|
241
319
|
Args:
|
242
320
|
specific_asserts: 指定要处理的断言列表,如果为None则处理所有断言
|
243
321
|
|
244
322
|
Returns:
|
245
|
-
|
323
|
+
Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: 断言结果列表和失败需重试的断言列表
|
246
324
|
"""
|
247
|
-
|
248
|
-
|
249
|
-
|
325
|
+
self._ensure_response_exists("进行断言")
|
326
|
+
|
250
327
|
asserts_config = self.config.get('asserts', [])
|
251
328
|
assert_results = []
|
252
329
|
failed_retryable_assertions = []
|
330
|
+
failed_assertions = [] # 收集所有失败的断言
|
253
331
|
|
254
332
|
# 处理断言重试配置
|
255
333
|
# 1. 只使用独立的retry_assertions配置
|
@@ -303,37 +381,103 @@ class HTTPRequest:
|
|
303
381
|
is_retryable = True
|
304
382
|
|
305
383
|
# 处理断言参数
|
306
|
-
if len(assertion) == 2: #
|
384
|
+
if len(assertion) == 2: # 简单存在性断言 ["header", "Location"]
|
307
385
|
extraction_path = assertion[1]
|
308
386
|
assertion_type = "exists"
|
309
387
|
expected_value = None
|
310
388
|
compare_operator = "eq" # 默认比较操作符
|
311
389
|
elif len(assertion) == 3: # 简单断言 ["status", "eq", 200]
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
390
|
+
# 检查第二个元素是否是操作符或二者兼用的断言类型
|
391
|
+
if assertion[1] in COMPARISON_OPERATORS or assertion[1] in DUAL_PURPOSE:
|
392
|
+
# 这是操作符格式的断言 ["status", "eq", 200]
|
393
|
+
extraction_path = None if extractor_type in ["status", "body", "response_time"] else assertion[1]
|
394
|
+
|
395
|
+
# 特殊处理二者兼用的断言类型
|
396
|
+
if assertion[1] in DUAL_PURPOSE:
|
397
|
+
if extractor_type in ["jsonpath", "body", "header"]:
|
398
|
+
# 当提取器是jsonpath/body/header时,将contains当作断言类型处理
|
399
|
+
assertion_type = assertion[1]
|
400
|
+
compare_operator = "eq" # 默认操作符
|
401
|
+
else:
|
402
|
+
# 否则当作操作符处理
|
403
|
+
assertion_type = "value"
|
404
|
+
compare_operator = assertion[1]
|
405
|
+
else:
|
406
|
+
assertion_type = "value" # 标记为值比较
|
407
|
+
compare_operator = assertion[1] # 比较操作符
|
408
|
+
|
316
409
|
expected_value = assertion[2] # 预期值
|
317
410
|
else:
|
411
|
+
# 这是断言类型格式 ["jsonpath", "$.id", "exists"]
|
318
412
|
extraction_path = assertion[1]
|
319
413
|
assertion_type = assertion[2]
|
320
|
-
|
321
|
-
|
322
|
-
|
414
|
+
|
415
|
+
# 检查断言类型是否有效
|
416
|
+
if assertion_type not in ASSERTION_TYPES:
|
417
|
+
raise ValueError(f"不支持的断言类型: {assertion_type} 在 {assertion}")
|
418
|
+
|
419
|
+
# 检查此断言类型是否需要预期值
|
420
|
+
if assertion_type not in ["exists", "not_exists"]:
|
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 = "eq" # 默认比较操作符
|
427
|
+
else:
|
428
|
+
expected_value = None
|
429
|
+
compare_operator = "eq" # 默认比较操作符
|
430
|
+
elif len(assertion) == 4: # 带操作符的断言 ["jsonpath", "$.id", "eq", 1] 或特殊断言 ["jsonpath", "$.type", "type", "string"]
|
323
431
|
extraction_path = assertion[1]
|
324
|
-
|
325
|
-
|
432
|
+
|
433
|
+
# 检查第三个元素是否是操作符
|
434
|
+
if assertion[2] in COMPARISON_OPERATORS or assertion[2] in DUAL_PURPOSE:
|
435
|
+
# 这是操作符形式的断言
|
326
436
|
assertion_type = "value" # 标记为值比较
|
327
437
|
compare_operator = assertion[2] # 比较操作符
|
328
438
|
expected_value = assertion[3] # 预期值
|
329
439
|
else:
|
440
|
+
# 检查断言类型是否有效
|
441
|
+
if assertion[2] not in ASSERTION_TYPES:
|
442
|
+
raise ValueError(f"不支持的断言类型: {assertion[2]} 在 {assertion}")
|
443
|
+
|
330
444
|
# 其他类型的断言,比如特殊断言
|
331
445
|
assertion_type = assertion[2]
|
332
|
-
|
333
|
-
|
446
|
+
|
447
|
+
# 根据断言类型决定如何处理操作符和期望值
|
448
|
+
if assertion_type == "length":
|
449
|
+
# 对于长度断言,总是需要有比较操作符和期望值
|
450
|
+
if len(assertion) < 4:
|
451
|
+
raise ValueError(f"长度断言需要提供预期值: {assertion}")
|
452
|
+
compare_operator = "eq" # 在这种4元素格式中,使用默认比较操作符eq
|
453
|
+
expected_value = assertion[3] # 预期长度值
|
454
|
+
else:
|
455
|
+
# 对于其他特殊断言,使用第四个元素作为期望值
|
456
|
+
compare_operator = "eq" # 默认比较操作符
|
457
|
+
expected_value = assertion[3]
|
334
458
|
else: # 5个参数,例如 ["jsonpath", "$", "length", "eq", 10]
|
335
459
|
extraction_path = assertion[1]
|
336
460
|
assertion_type = assertion[2]
|
461
|
+
|
462
|
+
# 检查断言类型是否有效
|
463
|
+
if assertion_type not in ASSERTION_TYPES:
|
464
|
+
raise ValueError(f"不支持的断言类型: {assertion_type} 在 {assertion}")
|
465
|
+
|
466
|
+
# 特殊处理长度断言
|
467
|
+
if assertion_type == "length":
|
468
|
+
# 验证第四个元素是有效的比较操作符
|
469
|
+
if assertion[3] not in COMPARISON_OPERATORS:
|
470
|
+
raise ValueError(f"长度断言的比较操作符必须是 {', '.join(COMPARISON_OPERATORS)} 之一: {assertion}")
|
471
|
+
|
472
|
+
# 验证第五个元素是有效的长度值
|
473
|
+
try:
|
474
|
+
# 尝试将预期长度转换为整数
|
475
|
+
expected_length = int(assertion[4])
|
476
|
+
if expected_length < 0:
|
477
|
+
raise ValueError(f"长度断言的预期值必须是非负整数: {assertion}")
|
478
|
+
except (ValueError, TypeError):
|
479
|
+
raise ValueError(f"长度断言的预期值必须是有效的整数: {assertion}")
|
480
|
+
|
337
481
|
compare_operator = assertion[3]
|
338
482
|
expected_value = assertion[4]
|
339
483
|
|
@@ -344,10 +488,35 @@ class HTTPRequest:
|
|
344
488
|
original_actual_value = actual_value
|
345
489
|
if assertion_type == "length" and extractor_type != "response_time" and extractor_type != "status" and extractor_type != "body":
|
346
490
|
try:
|
491
|
+
# 记录长度断言的原始值信息
|
492
|
+
allure.attach(
|
493
|
+
f"提取器: {extractor_type}\n路径: {extraction_path}\n原始值: {original_actual_value}\n类型: {type(original_actual_value).__name__}",
|
494
|
+
name=f"长度断言原始值: {extractor_type}",
|
495
|
+
attachment_type=allure.attachment_type.TEXT
|
496
|
+
)
|
497
|
+
|
347
498
|
actual_value = len(actual_value)
|
499
|
+
|
500
|
+
# 记录计算结果
|
501
|
+
allure.attach(
|
502
|
+
f"提取器: {extractor_type}\n路径: {extraction_path}\n长度: {actual_value}",
|
503
|
+
name=f"长度断言计算结果: {extractor_type}",
|
504
|
+
attachment_type=allure.attachment_type.TEXT
|
505
|
+
)
|
348
506
|
except Exception as e:
|
349
|
-
#
|
350
|
-
|
507
|
+
# 记录长度计算失败的详细信息
|
508
|
+
error_msg = (
|
509
|
+
f"无法计算长度: {type(e).__name__}: {str(e)}\n"
|
510
|
+
f"类型: {type(original_actual_value).__name__}\n"
|
511
|
+
f"值: {original_actual_value}"
|
512
|
+
)
|
513
|
+
allure.attach(
|
514
|
+
error_msg,
|
515
|
+
name=f"长度计算失败: {extractor_type} {extraction_path}",
|
516
|
+
attachment_type=allure.attachment_type.TEXT
|
517
|
+
)
|
518
|
+
# 抛出更具体的异常,而不是用0替代
|
519
|
+
raise ValueError(f"断言类型'length'无法应用于值 '{original_actual_value}': {str(e)}")
|
351
520
|
|
352
521
|
# 执行断言
|
353
522
|
assertion_result = {
|
@@ -368,19 +537,69 @@ class HTTPRequest:
|
|
368
537
|
# 验证断言
|
369
538
|
result = self._perform_assertion(assertion_type, compare_operator, actual_value, expected_value)
|
370
539
|
assertion_result['result'] = result
|
371
|
-
assertion_result['passed'] = True
|
372
540
|
|
373
|
-
#
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
541
|
+
# 根据返回的布尔值确定断言是否通过
|
542
|
+
if result:
|
543
|
+
assertion_result['passed'] = True
|
544
|
+
|
545
|
+
# 使用Allure记录断言成功
|
546
|
+
allure.attach(
|
547
|
+
self._format_assertion_details(assertion_result),
|
548
|
+
name=f"断言成功: {extractor_type}",
|
549
|
+
attachment_type=allure.attachment_type.TEXT
|
550
|
+
)
|
551
|
+
else:
|
552
|
+
# 断言失败但没有抛出异常
|
553
|
+
assertion_result['passed'] = False
|
554
|
+
assertion_result['error'] = "断言失败"
|
555
|
+
|
556
|
+
# 使用Allure记录断言失败
|
557
|
+
allure.attach(
|
558
|
+
self._format_assertion_details(assertion_result) + "\n\n错误: 断言结果为False",
|
559
|
+
name=f"断言失败: {extractor_type}",
|
560
|
+
attachment_type=allure.attachment_type.TEXT
|
561
|
+
)
|
562
|
+
|
563
|
+
# 如果断言可重试,添加到失败且需要重试的断言列表
|
564
|
+
if is_retryable:
|
565
|
+
failed_retryable_assertions.append(assertion_result)
|
566
|
+
|
567
|
+
# 构建格式化的错误消息
|
568
|
+
formatted_error = self._format_error_details(
|
569
|
+
extractor_type=extractor_type,
|
570
|
+
extraction_path=extraction_path,
|
571
|
+
assertion_type=assertion_type,
|
572
|
+
compare_operator=compare_operator,
|
573
|
+
actual_value=actual_value,
|
574
|
+
expected_value=expected_value,
|
575
|
+
original_actual_value=original_actual_value
|
576
|
+
)
|
577
|
+
|
578
|
+
# 收集断言失败,而不是立即抛出异常
|
579
|
+
failed_assertions.append((assertion_idx, formatted_error))
|
580
|
+
|
379
581
|
except AssertionError as e:
|
582
|
+
# 处理上面可能抛出的断言错误,或者其他断言错误
|
380
583
|
assertion_result['result'] = False
|
381
584
|
assertion_result['passed'] = False
|
382
585
|
assertion_result['error'] = str(e)
|
383
586
|
|
587
|
+
# 如果异常已经是格式化的多行消息,直接使用
|
588
|
+
if "\n" in str(e):
|
589
|
+
formatted_error = str(e)
|
590
|
+
else:
|
591
|
+
# 使用辅助方法构建详细的错误消息
|
592
|
+
formatted_error = self._format_error_details(
|
593
|
+
extractor_type=extractor_type,
|
594
|
+
extraction_path=extraction_path,
|
595
|
+
assertion_type=assertion_type,
|
596
|
+
compare_operator=compare_operator,
|
597
|
+
actual_value=actual_value,
|
598
|
+
expected_value=expected_value,
|
599
|
+
original_actual_value=original_actual_value,
|
600
|
+
error_message=str(e)
|
601
|
+
)
|
602
|
+
|
384
603
|
# 使用Allure记录断言失败
|
385
604
|
allure.attach(
|
386
605
|
self._format_assertion_details(assertion_result) + f"\n\n错误: {str(e)}",
|
@@ -392,11 +611,52 @@ class HTTPRequest:
|
|
392
611
|
if is_retryable:
|
393
612
|
failed_retryable_assertions.append(assertion_result)
|
394
613
|
|
395
|
-
#
|
396
|
-
|
614
|
+
# 收集断言失败,而不是立即抛出异常
|
615
|
+
failed_assertions.append((assertion_idx, formatted_error))
|
616
|
+
|
617
|
+
except Exception as e:
|
618
|
+
# 处理其他类型的异常
|
619
|
+
assertion_result['result'] = False
|
620
|
+
assertion_result['passed'] = False
|
621
|
+
assertion_result['error'] = f"未预期的异常: {type(e).__name__}: {str(e)}"
|
622
|
+
|
623
|
+
# 使用辅助方法构建详细的错误消息
|
624
|
+
formatted_error = self._format_error_details(
|
625
|
+
extractor_type=extractor_type,
|
626
|
+
extraction_path=extraction_path,
|
627
|
+
assertion_type=assertion_type,
|
628
|
+
compare_operator=compare_operator,
|
629
|
+
actual_value=actual_value,
|
630
|
+
expected_value=expected_value,
|
631
|
+
original_actual_value=original_actual_value,
|
632
|
+
error_message=str(e),
|
633
|
+
additional_context=type(e).__name__
|
634
|
+
)
|
635
|
+
|
636
|
+
# 使用Allure记录断言错误
|
637
|
+
allure.attach(
|
638
|
+
self._format_assertion_details(assertion_result) + f"\n\n错误: {assertion_result['error']}",
|
639
|
+
name=f"断言执行错误: {extractor_type}",
|
640
|
+
attachment_type=allure.attachment_type.TEXT
|
641
|
+
)
|
642
|
+
|
643
|
+
# 收集断言失败,而不是立即抛出异常
|
644
|
+
failed_assertions.append((assertion_idx, formatted_error))
|
397
645
|
|
398
646
|
assert_results.append(assertion_result)
|
399
647
|
|
648
|
+
# 执行完所有断言后,如果有失败的断言,抛出异常
|
649
|
+
if failed_assertions:
|
650
|
+
if len(failed_assertions) == 1:
|
651
|
+
# 只有一个断言失败时,直接使用该断言的错误消息
|
652
|
+
raise AssertionError(failed_assertions[0][1])
|
653
|
+
else:
|
654
|
+
# 多个断言失败时,汇总所有错误
|
655
|
+
error_summary = f"多个断言失败 ({len(failed_assertions)}/{len(process_asserts)}):\n"
|
656
|
+
for idx, (assertion_idx, error_msg) in enumerate(failed_assertions, 1):
|
657
|
+
error_summary += f"\n{idx}. 断言 #{assertion_idx+1}: {error_msg.split('断言失败')[0].strip()}"
|
658
|
+
raise AssertionError(error_summary)
|
659
|
+
|
400
660
|
# 返回断言结果和需要重试的断言
|
401
661
|
return assert_results, failed_retryable_assertions
|
402
662
|
|
@@ -590,28 +850,70 @@ class HTTPRequest:
|
|
590
850
|
Returns:
|
591
851
|
断言结果
|
592
852
|
"""
|
593
|
-
#
|
853
|
+
# 类型转换增强
|
594
854
|
if operator in ["eq", "neq", "lt", "lte", "gt", "gte"] and expected_value is not None:
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
if
|
599
|
-
|
600
|
-
|
601
|
-
|
855
|
+
# 尝试转换数字字符串为数字
|
856
|
+
try:
|
857
|
+
# 将预期值转换为适当的类型
|
858
|
+
if isinstance(expected_value, str):
|
859
|
+
# 去除空白字符和换行符
|
860
|
+
clean_expected = expected_value.strip()
|
861
|
+
|
862
|
+
# 判断是否是整数
|
863
|
+
if clean_expected.isdigit() or (clean_expected.startswith('-') and clean_expected[1:].isdigit()):
|
864
|
+
expected_value = int(clean_expected)
|
865
|
+
# 判断是否是浮点数(包括科学记数法)
|
866
|
+
elif (
|
867
|
+
# 标准小数
|
868
|
+
(clean_expected.replace('.', '', 1).isdigit()) or
|
869
|
+
# 负小数
|
870
|
+
(clean_expected.startswith('-') and clean_expected[1:].replace('.', '', 1).isdigit()) or
|
871
|
+
# 科学记数法 - 正
|
872
|
+
(('e' in clean_expected or 'E' in clean_expected) and
|
873
|
+
clean_expected.replace('e', '').replace('E', '').replace('+', '', 1).replace('-', '', 1).replace('.', '', 1).isdigit()) or
|
874
|
+
# 科学记数法 - 负
|
875
|
+
(clean_expected.startswith('-') and ('e' in clean_expected or 'E' in clean_expected) and
|
876
|
+
clean_expected[1:].replace('e', '').replace('E', '').replace('+', '', 1).replace('-', '', 1).replace('.', '', 1).isdigit())
|
877
|
+
):
|
878
|
+
expected_value = float(clean_expected)
|
602
879
|
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
880
|
+
# 将实际值转换为适当的类型
|
881
|
+
if isinstance(actual_value, str):
|
882
|
+
# 去除空白字符和换行符
|
883
|
+
clean_actual = actual_value.strip()
|
884
|
+
|
885
|
+
# 判断是否是整数
|
886
|
+
if clean_actual.isdigit() or (clean_actual.startswith('-') and clean_actual[1:].isdigit()):
|
887
|
+
actual_value = int(clean_actual)
|
888
|
+
# 判断是否是浮点数(包括科学记数法)
|
889
|
+
elif (
|
890
|
+
# 标准小数
|
891
|
+
(clean_actual.replace('.', '', 1).isdigit()) or
|
892
|
+
# 负小数
|
893
|
+
(clean_actual.startswith('-') and clean_actual[1:].replace('.', '', 1).isdigit()) or
|
894
|
+
# 科学记数法 - 正
|
895
|
+
(('e' in clean_actual or 'E' in clean_actual) and
|
896
|
+
clean_actual.replace('e', '').replace('E', '').replace('+', '', 1).replace('-', '', 1).replace('.', '', 1).isdigit()) or
|
897
|
+
# 科学记数法 - 负
|
898
|
+
(clean_actual.startswith('-') and ('e' in clean_actual or 'E' in clean_actual) and
|
899
|
+
clean_actual[1:].replace('e', '').replace('E', '').replace('+', '', 1).replace('-', '', 1).replace('.', '', 1).isdigit())
|
900
|
+
):
|
901
|
+
actual_value = float(clean_actual)
|
902
|
+
except (ValueError, TypeError) as e:
|
903
|
+
# 转换失败时记录日志但不抛出异常,保持原值进行比较
|
904
|
+
allure.attach(
|
905
|
+
f"类型转换失败: {str(e)}\n实际值: {actual_value} ({type(actual_value).__name__})\n预期值: {expected_value} ({type(expected_value).__name__})",
|
906
|
+
name="断言类型转换警告",
|
907
|
+
attachment_type=allure.attachment_type.TEXT
|
908
|
+
)
|
610
909
|
|
611
910
|
# 基于断言类型执行断言
|
612
|
-
if assertion_type == "value"
|
911
|
+
if assertion_type == "value":
|
613
912
|
# 直接使用操作符进行比较
|
614
913
|
return self._compare_values(actual_value, expected_value, operator)
|
914
|
+
elif assertion_type == "length":
|
915
|
+
# 长度比较
|
916
|
+
return self._compare_values(actual_value, expected_value, operator)
|
615
917
|
elif assertion_type == "exists":
|
616
918
|
return actual_value is not None
|
617
919
|
elif assertion_type == "not_exists":
|
@@ -631,27 +933,32 @@ class HTTPRequest:
|
|
631
933
|
return actual_value is None
|
632
934
|
return False
|
633
935
|
elif assertion_type == "contains":
|
634
|
-
|
635
|
-
|
936
|
+
# contains断言总是检查actual是否包含expected
|
937
|
+
if isinstance(actual_value, str):
|
938
|
+
return str(expected_value) in actual_value
|
636
939
|
elif isinstance(actual_value, (list, tuple, dict)):
|
637
940
|
return expected_value in actual_value
|
638
941
|
return False
|
942
|
+
elif assertion_type == "not_contains":
|
943
|
+
# not_contains断言总是检查actual是否不包含expected
|
944
|
+
if isinstance(actual_value, str):
|
945
|
+
return str(expected_value) not in actual_value
|
946
|
+
elif isinstance(actual_value, (list, tuple, dict)):
|
947
|
+
return expected_value not in actual_value
|
948
|
+
return True
|
639
949
|
elif assertion_type == "startswith":
|
640
|
-
return isinstance(actual_value, str) and actual_value.startswith(expected_value)
|
950
|
+
return isinstance(actual_value, str) and actual_value.startswith(str(expected_value))
|
641
951
|
elif assertion_type == "endswith":
|
642
|
-
return isinstance(actual_value, str) and actual_value.endswith(expected_value)
|
952
|
+
return isinstance(actual_value, str) and actual_value.endswith(str(expected_value))
|
643
953
|
elif assertion_type == "matches":
|
644
|
-
if not isinstance(actual_value, str)
|
954
|
+
if not isinstance(actual_value, str):
|
645
955
|
return False
|
646
956
|
try:
|
647
957
|
import re
|
648
|
-
|
958
|
+
pattern = str(expected_value)
|
959
|
+
return bool(re.search(pattern, actual_value))
|
649
960
|
except:
|
650
961
|
return False
|
651
|
-
elif assertion_type == "in":
|
652
|
-
return actual_value in expected_value
|
653
|
-
elif assertion_type == "not_in":
|
654
|
-
return actual_value not in expected_value
|
655
962
|
elif assertion_type == "schema":
|
656
963
|
try:
|
657
964
|
from jsonschema import validate
|
@@ -661,7 +968,7 @@ class HTTPRequest:
|
|
661
968
|
return False
|
662
969
|
else:
|
663
970
|
raise ValueError(f"不支持的断言类型: {assertion_type}")
|
664
|
-
|
971
|
+
|
665
972
|
def _compare_values(self, actual_value: Any, expected_value: Any, operator: str) -> bool:
|
666
973
|
"""比较两个值
|
667
974
|
|
@@ -686,29 +993,11 @@ class HTTPRequest:
|
|
686
993
|
elif operator == "gte":
|
687
994
|
return actual_value >= expected_value
|
688
995
|
elif operator == "in":
|
996
|
+
# in操作符检查actual是否在expected列表中
|
689
997
|
return actual_value in expected_value
|
690
998
|
elif operator == "not_in":
|
999
|
+
# not_in操作符检查actual是否不在expected列表中
|
691
1000
|
return actual_value not in expected_value
|
692
|
-
elif operator == "contains":
|
693
|
-
if isinstance(actual_value, str) and isinstance(expected_value, str):
|
694
|
-
return expected_value in actual_value
|
695
|
-
elif isinstance(actual_value, (list, tuple, dict)):
|
696
|
-
return expected_value in actual_value
|
697
|
-
return False
|
698
|
-
elif operator == "not_contains":
|
699
|
-
if isinstance(actual_value, str) and isinstance(expected_value, str):
|
700
|
-
return expected_value not in actual_value
|
701
|
-
elif isinstance(actual_value, (list, tuple, dict)):
|
702
|
-
return expected_value not in actual_value
|
703
|
-
return True
|
704
|
-
elif operator == "matches":
|
705
|
-
if not isinstance(actual_value, str) or not isinstance(expected_value, str):
|
706
|
-
return False
|
707
|
-
try:
|
708
|
-
import re
|
709
|
-
return bool(re.search(expected_value, actual_value))
|
710
|
-
except:
|
711
|
-
return False
|
712
1001
|
else:
|
713
1002
|
raise ValueError(f"不支持的比较操作符: {operator}")
|
714
1003
|
|