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,610 @@
1
+ """断言关键字模块
2
+
3
+ 该模块提供了针对不同数据类型的断言功能,以及JSON数据提取能力。
4
+ 支持字符串、数字、布尔值、列表和JSON对象的比较和断言。
5
+ """
6
+
7
+ import json
8
+ import re
9
+ import allure
10
+ from typing import Any, Dict, List, Union
11
+ import jsonpath_ng.ext as jsonpath
12
+ from pytest_dsl.core.keyword_manager import keyword_manager
13
+
14
+
15
+ def _extract_jsonpath(json_data: Union[Dict, List], path: str) -> Any:
16
+ """使用JSONPath从JSON数据中提取值
17
+
18
+ Args:
19
+ json_data: 要提取数据的JSON对象或数组
20
+ path: JSONPath表达式
21
+
22
+ Returns:
23
+ 提取的值或值列表
24
+
25
+ Raises:
26
+ ValueError: 如果JSONPath表达式无效或找不到匹配项
27
+ """
28
+ try:
29
+ if isinstance(json_data, str):
30
+ json_data = json.loads(json_data)
31
+
32
+ jsonpath_expr = jsonpath.parse(path)
33
+ matches = [match.value for match in jsonpath_expr.find(json_data)]
34
+
35
+ if not matches:
36
+ return None
37
+ elif len(matches) == 1:
38
+ return matches[0]
39
+ else:
40
+ return matches
41
+ except Exception as e:
42
+ raise ValueError(f"JSONPath提取错误: {str(e)}")
43
+
44
+
45
+ def _compare_values(actual: Any, expected: Any, operator: str = "==") -> bool:
46
+ """比较两个值
47
+
48
+ Args:
49
+ actual: 实际值
50
+ expected: 预期值
51
+ operator: 比较运算符 (==, !=, >, <, >=, <=, contains, not_contains, matches, and, or, not)
52
+
53
+ Returns:
54
+ 比较结果 (True/False)
55
+ """
56
+ # 执行比较
57
+ if operator == "==":
58
+ return actual == expected
59
+ elif operator == "!=":
60
+ return actual != expected
61
+ elif operator == ">":
62
+ return actual > expected
63
+ elif operator == "<":
64
+ return actual < expected
65
+ elif operator == ">=":
66
+ return actual >= expected
67
+ elif operator == "<=":
68
+ return actual <= expected
69
+ elif operator == "contains":
70
+ if isinstance(actual, str) and isinstance(expected, str):
71
+ return expected in actual
72
+ elif isinstance(actual, (list, tuple, dict)):
73
+ return expected in actual
74
+ return False
75
+ elif operator == "not_contains":
76
+ if isinstance(actual, str) and isinstance(expected, str):
77
+ return expected not in actual
78
+ elif isinstance(actual, (list, tuple, dict)):
79
+ return expected not in actual
80
+ return True
81
+ elif operator == "matches":
82
+ if isinstance(actual, str) and isinstance(expected, str):
83
+ try:
84
+ return bool(re.match(expected, actual))
85
+ except re.error:
86
+ raise ValueError(f"无效的正则表达式: {expected}")
87
+ return False
88
+ elif operator == "and":
89
+ return bool(actual) and bool(expected)
90
+ elif operator == "or":
91
+ return bool(actual) or bool(expected)
92
+ elif operator == "not":
93
+ return not bool(actual)
94
+ else:
95
+ raise ValueError(f"不支持的比较运算符: {operator}")
96
+
97
+
98
+ @keyword_manager.register('断言', [
99
+ {'name': '条件', 'mapping': 'condition', 'description': '断言条件表达式,例如: "${value} == 100" 或 "1 + 1 == 2"'},
100
+ {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息'},
101
+ ])
102
+ def assert_condition(**kwargs):
103
+ """执行表达式断言
104
+
105
+ Args:
106
+ condition: 断言条件表达式
107
+ message: 断言失败时的错误消息
108
+
109
+ Returns:
110
+ 断言结果 (True/False)
111
+
112
+ Raises:
113
+ AssertionError: 如果断言失败
114
+ """
115
+ condition = kwargs.get('condition')
116
+ message = kwargs.get('message', '断言失败')
117
+ context = kwargs.get('context')
118
+
119
+ # 简单解析表达式,支持 ==, !=, >, <, >=, <=, contains, not_contains, matches, in, and, or, not
120
+ # 格式: "left_value operator right_value" 或 "boolean_expression"
121
+ operators = ["==", "!=", ">", "<", ">=", "<=", "contains", "not_contains", "matches", "in", "and", "or", "not"]
122
+
123
+ # 先检查是否包含这些操作符
124
+ operator_used = None
125
+ for op in operators:
126
+ if f" {op} " in condition:
127
+ operator_used = op
128
+ break
129
+
130
+ if not operator_used:
131
+ # 如果没有找到操作符,尝试作为布尔表达式直接求值
132
+ try:
133
+ # 对条件进行变量替换
134
+ if '${' in condition:
135
+ condition = context.executor.variable_replacer.replace_in_string(condition)
136
+ # 尝试直接求值
137
+ result = eval(condition)
138
+ if not isinstance(result, bool):
139
+ raise ValueError(f"表达式结果不是布尔值: {result}")
140
+ if not result:
141
+ raise AssertionError(f"{message}. 布尔表达式求值为假: {condition}")
142
+ return True
143
+ except Exception as e:
144
+ raise AssertionError(f"{message}. 无法解析条件表达式: {condition}. 错误: {str(e)}")
145
+
146
+ # 解析左值和右值
147
+ left_value, right_value = condition.split(f" {operator_used} ", 1)
148
+ left_value = left_value.strip()
149
+ right_value = right_value.strip()
150
+
151
+ # 移除引号(如果有)
152
+ if left_value.startswith('"') and left_value.endswith('"'):
153
+ left_value = left_value[1:-1]
154
+ elif left_value.startswith("'") and left_value.endswith("'"):
155
+ left_value = left_value[1:-1]
156
+
157
+ if right_value.startswith('"') and right_value.endswith('"'):
158
+ right_value = right_value[1:-1]
159
+ elif right_value.startswith("'") and right_value.endswith("'"):
160
+ right_value = right_value[1:-1]
161
+
162
+ # 记录原始值(用于调试)
163
+ allure.attach(
164
+ f"原始左值: {left_value}\n原始右值: {right_value}\n操作符: {operator_used}",
165
+ name="表达式解析",
166
+ attachment_type=allure.attachment_type.TEXT
167
+ )
168
+
169
+ # 对左值进行变量替换和表达式计算
170
+ try:
171
+ # 如果左值包含变量引用,先进行变量替换
172
+ if '${' in left_value:
173
+ left_value = context.executor.variable_replacer.replace_in_string(left_value)
174
+
175
+ # 检查是否需要计算表达式
176
+ if any(op in str(left_value) for op in ['+', '-', '*', '/', '%', '(', ')']):
177
+ try:
178
+ # 确保数字类型的变量可以参与计算
179
+ if isinstance(left_value, (int, float)):
180
+ left_value = str(left_value)
181
+ # 尝试计算表达式
182
+ left_value = eval(str(left_value))
183
+ except Exception as e:
184
+ allure.attach(
185
+ f"表达式计算错误: {str(e)}\n表达式: {left_value}",
186
+ name="表达式计算错误",
187
+ attachment_type=allure.attachment_type.TEXT
188
+ )
189
+ raise ValueError(f"表达式计算错误: {str(e)}")
190
+
191
+ # 处理布尔值字符串和数字字符串
192
+ if isinstance(left_value, str):
193
+ if left_value.lower() in ('true', 'false'):
194
+ left_value = left_value.lower() == 'true'
195
+ elif left_value.lower() in ('yes', 'no', '1', '0', 't', 'f', 'y', 'n'):
196
+ left_value = left_value.lower() in ('yes', '1', 't', 'y')
197
+ else:
198
+ # 尝试转换为数字
199
+ try:
200
+ if '.' in left_value:
201
+ left_value = float(left_value)
202
+ else:
203
+ left_value = int(left_value)
204
+ except ValueError:
205
+ pass # 如果不是数字,保持原样
206
+ except Exception as e:
207
+ allure.attach(
208
+ f"左值处理异常: {str(e)}\n左值: {left_value}",
209
+ name="左值处理异常",
210
+ attachment_type=allure.attachment_type.TEXT
211
+ )
212
+ raise
213
+
214
+ # 对右值进行变量替换和表达式计算
215
+ try:
216
+ # 如果右值包含变量引用,先进行变量替换
217
+ if '${' in right_value:
218
+ right_value = context.executor.variable_replacer.replace_in_string(right_value)
219
+
220
+ # 检查是否需要计算表达式
221
+ if any(op in str(right_value) for op in ['+', '-', '*', '/', '%', '(', ')']):
222
+ try:
223
+ # 确保数字类型的变量可以参与计算
224
+ if isinstance(right_value, (int, float)):
225
+ right_value = str(right_value)
226
+ # 尝试计算表达式
227
+ right_value = eval(str(right_value))
228
+ except Exception as e:
229
+ allure.attach(
230
+ f"表达式计算错误: {str(e)}\n表达式: {right_value}",
231
+ name="表达式计算错误",
232
+ attachment_type=allure.attachment_type.TEXT
233
+ )
234
+ raise ValueError(f"表达式计算错误: {str(e)}")
235
+
236
+ # 处理布尔值字符串
237
+ if isinstance(right_value, str):
238
+ if right_value.lower() in ('true', 'false'):
239
+ right_value = right_value.lower() == 'true'
240
+ elif right_value.lower() in ('yes', 'no', '1', '0', 't', 'f', 'y', 'n'):
241
+ right_value = right_value.lower() in ('yes', '1', 't', 'y')
242
+ except Exception as e:
243
+ allure.attach(
244
+ f"右值处理异常: {str(e)}\n右值: {right_value}",
245
+ name="右值处理异常",
246
+ attachment_type=allure.attachment_type.TEXT
247
+ )
248
+ raise
249
+
250
+ # 类型转换和特殊处理
251
+ if operator_used == "contains":
252
+ # 特殊处理字符串包含操作
253
+ if isinstance(left_value, str) and isinstance(right_value, str):
254
+ result = right_value in left_value
255
+ elif isinstance(left_value, (list, tuple, dict)):
256
+ result = right_value in left_value
257
+ elif isinstance(left_value, (int, float, bool)):
258
+ # 将左值转换为字符串进行比较
259
+ result = str(right_value) in str(left_value)
260
+ else:
261
+ result = False
262
+ elif operator_used == "not_contains":
263
+ # 特殊处理字符串不包含操作
264
+ if isinstance(left_value, str) and isinstance(right_value, str):
265
+ result = right_value not in left_value
266
+ elif isinstance(left_value, (list, tuple, dict)):
267
+ result = right_value not in left_value
268
+ elif isinstance(left_value, (int, float, bool)):
269
+ # 将左值转换为字符串进行比较
270
+ result = str(right_value) not in str(left_value)
271
+ else:
272
+ result = True
273
+ elif operator_used == "matches":
274
+ # 特殊处理正则表达式匹配
275
+ try:
276
+ if isinstance(left_value, str) and isinstance(right_value, str):
277
+ result = bool(re.match(right_value, left_value))
278
+ else:
279
+ result = False
280
+ except re.error:
281
+ raise ValueError(f"无效的正则表达式: {right_value}")
282
+ elif operator_used == "in":
283
+ # 特殊处理 in 操作符
284
+ try:
285
+ # 尝试将右值解析为列表或字典
286
+ if isinstance(right_value, str):
287
+ right_value = eval(right_value)
288
+
289
+ # 如果是字典,检查键
290
+ if isinstance(right_value, dict):
291
+ result = left_value in right_value.keys()
292
+ else:
293
+ result = left_value in right_value
294
+ except Exception as e:
295
+ allure.attach(
296
+ f"in 操作符处理异常: {str(e)}\n左值: {left_value}\n右值: {right_value}",
297
+ name="in 操作符处理异常",
298
+ attachment_type=allure.attachment_type.TEXT
299
+ )
300
+ raise ValueError(f"in 操作符处理异常: {str(e)}")
301
+ else:
302
+ # 其他操作符需要类型转换
303
+ if isinstance(left_value, str) and isinstance(right_value, (int, float)):
304
+ try:
305
+ left_value = float(left_value)
306
+ except:
307
+ pass
308
+ elif isinstance(right_value, str) and isinstance(left_value, (int, float)):
309
+ try:
310
+ right_value = float(right_value)
311
+ except:
312
+ pass
313
+
314
+ # 记录类型转换后的值(用于调试)
315
+ allure.attach(
316
+ f"类型转换后左值: {left_value} ({type(left_value).__name__})\n类型转换后右值: {right_value} ({type(right_value).__name__})",
317
+ name="类型转换",
318
+ attachment_type=allure.attachment_type.TEXT
319
+ )
320
+
321
+ # 执行比较
322
+ result = _compare_values(left_value, right_value, operator_used)
323
+
324
+ # 记录和处理断言结果
325
+ if not result:
326
+ error_details = f"""
327
+ 断言失败详情:
328
+ 条件: {condition}
329
+ 实际值: {left_value} ({type(left_value).__name__})
330
+ 预期值: {right_value} ({type(right_value).__name__})
331
+ 操作符: {operator_used}
332
+ 消息: {message}
333
+ """
334
+ allure.attach(
335
+ error_details,
336
+ name="断言失败详情",
337
+ attachment_type=allure.attachment_type.TEXT
338
+ )
339
+ raise AssertionError(error_details)
340
+
341
+ # 记录成功的断言
342
+ allure.attach(
343
+ f"实际值: {left_value}\n预期值: {right_value}\n操作符: {operator_used}",
344
+ name="断言成功",
345
+ attachment_type=allure.attachment_type.TEXT
346
+ )
347
+ return True
348
+
349
+
350
+ @keyword_manager.register('JSON断言', [
351
+ {'name': 'JSON数据', 'mapping': 'json_data', 'description': 'JSON数据(字符串或对象)'},
352
+ {'name': 'JSONPath', 'mapping': 'jsonpath', 'description': 'JSONPath表达式'},
353
+ {'name': '预期值', 'mapping': 'expected_value', 'description': '预期的值'},
354
+ {'name': '操作符', 'mapping': 'operator', 'description': '比较操作符,默认为"=="'},
355
+ {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息'},
356
+ ])
357
+ def assert_json(**kwargs):
358
+ """执行JSON断言
359
+
360
+ Args:
361
+ json_data: JSON数据(字符串或对象)
362
+ jsonpath: JSONPath表达式
363
+ expected_value: 预期的值
364
+ operator: 比较操作符,默认为"=="
365
+ message: 断言失败时的错误消息
366
+
367
+ Returns:
368
+ 断言结果 (True/False)
369
+
370
+ Raises:
371
+ AssertionError: 如果断言失败
372
+ ValueError: 如果JSONPath无效或找不到匹配项
373
+ """
374
+ json_data = kwargs.get('json_data')
375
+ path = kwargs.get('jsonpath')
376
+ expected_value = kwargs.get('expected_value')
377
+ operator = kwargs.get('operator', '==')
378
+ message = kwargs.get('message', 'JSON断言失败')
379
+
380
+ # 解析JSON(如果需要)
381
+ if isinstance(json_data, str):
382
+ try:
383
+ json_data = json.loads(json_data)
384
+ except json.JSONDecodeError as e:
385
+ raise ValueError(f"无效的JSON数据: {str(e)}")
386
+
387
+ # 使用JSONPath提取值
388
+ actual_value = _extract_jsonpath(json_data, path)
389
+
390
+ # 记录提取的值
391
+ allure.attach(
392
+ f"JSONPath: {path}\n提取值: {actual_value}",
393
+ name="JSONPath提取结果",
394
+ attachment_type=allure.attachment_type.TEXT
395
+ )
396
+
397
+ # 比较值
398
+ result = _compare_values(actual_value, expected_value, operator)
399
+
400
+ # 记录和处理断言结果
401
+ if not result:
402
+ allure.attach(
403
+ f"实际值: {actual_value}\n预期值: {expected_value}\n操作符: {operator}",
404
+ name="JSON断言失败",
405
+ attachment_type=allure.attachment_type.TEXT
406
+ )
407
+ raise AssertionError(message)
408
+
409
+ # 记录成功的断言
410
+ allure.attach(
411
+ f"实际值: {actual_value}\n预期值: {expected_value}\n操作符: {operator}",
412
+ name="JSON断言成功",
413
+ attachment_type=allure.attachment_type.TEXT
414
+ )
415
+ return True
416
+
417
+
418
+ @keyword_manager.register('JSON提取', [
419
+ {'name': 'JSON数据', 'mapping': 'json_data', 'description': 'JSON数据(字符串或对象)'},
420
+ {'name': 'JSONPath', 'mapping': 'jsonpath', 'description': 'JSONPath表达式'},
421
+ {'name': '变量名', 'mapping': 'variable', 'description': '存储提取值的变量名'},
422
+ ])
423
+ def extract_json(**kwargs):
424
+ """从JSON数据中提取值并保存到变量
425
+
426
+ Args:
427
+ json_data: JSON数据(字符串或对象)
428
+ jsonpath: JSONPath表达式
429
+ variable: 存储提取值的变量名
430
+
431
+ Returns:
432
+ 提取的值
433
+
434
+ Raises:
435
+ ValueError: 如果JSONPath无效或找不到匹配项
436
+ """
437
+ json_data = kwargs.get('json_data')
438
+ path = kwargs.get('jsonpath')
439
+ variable = kwargs.get('variable')
440
+
441
+ # 解析JSON(如果需要)
442
+ if isinstance(json_data, str):
443
+ try:
444
+ json_data = json.loads(json_data)
445
+ except json.JSONDecodeError as e:
446
+ raise ValueError(f"无效的JSON数据: {str(e)}")
447
+
448
+ # 使用JSONPath提取值
449
+ value = _extract_jsonpath(json_data, path)
450
+
451
+ # 记录提取的值
452
+ allure.attach(
453
+ f"JSONPath: {path}\n提取值: {value}\n保存到变量: {variable}",
454
+ name="JSON数据提取",
455
+ attachment_type=allure.attachment_type.TEXT
456
+ )
457
+
458
+ # 返回提取的值用于变量赋值
459
+ return value
460
+
461
+
462
+ @keyword_manager.register('类型断言', [
463
+ {'name': '值', 'mapping': 'value', 'description': '要检查的值'},
464
+ {'name': '类型', 'mapping': 'type', 'description': '预期的类型 (string, number, boolean, list, object, null)'},
465
+ {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息'},
466
+ ])
467
+ def assert_type(**kwargs):
468
+ """断言值的类型
469
+
470
+ Args:
471
+ value: 要检查的值
472
+ type: 预期的类型 (string, number, boolean, list, object, null)
473
+ message: 断言失败时的错误消息
474
+
475
+ Returns:
476
+ 断言结果 (True/False)
477
+
478
+ Raises:
479
+ AssertionError: 如果断言失败
480
+ """
481
+ value = kwargs.get('value')
482
+ expected_type = kwargs.get('type')
483
+ message = kwargs.get('message', '类型断言失败')
484
+
485
+ # 检查类型
486
+ if expected_type == 'string':
487
+ result = isinstance(value, str)
488
+ elif expected_type == 'number':
489
+ result = isinstance(value, (int, float))
490
+ # 如果是字符串,尝试转换为数字
491
+ if not result and isinstance(value, str):
492
+ try:
493
+ float(value) # 尝试转换为数字
494
+ result = True
495
+ except ValueError:
496
+ pass
497
+ elif expected_type == 'boolean':
498
+ result = isinstance(value, bool)
499
+ # 如果是字符串,检查是否是布尔值字符串
500
+ if not result and isinstance(value, str):
501
+ value_lower = value.lower()
502
+ result = value_lower in ['true', 'false']
503
+ elif expected_type == 'list':
504
+ result = isinstance(value, list)
505
+ elif expected_type == 'object':
506
+ result = isinstance(value, dict)
507
+ elif expected_type == 'null':
508
+ result = value is None
509
+ else:
510
+ raise ValueError(f"不支持的类型: {expected_type}")
511
+
512
+ # 记录和处理断言结果
513
+ if not result:
514
+ actual_type = type(value).__name__
515
+ allure.attach(
516
+ f"值: {value}\n实际类型: {actual_type}\n预期类型: {expected_type}",
517
+ name="类型断言失败",
518
+ attachment_type=allure.attachment_type.TEXT
519
+ )
520
+ raise AssertionError(message)
521
+
522
+ # 记录成功的断言
523
+ allure.attach(
524
+ f"值: {value}\n类型: {expected_type}",
525
+ name="类型断言成功",
526
+ attachment_type=allure.attachment_type.TEXT
527
+ )
528
+ return True
529
+
530
+
531
+ @keyword_manager.register('数据比较', [
532
+ {'name': '实际值', 'mapping': 'actual', 'description': '实际值'},
533
+ {'name': '预期值', 'mapping': 'expected', 'description': '预期值'},
534
+ {'name': '操作符', 'mapping': 'operator', 'description': '比较操作符,默认为"=="'},
535
+ {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息'},
536
+ ])
537
+ def compare_values(**kwargs):
538
+ """比较两个值
539
+
540
+ Args:
541
+ actual: 实际值
542
+ expected: 预期值
543
+ operator: 比较操作符,默认为"=="
544
+ message: 断言失败时的错误消息
545
+
546
+ Returns:
547
+ 比较结果 (True/False)
548
+
549
+ Raises:
550
+ AssertionError: 如果比较失败
551
+ """
552
+ actual = kwargs.get('actual')
553
+ expected = kwargs.get('expected')
554
+ operator = kwargs.get('operator', '==')
555
+ message = kwargs.get('message', '数据比较失败')
556
+
557
+ # 处理布尔值字符串和表达式
558
+ if isinstance(actual, str):
559
+ # 检查是否需要计算表达式
560
+ if any(op in actual for op in ['+', '-', '*', '/', '%', '(', ')']):
561
+ try:
562
+ actual = eval(actual)
563
+ except Exception as e:
564
+ allure.attach(
565
+ f"表达式计算错误: {str(e)}\n表达式: {actual}",
566
+ name="表达式计算错误",
567
+ attachment_type=allure.attachment_type.TEXT
568
+ )
569
+ raise ValueError(f"表达式计算错误: {str(e)}")
570
+ elif actual.lower() in ('true', 'false'):
571
+ actual = actual.lower() == 'true'
572
+ elif actual.lower() in ('yes', 'no', '1', '0', 't', 'f', 'y', 'n'):
573
+ actual = actual.lower() in ('yes', '1', 't', 'y')
574
+
575
+ if isinstance(expected, str):
576
+ # 检查是否需要计算表达式
577
+ if any(op in expected for op in ['+', '-', '*', '/', '%', '(', ')']):
578
+ try:
579
+ expected = eval(expected)
580
+ except Exception as e:
581
+ allure.attach(
582
+ f"表达式计算错误: {str(e)}\n表达式: {expected}",
583
+ name="表达式计算错误",
584
+ attachment_type=allure.attachment_type.TEXT
585
+ )
586
+ raise ValueError(f"表达式计算错误: {str(e)}")
587
+ elif expected.lower() in ('true', 'false'):
588
+ expected = expected.lower() == 'true'
589
+ elif expected.lower() in ('yes', 'no', '1', '0', 't', 'f', 'y', 'n'):
590
+ expected = expected.lower() in ('yes', '1', 't', 'y')
591
+
592
+ # 比较值
593
+ result = _compare_values(actual, expected, operator)
594
+
595
+ # 记录和处理比较结果
596
+ if not result:
597
+ allure.attach(
598
+ f"实际值: {actual}\n预期值: {expected}\n操作符: {operator}",
599
+ name="数据比较失败",
600
+ attachment_type=allure.attachment_type.TEXT
601
+ )
602
+ raise AssertionError(message)
603
+
604
+ # 记录成功的比较
605
+ allure.attach(
606
+ f"实际值: {actual}\n预期值: {expected}\n操作符: {operator}",
607
+ name="数据比较成功",
608
+ attachment_type=allure.attachment_type.TEXT
609
+ )
610
+ return result
@@ -0,0 +1,51 @@
1
+ from pytest_dsl.core.keyword_manager import keyword_manager
2
+ from pytest_dsl.core.global_context import global_context
3
+
4
+
5
+ @keyword_manager.register(
6
+ name="设置全局变量",
7
+ parameters=[
8
+ {"name": "变量名", "mapping": "name", "description": "全局变量的名称"},
9
+ {"name": "值", "mapping": "value", "description": "全局变量的值"}
10
+ ]
11
+ )
12
+ def set_global_variable(name, value, context):
13
+ """设置全局变量"""
14
+ global_context.set_variable(name, value)
15
+ return value
16
+
17
+
18
+ @keyword_manager.register(
19
+ name="获取全局变量",
20
+ parameters=[
21
+ {"name": "变量名", "mapping": "name", "description": "全局变量的名称"}
22
+ ]
23
+ )
24
+ def get_global_variable(name, context):
25
+ """获取全局变量"""
26
+ value = global_context.get_variable(name)
27
+ if value is None:
28
+ raise Exception(f"全局变量未定义: {name}")
29
+ return value
30
+
31
+
32
+ @keyword_manager.register(
33
+ name="删除全局变量",
34
+ parameters=[
35
+ {"name": "变量名", "mapping": "name", "description": "全局变量的名称"}
36
+ ]
37
+ )
38
+ def delete_global_variable(name, context):
39
+ """删除全局变量"""
40
+ global_context.delete_variable(name)
41
+ return True
42
+
43
+
44
+ @keyword_manager.register(
45
+ name="清除所有全局变量",
46
+ parameters=[]
47
+ )
48
+ def clear_all_global_variables(context):
49
+ """清除所有全局变量"""
50
+ global_context.clear_all()
51
+ return True