pytest-dsl 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. pytest_dsl/__init__.py +10 -0
  2. pytest_dsl/cli.py +44 -0
  3. pytest_dsl/conftest_adapter.py +4 -0
  4. pytest_dsl/core/__init__.py +0 -0
  5. pytest_dsl/core/auth_provider.py +409 -0
  6. pytest_dsl/core/auto_decorator.py +181 -0
  7. pytest_dsl/core/auto_directory.py +81 -0
  8. pytest_dsl/core/context.py +23 -0
  9. pytest_dsl/core/custom_auth_example.py +425 -0
  10. pytest_dsl/core/dsl_executor.py +329 -0
  11. pytest_dsl/core/dsl_executor_utils.py +84 -0
  12. pytest_dsl/core/global_context.py +103 -0
  13. pytest_dsl/core/http_client.py +411 -0
  14. pytest_dsl/core/http_request.py +810 -0
  15. pytest_dsl/core/keyword_manager.py +109 -0
  16. pytest_dsl/core/lexer.py +139 -0
  17. pytest_dsl/core/parser.py +197 -0
  18. pytest_dsl/core/parsetab.py +76 -0
  19. pytest_dsl/core/plugin_discovery.py +187 -0
  20. pytest_dsl/core/utils.py +146 -0
  21. pytest_dsl/core/variable_utils.py +267 -0
  22. pytest_dsl/core/yaml_loader.py +62 -0
  23. pytest_dsl/core/yaml_vars.py +75 -0
  24. pytest_dsl/docs/custom_keywords.md +140 -0
  25. pytest_dsl/examples/__init__.py +5 -0
  26. pytest_dsl/examples/assert/assertion_example.auto +44 -0
  27. pytest_dsl/examples/assert/boolean_test.auto +34 -0
  28. pytest_dsl/examples/assert/expression_test.auto +49 -0
  29. pytest_dsl/examples/http/__init__.py +3 -0
  30. pytest_dsl/examples/http/builtin_auth_test.auto +79 -0
  31. pytest_dsl/examples/http/csrf_auth_test.auto +64 -0
  32. pytest_dsl/examples/http/custom_auth_test.auto +76 -0
  33. pytest_dsl/examples/http/file_reference_test.auto +111 -0
  34. pytest_dsl/examples/http/http_advanced.auto +91 -0
  35. pytest_dsl/examples/http/http_example.auto +147 -0
  36. pytest_dsl/examples/http/http_length_test.auto +55 -0
  37. pytest_dsl/examples/http/http_retry_assertions.auto +91 -0
  38. pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +94 -0
  39. pytest_dsl/examples/http/http_with_yaml.auto +58 -0
  40. pytest_dsl/examples/http/new_retry_test.auto +22 -0
  41. pytest_dsl/examples/http/retry_assertions_only.auto +52 -0
  42. pytest_dsl/examples/http/retry_config_only.auto +49 -0
  43. pytest_dsl/examples/http/retry_debug.auto +22 -0
  44. pytest_dsl/examples/http/retry_with_fix.auto +21 -0
  45. pytest_dsl/examples/http/simple_retry.auto +20 -0
  46. pytest_dsl/examples/http/vars.yaml +55 -0
  47. pytest_dsl/examples/http_clients.yaml +48 -0
  48. pytest_dsl/examples/keyword_example.py +70 -0
  49. pytest_dsl/examples/test_assert.py +16 -0
  50. pytest_dsl/examples/test_http.py +168 -0
  51. pytest_dsl/keywords/__init__.py +10 -0
  52. pytest_dsl/keywords/assertion_keywords.py +610 -0
  53. pytest_dsl/keywords/global_keywords.py +51 -0
  54. pytest_dsl/keywords/http_keywords.py +430 -0
  55. pytest_dsl/keywords/system_keywords.py +17 -0
  56. pytest_dsl/main_adapter.py +7 -0
  57. pytest_dsl/plugin.py +44 -0
  58. pytest_dsl-0.1.0.dist-info/METADATA +537 -0
  59. pytest_dsl-0.1.0.dist-info/RECORD +63 -0
  60. pytest_dsl-0.1.0.dist-info/WHEEL +5 -0
  61. pytest_dsl-0.1.0.dist-info/entry_points.txt +5 -0
  62. pytest_dsl-0.1.0.dist-info/licenses/LICENSE +21 -0
  63. pytest_dsl-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,810 @@
