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.
- pytest_dsl/__init__.py +10 -0
- pytest_dsl/cli.py +44 -0
- pytest_dsl/conftest_adapter.py +4 -0
- pytest_dsl/core/__init__.py +0 -0
- pytest_dsl/core/auth_provider.py +409 -0
- pytest_dsl/core/auto_decorator.py +181 -0
- pytest_dsl/core/auto_directory.py +81 -0
- pytest_dsl/core/context.py +23 -0
- pytest_dsl/core/custom_auth_example.py +425 -0
- pytest_dsl/core/dsl_executor.py +329 -0
- pytest_dsl/core/dsl_executor_utils.py +84 -0
- pytest_dsl/core/global_context.py +103 -0
- pytest_dsl/core/http_client.py +411 -0
- pytest_dsl/core/http_request.py +810 -0
- pytest_dsl/core/keyword_manager.py +109 -0
- pytest_dsl/core/lexer.py +139 -0
- pytest_dsl/core/parser.py +197 -0
- pytest_dsl/core/parsetab.py +76 -0
- pytest_dsl/core/plugin_discovery.py +187 -0
- pytest_dsl/core/utils.py +146 -0
- pytest_dsl/core/variable_utils.py +267 -0
- pytest_dsl/core/yaml_loader.py +62 -0
- pytest_dsl/core/yaml_vars.py +75 -0
- pytest_dsl/docs/custom_keywords.md +140 -0
- pytest_dsl/examples/__init__.py +5 -0
- pytest_dsl/examples/assert/assertion_example.auto +44 -0
- pytest_dsl/examples/assert/boolean_test.auto +34 -0
- pytest_dsl/examples/assert/expression_test.auto +49 -0
- pytest_dsl/examples/http/__init__.py +3 -0
- pytest_dsl/examples/http/builtin_auth_test.auto +79 -0
- pytest_dsl/examples/http/csrf_auth_test.auto +64 -0
- pytest_dsl/examples/http/custom_auth_test.auto +76 -0
- pytest_dsl/examples/http/file_reference_test.auto +111 -0
- pytest_dsl/examples/http/http_advanced.auto +91 -0
- pytest_dsl/examples/http/http_example.auto +147 -0
- pytest_dsl/examples/http/http_length_test.auto +55 -0
- pytest_dsl/examples/http/http_retry_assertions.auto +91 -0
- pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +94 -0
- pytest_dsl/examples/http/http_with_yaml.auto +58 -0
- pytest_dsl/examples/http/new_retry_test.auto +22 -0
- pytest_dsl/examples/http/retry_assertions_only.auto +52 -0
- pytest_dsl/examples/http/retry_config_only.auto +49 -0
- pytest_dsl/examples/http/retry_debug.auto +22 -0
- pytest_dsl/examples/http/retry_with_fix.auto +21 -0
- pytest_dsl/examples/http/simple_retry.auto +20 -0
- pytest_dsl/examples/http/vars.yaml +55 -0
- pytest_dsl/examples/http_clients.yaml +48 -0
- pytest_dsl/examples/keyword_example.py +70 -0
- pytest_dsl/examples/test_assert.py +16 -0
- pytest_dsl/examples/test_http.py +168 -0
- pytest_dsl/keywords/__init__.py +10 -0
- pytest_dsl/keywords/assertion_keywords.py +610 -0
- pytest_dsl/keywords/global_keywords.py +51 -0
- pytest_dsl/keywords/http_keywords.py +430 -0
- pytest_dsl/keywords/system_keywords.py +17 -0
- pytest_dsl/main_adapter.py +7 -0
- pytest_dsl/plugin.py +44 -0
- pytest_dsl-0.1.0.dist-info/METADATA +537 -0
- pytest_dsl-0.1.0.dist-info/RECORD +63 -0
- pytest_dsl-0.1.0.dist-info/WHEEL +5 -0
- pytest_dsl-0.1.0.dist-info/entry_points.txt +5 -0
- pytest_dsl-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|