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,329 @@
1
+ import re
2
+ import allure
3
+ import csv
4
+ import os
5
+ import pytest
6
+ from pytest_dsl.core.lexer import get_lexer
7
+ from pytest_dsl.core.parser import get_parser, Node
8
+ from pytest_dsl.core.keyword_manager import keyword_manager
9
+ from pytest_dsl.core.global_context import global_context
10
+ from pytest_dsl.core.context import TestContext
11
+ import pytest_dsl.keywords
12
+ from pytest_dsl.core.yaml_vars import yaml_vars
13
+ from pytest_dsl.core.variable_utils import VariableReplacer
14
+
15
+
16
+ class DSLExecutor:
17
+ """DSL执行器,负责执行解析后的AST
18
+
19
+ 环境变量控制:
20
+ - PYTEST_DSL_KEEP_VARIABLES=1: 执行完成后保留变量,用于单元测试中检查变量值
21
+ - PYTEST_DSL_KEEP_VARIABLES=0: (默认) 执行完成后清空变量,用于正常DSL执行
22
+ """
23
+ def __init__(self):
24
+ """初始化DSL执行器"""
25
+ self.variables = {}
26
+ self.test_context = TestContext()
27
+ self.test_context.executor = self # 让 test_context 能够访问到 executor
28
+ self.variable_replacer = VariableReplacer(self.variables, self.test_context)
29
+
30
+ def set_current_data(self, data):
31
+ """设置当前测试数据集"""
32
+ if data:
33
+ self.variables.update(data)
34
+ # 同时将数据添加到测试上下文
35
+ for key, value in data.items():
36
+ self.test_context.set(key, value)
37
+
38
+ def _load_test_data(self, data_source):
39
+ """加载测试数据
40
+
41
+ :param data_source: 数据源配置,包含 file 和 format 字段
42
+ :return: 包含测试数据的列表
43
+ """
44
+ if not data_source:
45
+ return [{}] # 如果没有数据源,返回一个空的数据集
46
+
47
+ file_path = data_source['file']
48
+ format_type = data_source['format']
49
+
50
+ if not os.path.exists(file_path):
51
+ raise Exception(f"数据文件不存在: {file_path}")
52
+
53
+ if format_type.lower() == 'csv':
54
+ return self._load_csv_data(file_path)
55
+ else:
56
+ raise Exception(f"不支持的数据格式: {format_type}")
57
+
58
+ def _load_csv_data(self, file_path):
59
+ """加载CSV格式的测试数据
60
+
61
+ :param file_path: CSV文件路径
62
+ :return: 包含测试数据的列表
63
+ """
64
+ data_sets = []
65
+ with open(file_path, 'r', encoding='utf-8') as f:
66
+ reader = csv.DictReader(f)
67
+ for row in reader:
68
+ data_sets.append(row)
69
+ return data_sets
70
+
71
+ def eval_expression(self, expr_node):
72
+ """
73
+ 对表达式节点进行求值,返回表达式的值。
74
+
75
+ :param expr_node: AST中的表达式节点
76
+ :return: 表达式求值后的结果
77
+ :raises Exception: 当遇到未定义变量或无法求值的类型时抛出异常
78
+ """
79
+ if expr_node.type == 'Expression':
80
+ value = self._eval_expression_value(expr_node.value)
81
+ # 统一处理变量替换
82
+ return self.variable_replacer.replace_in_value(value)
83
+ elif expr_node.type == 'KeywordCall':
84
+ return self.execute(expr_node)
85
+ elif expr_node.type == 'ListExpr':
86
+ # 处理列表表达式
87
+ result = []
88
+ for item in expr_node.children:
89
+ item_value = self.eval_expression(item)
90
+ result.append(item_value)
91
+ return result
92
+ elif expr_node.type == 'BooleanExpr':
93
+ # 处理布尔值表达式
94
+ return expr_node.value
95
+ else:
96
+ raise Exception(f"无法求值的表达式类型: {expr_node.type}")
97
+
98
+ def _eval_expression_value(self, value):
99
+ """处理表达式值的具体逻辑"""
100
+ if isinstance(value, Node):
101
+ return self.eval_expression(value)
102
+ elif isinstance(value, str):
103
+ # 定义变量引用模式
104
+ pattern = r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
105
+ # 检查整个字符串是否完全匹配单一变量引用模式
106
+ match = re.fullmatch(pattern, value)
107
+ if match:
108
+ var_name = match.group(1)
109
+ return self.variable_replacer.get_variable(var_name)
110
+ else:
111
+ # 如果不是单一变量,则替换字符串中的所有变量引用
112
+ return self.variable_replacer.replace_in_string(value)
113
+ return value
114
+
115
+ def _get_variable(self, var_name):
116
+ """获取变量值,优先从本地变量获取,如果不存在则尝试从全局上下文获取"""
117
+ return self.variable_replacer.get_variable(var_name)
118
+
119
+ def _replace_variables_in_string(self, value):
120
+ """替换字符串中的变量引用"""
121
+ return self.variable_replacer.replace_in_string(value)
122
+
123
+ def _handle_start(self, node):
124
+ """处理开始节点"""
125
+ try:
126
+ # 清空上下文,确保每个测试用例都有一个新的上下文
127
+ self.test_context.clear()
128
+ metadata = {}
129
+ teardown_node = None
130
+
131
+ # 先处理元数据和找到teardown节点
132
+ for child in node.children:
133
+ if child.type == 'Metadata':
134
+ for item in child.children:
135
+ metadata[item.type] = item.value
136
+ elif child.type == 'Teardown':
137
+ teardown_node = child
138
+
139
+ # 执行测试
140
+ self._execute_test_iteration(metadata, node, teardown_node)
141
+
142
+ except Exception as e:
143
+ # 如果是断言错误,直接抛出
144
+ if isinstance(e, AssertionError):
145
+ raise
146
+ # 如果是语法错误,记录并抛出
147
+ if "语法错误" in str(e):
148
+ print(f"DSL语法错误: {str(e)}")
149
+ raise
150
+ # 其他错误,记录并抛出
151
+ print(f"测试执行错误: {str(e)}")
152
+ raise
153
+ finally:
154
+ # 测试用例执行完成后清空上下文
155
+ self.test_context.clear()
156
+
157
+ def _execute_test_iteration(self, metadata, node, teardown_node):
158
+ """执行测试迭代"""
159
+ try:
160
+ # 设置 Allure 报告信息
161
+ if '@name' in metadata:
162
+ test_name = metadata['@name']
163
+ allure.dynamic.title(test_name)
164
+ if '@description' in metadata:
165
+ description = metadata['@description']
166
+ allure.dynamic.description(description)
167
+ if '@tags' in metadata:
168
+ for tag in metadata['@tags']:
169
+ allure.dynamic.tag(tag.value)
170
+
171
+ # 执行所有非teardown节点
172
+ for child in node.children:
173
+ if child.type != 'Teardown' and child.type != 'Metadata':
174
+ self.execute(child)
175
+
176
+ # 执行teardown
177
+ if teardown_node:
178
+ with allure.step("执行清理操作"):
179
+ try:
180
+ self.execute(teardown_node)
181
+ except Exception as e:
182
+ allure.attach(
183
+ f"清理失败: {str(e)}",
184
+ name="清理失败",
185
+ attachment_type=allure.attachment_type.TEXT
186
+ )
187
+ finally:
188
+ # 使用环境变量控制是否清空变量
189
+ # 当 PYTEST_DSL_KEEP_VARIABLES=1 时,保留变量(用于单元测试)
190
+ # 否则清空变量(用于正常DSL执行)
191
+ import os
192
+ keep_variables = os.environ.get('PYTEST_DSL_KEEP_VARIABLES', '0') == '1'
193
+
194
+ if not keep_variables:
195
+ self.variables.clear()
196
+ # 同时清空测试上下文
197
+ self.test_context.clear()
198
+
199
+ def _handle_statements(self, node):
200
+ """处理语句列表"""
201
+ for stmt in node.children:
202
+ self.execute(stmt)
203
+
204
+ @allure.step("变量赋值")
205
+ def _handle_assignment(self, node):
206
+ """处理赋值语句"""
207
+ var_name = node.value
208
+ expr_value = self.eval_expression(node.children[0])
209
+
210
+ # 检查变量名是否以g_开头,如果是则设置为全局变量
211
+ if var_name.startswith('g_'):
212
+ global_context.set_variable(var_name, expr_value)
213
+ allure.attach(
214
+ f"全局变量: {var_name}\n值: {expr_value}",
215
+ name="全局变量赋值",
216
+ attachment_type=allure.attachment_type.TEXT
217
+ )
218
+ else:
219
+ # 存储在本地变量字典和测试上下文中
220
+ self.variable_replacer.local_variables[var_name] = expr_value
221
+ self.test_context.set(var_name, expr_value) # 同时添加到测试上下文
222
+ allure.attach(
223
+ f"变量: {var_name}\n值: {expr_value}",
224
+ name="赋值详情",
225
+ attachment_type=allure.attachment_type.TEXT
226
+ )
227
+
228
+ @allure.step("关键字调用赋值")
229
+ def _handle_assignment_keyword_call(self, node):
230
+ """处理关键字调用赋值"""
231
+ var_name = node.value
232
+ keyword_call_node = node.children[0]
233
+ result = self.execute(keyword_call_node)
234
+
235
+ if result is not None:
236
+ # 检查变量名是否以g_开头,如果是则设置为全局变量
237
+ if var_name.startswith('g_'):
238
+ global_context.set_variable(var_name, result)
239
+ allure.attach(
240
+ f"全局变量: {var_name}\n值: {result}",
241
+ name="全局变量赋值",
242
+ attachment_type=allure.attachment_type.TEXT
243
+ )
244
+ else:
245
+ # 存储在本地变量字典和测试上下文中
246
+ self.variable_replacer.local_variables[var_name] = result
247
+ self.test_context.set(var_name, result) # 同时添加到测试上下文
248
+ allure.attach(
249
+ f"变量: {var_name}\n值: {result}",
250
+ name="赋值详情",
251
+ attachment_type=allure.attachment_type.TEXT
252
+ )
253
+ else:
254
+ raise Exception(f"关键字 {keyword_call_node.value} 没有返回结果")
255
+
256
+ @allure.step("执行循环")
257
+ def _handle_for_loop(self, node):
258
+ """处理for循环"""
259
+ var_name = node.value
260
+ start = self.eval_expression(node.children[0])
261
+ end = self.eval_expression(node.children[1])
262
+
263
+ for i in range(int(start), int(end)):
264
+ # 存储在本地变量字典和测试上下文中
265
+ self.variable_replacer.local_variables[var_name] = i
266
+ self.test_context.set(var_name, i) # 同时添加到测试上下文
267
+ with allure.step(f"循环轮次: {var_name} = {i}"):
268
+ self.execute(node.children[2])
269
+
270
+ def _execute_keyword_call(self, node):
271
+ """执行关键字调用"""
272
+ keyword_name = node.value
273
+ keyword_info = keyword_manager.get_keyword_info(keyword_name)
274
+ if not keyword_info:
275
+ raise Exception(f"未注册的关键字: {keyword_name}")
276
+
277
+ kwargs = self._prepare_keyword_params(node, keyword_info)
278
+
279
+ try:
280
+ # 由于KeywordManager中的wrapper已经添加了allure.step和日志,这里不再重复添加
281
+ result = keyword_manager.execute(keyword_name, **kwargs)
282
+ return result
283
+ except Exception as e:
284
+ # 异常会在KeywordManager的wrapper中记录,这里只需要向上抛出
285
+ raise
286
+
287
+ def _prepare_keyword_params(self, node, keyword_info):
288
+ """准备关键字调用参数"""
289
+ mapping = keyword_info.get('mapping', {})
290
+ kwargs = {'context': self.test_context} # 默认传入context参数
291
+
292
+ # 检查是否有参数列表
293
+ if node.children[0]:
294
+ for param in node.children[0]:
295
+ param_name = param.value
296
+ english_param_name = mapping.get(param_name, param_name)
297
+ # 对参数值进行变量替换
298
+ param_value = self.eval_expression(param.children[0])
299
+ kwargs[english_param_name] = param_value
300
+
301
+ return kwargs
302
+
303
+ @allure.step("执行清理操作")
304
+ def _handle_teardown(self, node):
305
+ """处理清理操作"""
306
+ self.execute(node.children[0])
307
+
308
+ def execute(self, node):
309
+ """执行AST节点"""
310
+ handlers = {
311
+ 'Start': self._handle_start,
312
+ 'Metadata': lambda _: None,
313
+ 'Statements': self._handle_statements,
314
+ 'Assignment': self._handle_assignment,
315
+ 'AssignmentKeywordCall': self._handle_assignment_keyword_call,
316
+ 'ForLoop': self._handle_for_loop,
317
+ 'KeywordCall': self._execute_keyword_call,
318
+ 'Teardown': self._handle_teardown
319
+ }
320
+
321
+ handler = handlers.get(node.type)
322
+ if handler:
323
+ return handler(node)
324
+ raise Exception(f"未知的节点类型: {node.type}")
325
+
326
+ def read_file(filename):
327
+ """读取 DSL 文件内容"""
328
+ with open(filename, 'r', encoding='utf-8') as f:
329
+ return f.read()
@@ -0,0 +1,84 @@
1
+ """DSL执行器工具模块
2
+
3
+ 该模块提供DSL文件的读取和执行功能,作为conftest.py和DSL执行器之间的桥梁。
4
+ """
5
+
6
+ from pathlib import Path
7
+ from pytest_dsl.core.dsl_executor import DSLExecutor
8
+ from pytest_dsl.core.lexer import get_lexer
9
+ from pytest_dsl.core.parser import get_parser
10
+
11
+ # 获取词法分析器和解析器实例
12
+ lexer = get_lexer()
13
+ parser = get_parser()
14
+
15
+
16
+ def read_file(filename):
17
+ """读取DSL文件内容
18
+
19
+ Args:
20
+ filename: 文件路径
21
+
22
+ Returns:
23
+ str: 文件内容
24
+ """
25
+ with open(filename, 'r', encoding='utf-8') as f:
26
+ return f.read()
27
+
28
+
29
+ def execute_dsl_file(filename: str) -> None:
30
+ """执行DSL文件
31
+
32
+ Args:
33
+ filename: DSL文件路径
34
+ """
35
+ try:
36
+ # 读取文件内容
37
+ with open(filename, 'r', encoding='utf-8') as f:
38
+ content = f.read()
39
+
40
+ # 创建词法分析器和语法分析器
41
+ lexer = get_lexer()
42
+ parser = get_parser()
43
+
44
+ # 解析DSL文件
45
+ ast = parser.parse(content, lexer=lexer)
46
+
47
+ # 创建执行器并执行
48
+ executor = DSLExecutor()
49
+ executor.execute(ast)
50
+
51
+ except Exception as e:
52
+ # 如果是语法错误,记录并抛出
53
+ if "语法错误" in str(e):
54
+ print(f"DSL语法错误: {str(e)}")
55
+ raise
56
+ # 其他错误直接抛出
57
+ raise
58
+
59
+
60
+ def extract_metadata_from_ast(ast):
61
+ """从AST中提取元数据
62
+
63
+ 提取DSL文件中的元数据信息,如@data和@name标记。
64
+
65
+ Args:
66
+ ast: 解析后的抽象语法树
67
+
68
+ Returns:
69
+ tuple: (data_source, test_title) 元组,如果不存在则为None
70
+ """
71
+ data_source = None
72
+ test_title = None
73
+
74
+ for child in ast.children:
75
+ if child.type == 'Metadata':
76
+ for item in child.children:
77
+ if item.type == '@data':
78
+ data_source = item.value
79
+ elif item.type == '@name':
80
+ test_title = item.value
81
+ if data_source and test_title:
82
+ break
83
+
84
+ return data_source, test_title
@@ -0,0 +1,103 @@
1
+ import os
2
+ import json
3
+ import tempfile
4
+ import allure
5
+ from typing import Dict, Any, Optional
6
+ from filelock import FileLock
7
+ from .yaml_vars import yaml_vars
8
+
9
+
10
+ class GlobalContext:
11
+ """全局上下文管理器,支持多进程环境下的变量共享"""
12
+
13
+ def __init__(self):
14
+ # 使用临时目录存储全局变量
15
+ self._storage_dir = os.path.join(
16
+ tempfile.gettempdir(), "pytest_dsl_global_vars")
17
+ os.makedirs(self._storage_dir, exist_ok=True)
18
+ self._storage_file = os.path.join(
19
+ self._storage_dir, "global_vars.json")
20
+ self._lock_file = os.path.join(self._storage_dir, "global_vars.lock")
21
+
22
+ def set_variable(self, name: str, value: Any) -> None:
23
+ """设置全局变量"""
24
+ with FileLock(self._lock_file):
25
+ variables = self._load_variables()
26
+ variables[name] = value
27
+ self._save_variables(variables)
28
+
29
+ allure.attach(
30
+ f"全局变量: {name}\n值: {value}",
31
+ name="全局变量设置",
32
+ attachment_type=allure.attachment_type.TEXT
33
+ )
34
+
35
+ def get_variable(self, name: str) -> Any:
36
+ """获取全局变量,优先从YAML变量中获取"""
37
+ # 首先尝试从YAML变量中获取
38
+ yaml_value = yaml_vars.get_variable(name)
39
+ if yaml_value is not None:
40
+ return yaml_value
41
+
42
+ # 如果YAML中没有,则从全局变量存储中获取
43
+ with FileLock(self._lock_file):
44
+ variables = self._load_variables()
45
+ return variables.get(name)
46
+
47
+ def has_variable(self, name: str) -> bool:
48
+ """检查全局变量是否存在(包括YAML变量)"""
49
+ # 首先检查YAML变量
50
+ if yaml_vars.get_variable(name) is not None:
51
+ return True
52
+
53
+ # 然后检查全局变量存储
54
+ with FileLock(self._lock_file):
55
+ variables = self._load_variables()
56
+ return name in variables
57
+
58
+ def delete_variable(self, name: str) -> None:
59
+ """删除全局变量(仅删除存储的变量,不影响YAML变量)"""
60
+ with FileLock(self._lock_file):
61
+ variables = self._load_variables()
62
+ if name in variables:
63
+ del variables[name]
64
+ self._save_variables(variables)
65
+
66
+ allure.attach(
67
+ f"删除全局变量: {name}",
68
+ name="全局变量删除",
69
+ attachment_type=allure.attachment_type.TEXT
70
+ )
71
+
72
+ def clear_all(self) -> None:
73
+ """清除所有全局变量(包括YAML变量)"""
74
+ with FileLock(self._lock_file):
75
+ self._save_variables({})
76
+
77
+ # 清除YAML变量
78
+ yaml_vars.clear()
79
+
80
+ allure.attach(
81
+ "清除所有全局变量",
82
+ name="全局变量清除",
83
+ attachment_type=allure.attachment_type.TEXT
84
+ )
85
+
86
+ def _load_variables(self) -> Dict[str, Any]:
87
+ """从文件加载变量"""
88
+ if not os.path.exists(self._storage_file):
89
+ return {}
90
+ try:
91
+ with open(self._storage_file, 'r', encoding='utf-8') as f:
92
+ return json.load(f)
93
+ except (json.JSONDecodeError, FileNotFoundError):
94
+ return {}
95
+
96
+ def _save_variables(self, variables: Dict[str, Any]) -> None:
97
+ """保存变量到文件"""
98
+ with open(self._storage_file, 'w', encoding='utf-8') as f:
99
+ json.dump(variables, f, ensure_ascii=False, indent=2)
100
+
101
+
102
+ # 创建全局上下文管理器实例
103
+ global_context = GlobalContext()