1
+ import json
2
+ import re
3
+ import yaml
4
+ import jsonpath_ng.ext as jsonpath
5
+ from typing import Dict, List, Any, Union, Optional, Tuple
6
+ import lxml.etree as etree
7
+ from requests import Response
8
+ import allure
9
+ import requests
10
+
11
+ from pytest_dsl.core.http_client import http_client_manager
12
+
13
+
14
+ class HTTPRequest:
15
+ """HTTP请求处理类
16
+
17
+ 负责处理HTTP请求、响应捕获和断言
18
+ """
19
+
20
+ def __init__(self, config: Dict[str, Any], client_name: str = "default", session_name: str = None):
21
+ """初始化HTTP请求
22
+
23
+ Args:
24
+ config: 请求配置
25
+ client_name: 客户端名称
26
+ session_name: 会话名称(如果需要使用命名会话)
27
+ """
28
+ self.config = config
29
+ self.client_name = client_name
30
+ self.session_name = session_name
31
+ self.response = None
32
+ self.captured_values = {}
33
+
34
+ def execute(self, disable_auth: bool = False) -> Response:
35
+ """执行HTTP请求
36
+
37
+ Args:
38
+ disable_auth: 是否禁用认证
39
+
40
+ Returns:
41
+ Response对象
42
+ """
43
+ # 获取HTTP客户端
44
+ if self.session_name:
45
+ client = http_client_manager.get_session(self.session_name, self.client_name)
46
+ else:
47
+ client = http_client_manager.get_client(self.client_name)
48
+
49
+ # 验证客户端有效性
50
+ if client is None:
51
+ error_message = f"无法获取HTTP客户端: {self.client_name}"
52
+ allure.attach(
53
+ error_message,
54
+ name="HTTP客户端错误",
55
+ attachment_type=allure.attachment_type.TEXT
56
+ )
57
+ raise ValueError(error_message)
58
+
59
+ # 提取请求参数
60
+ method = self.config.get('method', 'GET').upper()
61
+ url = self.config.get('url', '')
62
+
63
+ # 配置中是否禁用认证
64
+ disable_auth = disable_auth or self.config.get('disable_auth', False)
65
+
66
+ request_config = self.config.get('request', {})
67
+
68
+ # 构建请求参数
69
+ request_kwargs = {
70
+ 'params': request_config.get('params'),
71
+ 'headers': request_config.get('headers'),
72
+ 'json': request_config.get('json'),
73
+ 'data': request_config.get('data'),
74
+ 'files': request_config.get('files'),
75
+ 'cookies': request_config.get('cookies'),
76
+ 'auth': tuple(request_config.get('auth')) if request_config.get('auth') else None,
77
+ 'timeout': request_config.get('timeout'),
78
+ 'allow_redirects': request_config.get('allow_redirects'),
79
+ 'verify': request_config.get('verify'),
80
+ 'cert': request_config.get('cert'),
81
+ 'proxies': request_config.get('proxies'),
82
+ 'disable_auth': disable_auth # 传递禁用认证标志
83
+ }
84
+
85
+ # 过滤掉None值
86
+ request_kwargs = {k: v for k, v in request_kwargs.items() if v is not None}
87
+
88
+ # 使用Allure记录请求信息
89
+ self._log_request_to_allure(method, url, request_kwargs)
90
+
91
+ try:
92
+ # 发送请求
93
+ self.response = client.make_request(method, url, **request_kwargs)
94
+
95
+ # 使用Allure记录响应信息
96
+ self._log_response_to_allure(self.response)
97
+
98
+ # 处理捕获
99
+ self.process_captures()
100
+
101
+ return self.response
102
+ except requests.exceptions.RequestException as e:
103
+ # 记录请求异常到Allure
104
+ error_message = f"请求异常: {str(e)}"
105
+ allure.attach(
106
+ error_message,
107
+ name=f"HTTP请求失败: {method} {url}",
108
+ attachment_type=allure.attachment_type.TEXT
109
+ )
110
+
111
+ # 重新抛出更有意义的异常
112
+ raise ValueError(f"HTTP请求失败: {str(e)}") from e
113
+ except Exception as e:
114
+ # 捕获所有其他异常
115
+ error_message = f"未预期的异常: {type(e).__name__}: {str(e)}"
116
+ allure.attach(
117
+ error_message,
118
+ name=f"HTTP请求执行错误: {method} {url}",
119
+ attachment_type=allure.attachment_type.TEXT
120
+ )
121
+
122
+ # 重新抛出异常
123
+ raise ValueError(f"HTTP请求执行错误: {str(e)}") from e
124
+
125
+ def process_captures(self) -> Dict[str, Any]:
126
+ """处理响应捕获
127
+
128
+ Returns:
129
+ 捕获的值字典
130
+ """
131
+ if not self.response:
132
+ error_message = "需要先执行请求才能捕获响应"
133
+ # 记录更详细的错误信息到Allure
134
+ debug_info = (
135
+ f"错误详情: self.response 为 None\n"
136
+ f"配置信息: {json.dumps(self.config, indent=2, ensure_ascii=False, default=str)}\n"
137
+ f"当前状态: 客户端名称={self.client_name}, 会话名称={self.session_name}"
138
+ )
139
+ allure.attach(
140
+ debug_info,
141
+ name="捕获失败详情",
142
+ attachment_type=allure.attachment_type.TEXT
143
+ )
144
+ raise ValueError(error_message)
145
+
146
+ captures_config = self.config.get('captures', {})
147
+
148
+ for var_name, capture_spec in captures_config.items():
149
+ if not isinstance(capture_spec, list):
150
+ raise ValueError(f"无效的捕获规格: {var_name}: {capture_spec}")
151
+
152
+ # 提取捕获信息
153
+ try:
154
+ extractor_type = capture_spec[0]
155
+ extraction_path = capture_spec[1] if len(capture_spec) > 1 else None
156
+
157
+ # 检查是否有length参数
158
+ is_length_capture = False
159
+ if len(capture_spec) > 2 and capture_spec[2] == "length":
160
+ is_length_capture = True
161
+ default_value = capture_spec[3] if len(capture_spec) > 3 else None
162
+ else:
163
+ default_value = capture_spec[2] if len(capture_spec) > 2 else None
164
+
165
+ # 提取值
166
+ captured_value = self._extract_value(extractor_type, extraction_path, default_value)
167
+
168
+ # 特殊处理length
169
+ if is_length_capture:
170
+ try:
171
+ original_value = captured_value
172
+ captured_value = len(captured_value)
173
+
174
+ # 记录长度到Allure
175
+ allure.attach(
176
+ f"变量名: {var_name}\n提取器: {extractor_type}\n路径: {extraction_path}\n原始值: {str(original_value)}\n长度: {captured_value}",
177
+ name=f"捕获长度: {var_name}",
178
+ attachment_type=allure.attachment_type.TEXT
179
+ )
180
+ except Exception as e:
181
+ # 如果无法计算长度,记录错误并添加请求和响应信息
182
+ error_msg = f"变量名: {var_name}\n提取器: {extractor_type}\n路径: {extraction_path}\n错误: 无法计算长度: {str(e)}"
183
+
184
+ # 添加请求信息
185
+ error_msg += "\n\n=== 请求信息 ==="
186
+ error_msg += f"\nMethod: {self.config.get('method', 'GET')}"
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
+
207
+ allure.attach(
208
+ error_msg,
209
+ name=f"捕获长度失败: {var_name}",
210
+ attachment_type=allure.attachment_type.TEXT
211
+ )
212
+ captured_value = 0 # 默认长度
213
+ else:
214
+ # 记录捕获到Allure
215
+ allure.attach(
216
+ f"变量名: {var_name}\n提取器: {extractor_type}\n路径: {extraction_path}\n提取值: {str(captured_value)}",
217
+ name=f"捕获变量: {var_name}",
218
+ attachment_type=allure.attachment_type.TEXT
219
+ )
220
+
221
+ self.captured_values[var_name] = captured_value
222
+ except Exception as e:
223
+ error_msg = (
224
+ f"变量捕获失败: {var_name}\n"
225
+ f"捕获规格: {capture_spec}\n"
226
+ f"错误: {type(e).__name__}: {str(e)}"
227
+ )
228
+ allure.attach(
229
+ error_msg,
230
+ name=f"变量捕获失败: {var_name}",
231
+ attachment_type=allure.attachment_type.TEXT
232
+ )
233
+ # 设置默认值
234
+ self.captured_values[var_name] = None
235
+
236
+ return self.captured_values
237
+
238
+ def process_asserts(self, specific_asserts=None) -> List[Dict[str, Any]]:
239
+ """处理响应断言
240
+
241
+ Args:
242
+ specific_asserts: 指定要处理的断言列表,如果为None则处理所有断言
243
+
244
+ Returns:
245
+ 断言结果列表
246
+ """
247
+ if not self.response:
248
+ raise ValueError("需要先执行请求才能进行断言")
249
+
250
+ asserts_config = self.config.get('asserts', [])
251
+ assert_results = []
252
+ failed_retryable_assertions = []
253
+
254
+ # 处理断言重试配置
255
+ # 1. 只使用独立的retry_assertions配置
256
+ retry_assertions_config = self.config.get('retry_assertions', {})
257
+ has_dedicated_retry_config = bool(retry_assertions_config)
258
+
259
+ # 2. 向后兼容: 检查全局retry配置(仅作为默认值使用)
260
+ retry_config = self.config.get('retry', {})
261
+ global_retry_enabled = bool(retry_config)
262
+
263
+ # 3. 提取重试默认设置
264
+ global_retry_count = retry_assertions_config.get('count', retry_config.get('count', 3))
265
+ global_retry_interval = retry_assertions_config.get('interval', retry_config.get('interval', 1))
266
+
267
+ # 4. 提取应该重试的断言索引列表
268
+ retry_all_assertions = retry_assertions_config.get('all', global_retry_enabled)
269
+ retry_assertion_indices = retry_assertions_config.get('indices', [])
270
+
271
+ # 5. 提取特定断言的重试配置
272
+ specific_assertion_configs = retry_assertions_config.get('specific', {})
273
+
274
+ # 如果传入了specific_asserts,只处理指定的断言
275
+ process_asserts = specific_asserts if specific_asserts is not None else asserts_config
276
+
277
+ for assertion_idx, assertion in enumerate(process_asserts):
278
+ if not isinstance(assertion, list) or len(assertion) < 2:
279
+ raise ValueError(f"无效的断言配置: {assertion}")
280
+
281
+ # 提取断言参数
282
+ extractor_type = assertion[0]
283
+
284
+ # 判断该断言是否应该重试(只使用retry_assertions配置)
285
+ is_retryable = False
286
+ assertion_retry_count = global_retry_count
287
+ assertion_retry_interval = global_retry_interval
288
+
289
+ # retry_assertions特定配置
290
+ if str(assertion_idx) in specific_assertion_configs:
291
+ spec_config = specific_assertion_configs[str(assertion_idx)]
292
+ is_retryable = True
293
+ if isinstance(spec_config, dict):
294
+ if 'count' in spec_config:
295
+ assertion_retry_count = spec_config['count']
296
+ if 'interval' in spec_config:
297
+ assertion_retry_interval = spec_config['interval']
298
+ # retry_assertions索引列表
299
+ elif assertion_idx in retry_assertion_indices:
300
+ is_retryable = True
301
+ # retry_assertions全局配置
302
+ elif retry_all_assertions:
303
+ is_retryable = True
304
+
305
+ # 处理断言参数
306
+ if len(assertion) == 2: # 存在性断言 ["header", "Location", "exists"]
307
+ extraction_path = assertion[1]
308
+ assertion_type = "exists"
309
+ expected_value = None
310
+ compare_operator = "eq" # 默认比较操作符
311
+ elif len(assertion) == 3: # 简单断言 ["status", "eq", 200]
312
+ if extractor_type in ["status", "body", "response_time"]:
313
+ extraction_path = None
314
+ assertion_type = "value" # 标记为简单值比较
315
+ compare_operator = assertion[1] # 比较操作符
316
+ expected_value = assertion[2] # 预期值
317
+ else:
318
+ extraction_path = assertion[1]
319
+ assertion_type = assertion[2]
320
+ compare_operator = "eq" # 默认比较操作符
321
+ expected_value = None
322
+ elif len(assertion) == 4: # 带操作符的断言 ["jsonpath", "$.id", "eq", 1]
323
+ extraction_path = assertion[1]
324
+ if assertion[2] in ["eq", "neq", "lt", "lte", "gt", "gte"]:
325
+ # 这是带比较操作符的断言
326
+ assertion_type = "value" # 标记为值比较
327
+ compare_operator = assertion[2] # 比较操作符
328
+ expected_value = assertion[3] # 预期值
329
+ else:
330
+ # 其他类型的断言,比如特殊断言
331
+ assertion_type = assertion[2]
332
+ compare_operator = "eq" # 默认比较操作符
333
+ expected_value = assertion[3]
334
+ else: # 5个参数,例如 ["jsonpath", "$", "length", "eq", 10]
335
+ extraction_path = assertion[1]
336
+ assertion_type = assertion[2]
337
+ compare_operator = assertion[3]
338
+ expected_value = assertion[4]
339
+
340
+ # 提取实际值
341
+ actual_value = self._extract_value(extractor_type, extraction_path)
342
+
343
+ # 特殊处理"length"断言类型
344
+ original_actual_value = actual_value
345
+ if assertion_type == "length" and extractor_type != "response_time" and extractor_type != "status" and extractor_type != "body":
346
+ try:
347
+ actual_value = len(actual_value)
348
+ except Exception as e:
349
+ # 长度计算失败的信息已在_extract_value中记录
350
+ actual_value = 0
351
+
352
+ # 执行断言
353
+ assertion_result = {
354
+ 'type': extractor_type,
355
+ 'path': extraction_path,
356
+ 'assertion_type': assertion_type,
357
+ 'operator': compare_operator,
358
+ 'actual_value': actual_value,
359
+ 'expected_value': expected_value,
360
+ 'original_value': original_actual_value if assertion_type == "length" else None,
361
+ 'retryable': is_retryable,
362
+ 'retry_count': assertion_retry_count,
363
+ 'retry_interval': assertion_retry_interval,
364
+ 'index': assertion_idx # 记录断言在原始列表中的索引
365
+ }
366
+
367
+ try:
368
+ # 验证断言
369
+ result = self._perform_assertion(assertion_type, compare_operator, actual_value, expected_value)
370
+ assertion_result['result'] = result
371
+ assertion_result['passed'] = True
372
+
373
+ # 使用Allure记录断言成功
374
+ allure.attach(
375
+ self._format_assertion_details(assertion_result),
376
+ name=f"断言成功: {extractor_type}",
377
+ attachment_type=allure.attachment_type.TEXT
378
+ )
379
+ except AssertionError as e:
380
+ assertion_result['result'] = False
381
+ assertion_result['passed'] = False
382
+ assertion_result['error'] = str(e)
383
+
384
+ # 使用Allure记录断言失败
385
+ allure.attach(
386
+ self._format_assertion_details(assertion_result) + f"\n\n错误: {str(e)}",
387
+ name=f"断言失败: {extractor_type}",
388
+ attachment_type=allure.attachment_type.TEXT
389
+ )
390
+
391
+ # 如果断言可重试,添加到失败且需要重试的断言列表
392
+ if is_retryable:
393
+ failed_retryable_assertions.append(assertion_result)
394
+
395
+ # 抛出异常(会在外层捕获)
396
+ raise AssertionError(f"断言失败 [{extractor_type}]: {str(e)}")
397
+
398
+ assert_results.append(assertion_result)
399
+
400
+ # 返回断言结果和需要重试的断言
401
+ return assert_results, failed_retryable_assertions
402
+
403
+ def _format_assertion_details(self, assertion_result: Dict[str, Any]) -> str:
404
+ """格式化断言详情,用于Allure报告
405
+
406
+ Args:
407
+ assertion_result: 断言结果字典
408
+
409
+ Returns:
410
+ 格式化的断言详情字符串
411
+ """
412
+ details = f"类型: {assertion_result['type']}\n"
413
+ if assertion_result['path']:
414
+ details += f"路径: {assertion_result['path']}\n"
415
+
416
+ if assertion_result['assertion_type'] == 'length':
417
+ details += f"原始值: {assertion_result['original_value']}\n"
418
+ details += f"长度: {assertion_result['actual_value']}\n"
419
+ else:
420
+ details += f"实际值: {assertion_result['actual_value']}\n"
421
+
422
+ details += f"操作符: {assertion_result['operator']}\n"
423
+
424
+ if assertion_result['expected_value'] is not None:
425
+ details += f"预期值: {assertion_result['expected_value']}\n"
426
+
427
+ details += f"结果: {'通过' if assertion_result['passed'] else '失败'}"
428
+
429
+ return details
430
+
431
+ def _extract_value(self, extractor_type: str, extraction_path: str = None, default_value: Any = None) -> Any:
432
+ """从响应提取值
433
+
434
+ Args:
435
+ extractor_type: 提取器类型
436
+ extraction_path: 提取路径
437
+ default_value: 默认值
438
+
439
+ Returns:
440
+ 提取的值
441
+ """
442
+ if not self.response:
443
+ return default_value
444
+
445
+ try:
446
+ if extractor_type == "jsonpath":
447
+ return self._extract_jsonpath(extraction_path, default_value)
448
+ elif extractor_type == "xpath":
449
+ return self._extract_xpath(extraction_path, default_value)
450
+ elif extractor_type == "regex":
451
+ return self._extract_regex(extraction_path, default_value)
452
+ elif extractor_type == "header":
453
+ return self._extract_header(extraction_path, default_value)
454
+ elif extractor_type == "cookie":
455
+ return self._extract_cookie(extraction_path, default_value)
456
+ elif extractor_type == "status":
457
+ return self.response.status_code
458
+ elif extractor_type == "body":
459
+ return self.response.text
460
+ elif extractor_type == "response_time":
461
+ return self.response.elapsed.total_seconds() * 1000
462
+ else:
463
+ raise ValueError(f"不支持的提取器类型: {extractor_type}")
464
+ except Exception as e:
465
+ if default_value is not None:
466
+ return default_value
467
+ raise ValueError(f"提取值失败({extractor_type}, {extraction_path}): {str(e)}")
468
+
469
+ def _extract_jsonpath(self, path: str, default_value: Any = None) -> Any:
470
+ """使用JSONPath从JSON响应提取值
471
+
472
+ Args:
473
+ path: JSONPath表达式
474
+ default_value: 默认值
475
+
476
+ Returns:
477
+ 提取的值
478
+ """
479
+ try:
480
+ json_data = self.response.json()
481
+
482
+ jsonpath_expr = jsonpath.parse(path)
483
+ matches = [match.value for match in jsonpath_expr.find(json_data)]
484
+
485
+ if not matches:
486
+ return default_value
487
+ elif len(matches) == 1:
488
+ return matches[0]
489
+ else:
490
+ return matches
491
+ except Exception as e:
492
+ if default_value is not None:
493
+ return default_value
494
+ raise ValueError(f"JSONPath提取失败: {str(e)}")
495
+
496
+ def _extract_xpath(self, path: str, default_value: Any = None) -> Any:
497
+ """使用XPath从HTML/XML响应提取值
498
+
499
+ Args:
500
+ path: XPath表达式
501
+ default_value: 默认值
502
+
503
+ Returns:
504
+ 提取的值
505
+ """
506
+ try:
507
+ # 尝试解析响应内容
508
+ parser = etree.HTMLParser()
509
+ tree = etree.fromstring(self.response.content, parser)
510
+
511
+ # 执行XPath
512
+ result = tree.xpath(path)
513
+
514
+ if not result:
515
+ return default_value
516
+ elif len(result) == 1:
517
+ return result[0]
518
+ else:
519
+ return result
520
+ except Exception as e:
521
+ if default_value is not None:
522
+ return default_value
523
+ raise ValueError(f"XPath提取失败: {str(e)}")
524
+
525
+ def _extract_regex(self, pattern: str, default_value: Any = None) -> Any:
526
+ """使用正则表达式从响应提取值
527
+
528
+ Args:
529
+ pattern: 正则表达式模式
530
+ default_value: 默认值
531
+
532
+ Returns:
533
+ 提取的值
534
+ """
535
+ try:
536
+ # 如果响应是JSON格式,先转换为字符串
537
+ if 'application/json' in self.response.headers.get('Content-Type', ''):
538
+ text = json.dumps(self.response.json())
539
+ else:
540
+ text = self.response.text
541
+
542
+ matches = re.findall(pattern, text)
543
+
544
+ if not matches:
545
+ return default_value
546
+ elif len(matches) == 1:
547
+ return matches[0]
548
+ else:
549
+ return matches
550
+ except Exception as e:
551
+ if default_value is not None:
552
+ return default_value
553
+ raise ValueError(f"正则表达式提取失败: {str(e)}")
554
+
555
+ def _extract_header(self, header_name: str, default_value: Any = None) -> Any:
556
+ """从响应头提取值
557
+
558
+ Args:
559
+ header_name: 响应头名称
560
+ default_value: 默认值
561
+
562
+ Returns:
563
+ 提取的值
564
+ """
565
+ header_value = self.response.headers.get(header_name)
566
+ return header_value if header_value is not None else default_value
567
+
568
+ def _extract_cookie(self, cookie_name: str, default_value: Any = None) -> Any:
569
+ """从响应Cookie提取值
570
+
571
+ Args:
572
+ cookie_name: Cookie名称
573
+ default_value: 默认值
574
+
575
+ Returns:
576
+ 提取的值
577
+ """
578
+ cookie = self.response.cookies.get(cookie_name)
579
+ return cookie if cookie is not None else default_value
580
+
581
+ def _perform_assertion(self, assertion_type: str, operator: str, actual_value: Any, expected_value: Any = None) -> bool:
582
+ """执行断言
583
+
584
+ Args:
585
+ assertion_type: 断言类型
586
+ operator: 比较操作符
587
+ actual_value: 实际值
588
+ expected_value: 预期值
589
+
590
+ Returns:
591
+ 断言结果
592
+ """
593
+ # 类型转换
594
+ if operator in ["eq", "neq", "lt", "lte", "gt", "gte"] and expected_value is not None:
595
+ if isinstance(expected_value, str):
596
+ # 去除空白字符和换行符后再判断
597
+ clean_expected = expected_value.strip()
598
+ if clean_expected.isdigit():
599
+ expected_value = int(clean_expected)
600
+ elif clean_expected.replace('.', '', 1).isdigit():
601
+ expected_value = float(clean_expected)
602
+
603
+ if isinstance(actual_value, str):
604
+ # 去除空白字符和换行符后再判断
605
+ clean_actual = actual_value.strip()
606
+ if clean_actual.isdigit():
607
+ actual_value = int(clean_actual)
608
+ elif clean_actual.replace('.', '', 1).isdigit():
609
+ actual_value = float(clean_actual)
610
+
611
+ # 基于断言类型执行断言
612
+ if assertion_type == "value" or assertion_type == "length":
613
+ # 直接使用操作符进行比较
614
+ return self._compare_values(actual_value, expected_value, operator)
615
+ elif assertion_type == "exists":
616
+ return actual_value is not None
617
+ elif assertion_type == "not_exists":
618
+ return actual_value is None
619
+ elif assertion_type == "type":
620
+ if expected_value == "string":
621
+ return isinstance(actual_value, str)
622
+ elif expected_value == "number":
623
+ return isinstance(actual_value, (int, float))
624
+ elif expected_value == "boolean":
625
+ return isinstance(actual_value, bool)
626
+ elif expected_value == "array":
627
+ return isinstance(actual_value, list)
628
+ elif expected_value == "object":
629
+ return isinstance(actual_value, dict)
630
+ elif expected_value == "null":
631
+ return actual_value is None
632
+ return False
633
+ elif assertion_type == "contains":
634
+ if isinstance(actual_value, str) and isinstance(expected_value, str):
635
+ return expected_value in actual_value
636
+ elif isinstance(actual_value, (list, tuple, dict)):
637
+ return expected_value in actual_value
638
+ return False
639
+ elif assertion_type == "startswith":
640
+ return isinstance(actual_value, str) and actual_value.startswith(expected_value)
641
+ elif assertion_type == "endswith":
642
+ return isinstance(actual_value, str) and actual_value.endswith(expected_value)
643
+ elif assertion_type == "matches":
644
+ if not isinstance(actual_value, str) or not isinstance(expected_value, str):
645
+ return False
646
+ try:
647
+ import re
648
+ return bool(re.search(expected_value, actual_value))
649
+ except:
650
+ 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
+ elif assertion_type == "schema":
656
+ try:
657
+ from jsonschema import validate
658
+ validate(instance=actual_value, schema=expected_value)
659
+ return True
660
+ except:
661
+ return False
662
+ else:
663
+ raise ValueError(f"不支持的断言类型: {assertion_type}")
664
+
665
+ def _compare_values(self, actual_value: Any, expected_value: Any, operator: str) -> bool:
666
+ """比较两个值
667
+
668
+ Args:
669
+ actual_value: 实际值
670
+ expected_value: 预期值
671
+ operator: 比较操作符
672
+
673
+ Returns:
674
+ 比较结果
675
+ """
676
+ if operator == "eq":
677
+ return actual_value == expected_value
678
+ elif operator == "neq":
679
+ return actual_value != expected_value
680
+ elif operator == "lt":
681
+ return actual_value < expected_value
682
+ elif operator == "lte":
683
+ return actual_value <= expected_value
684
+ elif operator == "gt":
685
+ return actual_value > expected_value
686
+ elif operator == "gte":
687
+ return actual_value >= expected_value
688
+ elif operator == "in":
689
+ return actual_value in expected_value
690
+ elif operator == "not_in":
691
+ 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
+ else:
713
+ raise ValueError(f"不支持的比较操作符: {operator}")
714
+
715
+ def _log_request_to_allure(self, method: str, url: str, request_kwargs: Dict[str, Any]) -> None:
716
+ """使用Allure记录请求信息
717
+
718
+ Args:
719
+ method: HTTP方法
720
+ url: 请求URL
721
+ request_kwargs: 请求参数
722
+ """
723
+ # 创建请求信息摘要
724
+ request_summary = f"{method} {url}"
725
+
726
+ # 创建详细请求信息
727
+ request_details = [f"Method: {method}", f"URL: {url}"]
728
+
729
+ # 添加请求头
730
+ if "headers" in request_kwargs and request_kwargs["headers"]:
731
+ # 隐藏敏感信息
732
+ safe_headers = {}
733
+ for key, value in request_kwargs["headers"].items():
734
+ if key.lower() in ["authorization", "x-api-key", "token", "api-key"]:
735
+ safe_headers[key] = "***REDACTED***"
736
+ else:
737
+ safe_headers[key] = value
738
+ request_details.append("Headers:")
739
+ for key, value in safe_headers.items():
740
+ request_details.append(f" {key}: {value}")
741
+
742
+ # 添加查询参数
743
+ if "params" in request_kwargs and request_kwargs["params"]:
744
+ request_details.append("Query Parameters:")
745
+ for key, value in request_kwargs["params"].items():
746
+ request_details.append(f" {key}: {value}")
747
+
748
+ # 添加请求体
749
+ if "json" in request_kwargs and request_kwargs["json"]:
750
+ request_details.append("JSON Body:")
751
+ try:
752
+ request_details.append(json.dumps(request_kwargs["json"], indent=2, ensure_ascii=False))
753
+ except:
754
+ request_details.append(str(request_kwargs["json"]))
755
+ elif "data" in request_kwargs and request_kwargs["data"]:
756
+ request_details.append("Form Data:")
757
+ for key, value in request_kwargs["data"].items():
758
+ request_details.append(f" {key}: {value}")
759
+
760
+ # 添加文件信息
761
+ if "files" in request_kwargs and request_kwargs["files"]:
762
+ request_details.append("Files:")
763
+ for key, value in request_kwargs["files"].items():
764
+ request_details.append(f" {key}: <File object>")
765
+
766
+ # 记录到Allure
767
+ allure.attach(
768
+ "\n".join(request_details),
769
+ name=f"HTTP请求: {request_summary}",
770
+ attachment_type=allure.attachment_type.TEXT
771
+ )
772
+
773
+ def _log_response_to_allure(self, response: Response) -> None:
774
+ """使用Allure记录响应信息
775
+
776
+ Args:
777
+ response: 响应对象
778
+ """
779
+ # 创建响应信息摘要
780
+ response_summary = f"{response.status_code} {response.reason} ({response.elapsed.total_seconds() * 1000:.2f}ms)"
781
+
782
+ # 创建详细响应信息
783
+ response_details = [
784
+ f"Status: {response.status_code} {response.reason}",
785
+ f"Response Time: {response.elapsed.total_seconds() * 1000:.2f}ms"
786
+ ]
787
+
788
+ # 添加响应头
789
+ response_details.append("Headers:")
790
+ for key, value in response.headers.items():
791
+ response_details.append(f" {key}: {value}")
792
+
793
+ # 添加响应体
794
+ response_details.append("Body:")
795
+ try:
796
+ if 'application/json' in response.headers.get('Content-Type', ''):
797
+ response_details.append(json.dumps(response.json(), indent=2, ensure_ascii=False))
798
+ elif len(response.content) < 10240: # 限制大小
799
+ response_details.append(response.text)
800
+ else:
801
+ response_details.append(f"<{len(response.content)} bytes>")
802
+ except Exception as e:
803
+ response_details.append(f"<Error parsing body: {str(e)}>")
804
+
805
+ # 记录到Allure
806
+ allure.attach(
807
+ "\n".join(response_details),
808
+ name=f"HTTP响应: {response_summary}",
809
+ attachment_type=allure.attachment_type.TEXT
810
+ )