pytest-dsl 0.1.1__py3-none-any.whl → 0.3.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/cli.py +42 -6
- pytest_dsl/core/__init__.py +7 -0
- pytest_dsl/core/custom_keyword_manager.py +213 -0
- pytest_dsl/core/dsl_executor.py +171 -3
- pytest_dsl/core/http_request.py +163 -54
- pytest_dsl/core/lexer.py +36 -1
- pytest_dsl/core/parser.py +155 -13
- pytest_dsl/core/parsetab.py +82 -49
- pytest_dsl/core/variable_utils.py +1 -1
- pytest_dsl/examples/custom/test_advanced_keywords.auto +31 -0
- pytest_dsl/examples/custom/test_custom_keywords.auto +37 -0
- pytest_dsl/examples/custom/test_default_values.auto +34 -0
- pytest_dsl/examples/http/http_retry_assertions.auto +2 -2
- pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +2 -2
- pytest_dsl/examples/test_custom_keyword.py +9 -0
- pytest_dsl/examples/test_http.py +0 -139
- pytest_dsl/keywords/http_keywords.py +290 -102
- pytest_dsl/parsetab.py +69 -0
- pytest_dsl-0.3.0.dist-info/METADATA +448 -0
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/RECORD +24 -24
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/WHEEL +1 -1
- pytest_dsl/core/custom_auth_example.py +0 -425
- pytest_dsl/examples/csrf_auth_provider.py +0 -232
- pytest_dsl/examples/http/csrf_auth_test.auto +0 -64
- pytest_dsl/examples/http/custom_auth_test.auto +0 -76
- pytest_dsl/examples/http_clients.yaml +0 -48
- pytest_dsl/examples/keyword_example.py +0 -70
- pytest_dsl-0.1.1.dist-info/METADATA +0 -504
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.1.1.dist-info → pytest_dsl-0.3.0.dist-info}/top_level.txt +0 -0
pytest_dsl/cli.py
CHANGED
@@ -5,12 +5,14 @@ pytest-dsl命令行入口
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import sys
|
8
|
+
import argparse
|
8
9
|
import pytest
|
9
10
|
from pathlib import Path
|
10
11
|
|
11
12
|
from pytest_dsl.core.lexer import get_lexer
|
12
13
|
from pytest_dsl.core.parser import get_parser
|
13
14
|
from pytest_dsl.core.dsl_executor import DSLExecutor
|
15
|
+
from pytest_dsl.core.yaml_vars import yaml_vars
|
14
16
|
|
15
17
|
|
16
18
|
def read_file(filename):
|
@@ -19,20 +21,54 @@ def read_file(filename):
|
|
19
21
|
return f.read()
|
20
22
|
|
21
23
|
|
24
|
+
def parse_args():
|
25
|
+
"""解析命令行参数"""
|
26
|
+
parser = argparse.ArgumentParser(description='执行DSL测试文件')
|
27
|
+
parser.add_argument('dsl_file', help='要执行的DSL文件路径')
|
28
|
+
parser.add_argument('--yaml-vars', action='append', default=[],
|
29
|
+
help='YAML变量文件路径,可以指定多个文件 (例如: --yaml-vars vars1.yaml --yaml-vars vars2.yaml)')
|
30
|
+
parser.add_argument('--yaml-vars-dir', default=None,
|
31
|
+
help='YAML变量文件目录路径,将加载该目录下所有.yaml文件')
|
32
|
+
|
33
|
+
return parser.parse_args()
|
34
|
+
|
35
|
+
|
36
|
+
def load_yaml_variables(args):
|
37
|
+
"""从命令行参数加载YAML变量"""
|
38
|
+
# 加载单个YAML文件
|
39
|
+
if args.yaml_vars:
|
40
|
+
yaml_vars.load_yaml_files(args.yaml_vars)
|
41
|
+
print(f"已加载YAML变量文件: {', '.join(args.yaml_vars)}")
|
42
|
+
|
43
|
+
# 加载目录中的YAML文件
|
44
|
+
if args.yaml_vars_dir:
|
45
|
+
yaml_vars_dir = args.yaml_vars_dir
|
46
|
+
try:
|
47
|
+
yaml_vars.load_from_directory(yaml_vars_dir)
|
48
|
+
print(f"已加载YAML变量目录: {yaml_vars_dir}")
|
49
|
+
loaded_files = yaml_vars.get_loaded_files()
|
50
|
+
if loaded_files:
|
51
|
+
dir_files = [f for f in loaded_files if Path(f).parent == Path(yaml_vars_dir)]
|
52
|
+
if dir_files:
|
53
|
+
print(f"目录中加载的文件: {', '.join(dir_files)}")
|
54
|
+
except NotADirectoryError:
|
55
|
+
print(f"YAML变量目录不存在: {yaml_vars_dir}")
|
56
|
+
sys.exit(1)
|
57
|
+
|
58
|
+
|
22
59
|
def main():
|
23
60
|
"""命令行入口点"""
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
filename = sys.argv[1]
|
61
|
+
args = parse_args()
|
62
|
+
|
63
|
+
# 加载YAML变量
|
64
|
+
load_yaml_variables(args)
|
29
65
|
|
30
66
|
lexer = get_lexer()
|
31
67
|
parser = get_parser()
|
32
68
|
executor = DSLExecutor()
|
33
69
|
|
34
70
|
try:
|
35
|
-
dsl_code = read_file(
|
71
|
+
dsl_code = read_file(args.dsl_file)
|
36
72
|
ast = parser.parse(dsl_code, lexer=lexer)
|
37
73
|
executor.execute(ast)
|
38
74
|
except Exception as e:
|
pytest_dsl/core/__init__.py
CHANGED
@@ -0,0 +1,7 @@
|
|
1
|
+
# 导入关键模块确保它们被初始化
|
2
|
+
from pytest_dsl.core.keyword_manager import keyword_manager
|
3
|
+
from pytest_dsl.core.global_context import global_context
|
4
|
+
from pytest_dsl.core.yaml_vars import yaml_vars
|
5
|
+
|
6
|
+
# 导入自定义关键字管理器
|
7
|
+
from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
|
@@ -0,0 +1,213 @@
|
|
1
|
+
from typing import Dict, Any, List, Optional
|
2
|
+
import os
|
3
|
+
import pathlib
|
4
|
+
from pytest_dsl.core.lexer import get_lexer
|
5
|
+
from pytest_dsl.core.parser import get_parser, Node
|
6
|
+
from pytest_dsl.core.dsl_executor import DSLExecutor
|
7
|
+
from pytest_dsl.core.keyword_manager import keyword_manager
|
8
|
+
from pytest_dsl.core.context import TestContext
|
9
|
+
|
10
|
+
|
11
|
+
class CustomKeywordManager:
|
12
|
+
"""自定义关键字管理器
|
13
|
+
|
14
|
+
负责加载和注册自定义关键字
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self):
|
18
|
+
"""初始化自定义关键字管理器"""
|
19
|
+
self.resource_cache = {} # 缓存已加载的资源文件
|
20
|
+
self.resource_paths = [] # 资源文件搜索路径
|
21
|
+
|
22
|
+
def add_resource_path(self, path: str) -> None:
|
23
|
+
"""添加资源文件搜索路径
|
24
|
+
|
25
|
+
Args:
|
26
|
+
path: 资源文件路径
|
27
|
+
"""
|
28
|
+
if path not in self.resource_paths:
|
29
|
+
self.resource_paths.append(path)
|
30
|
+
|
31
|
+
def load_resource_file(self, file_path: str) -> None:
|
32
|
+
"""加载资源文件
|
33
|
+
|
34
|
+
Args:
|
35
|
+
file_path: 资源文件路径
|
36
|
+
"""
|
37
|
+
# 规范化路径,解决路径叠加的问题
|
38
|
+
file_path = os.path.normpath(file_path)
|
39
|
+
|
40
|
+
# 如果已经缓存,则跳过
|
41
|
+
absolute_path = os.path.abspath(file_path)
|
42
|
+
if absolute_path in self.resource_cache:
|
43
|
+
return
|
44
|
+
|
45
|
+
# 读取文件内容
|
46
|
+
if not os.path.exists(file_path):
|
47
|
+
# 尝试在资源路径中查找
|
48
|
+
for resource_path in self.resource_paths:
|
49
|
+
full_path = os.path.join(resource_path, file_path)
|
50
|
+
if os.path.exists(full_path):
|
51
|
+
file_path = full_path
|
52
|
+
absolute_path = os.path.abspath(file_path)
|
53
|
+
break
|
54
|
+
else:
|
55
|
+
# 如果文件不存在,尝试在根项目目录中查找
|
56
|
+
# 一般情况下文件路径可能是相对于项目根目录的
|
57
|
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
58
|
+
full_path = os.path.join(project_root, file_path)
|
59
|
+
if os.path.exists(full_path):
|
60
|
+
file_path = full_path
|
61
|
+
absolute_path = os.path.abspath(file_path)
|
62
|
+
else:
|
63
|
+
raise FileNotFoundError(f"资源文件不存在: {file_path}")
|
64
|
+
|
65
|
+
try:
|
66
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
67
|
+
content = f.read()
|
68
|
+
|
69
|
+
# 解析资源文件
|
70
|
+
lexer = get_lexer()
|
71
|
+
parser = get_parser()
|
72
|
+
ast = parser.parse(content, lexer=lexer)
|
73
|
+
|
74
|
+
# 标记为已加载
|
75
|
+
self.resource_cache[absolute_path] = True
|
76
|
+
|
77
|
+
# 处理导入指令
|
78
|
+
self._process_imports(ast, os.path.dirname(file_path))
|
79
|
+
|
80
|
+
# 注册关键字
|
81
|
+
self._register_keywords(ast, file_path)
|
82
|
+
except Exception as e:
|
83
|
+
print(f"资源文件 {file_path} 加载失败: {str(e)}")
|
84
|
+
raise
|
85
|
+
|
86
|
+
def _process_imports(self, ast: Node, base_dir: str) -> None:
|
87
|
+
"""处理资源文件中的导入指令
|
88
|
+
|
89
|
+
Args:
|
90
|
+
ast: 抽象语法树
|
91
|
+
base_dir: 基础目录
|
92
|
+
"""
|
93
|
+
if ast.type != 'Start' or not ast.children:
|
94
|
+
return
|
95
|
+
|
96
|
+
metadata_node = ast.children[0]
|
97
|
+
if metadata_node.type != 'Metadata':
|
98
|
+
return
|
99
|
+
|
100
|
+
for item in metadata_node.children:
|
101
|
+
if item.type == '@import':
|
102
|
+
imported_file = item.value
|
103
|
+
# 处理相对路径
|
104
|
+
if not os.path.isabs(imported_file):
|
105
|
+
imported_file = os.path.join(base_dir, imported_file)
|
106
|
+
|
107
|
+
# 规范化路径,避免路径叠加问题
|
108
|
+
imported_file = os.path.normpath(imported_file)
|
109
|
+
|
110
|
+
# 递归加载导入的资源文件
|
111
|
+
self.load_resource_file(imported_file)
|
112
|
+
|
113
|
+
def _register_keywords(self, ast: Node, file_path: str) -> None:
|
114
|
+
"""从AST中注册关键字
|
115
|
+
|
116
|
+
Args:
|
117
|
+
ast: 抽象语法树
|
118
|
+
file_path: 文件路径
|
119
|
+
"""
|
120
|
+
if ast.type != 'Start' or len(ast.children) < 2:
|
121
|
+
return
|
122
|
+
|
123
|
+
# 遍历语句节点
|
124
|
+
statements_node = ast.children[1]
|
125
|
+
if statements_node.type != 'Statements':
|
126
|
+
return
|
127
|
+
|
128
|
+
for node in statements_node.children:
|
129
|
+
if node.type == 'CustomKeyword':
|
130
|
+
self._register_custom_keyword(node, file_path)
|
131
|
+
|
132
|
+
def _register_custom_keyword(self, node: Node, file_path: str) -> None:
|
133
|
+
"""注册自定义关键字
|
134
|
+
|
135
|
+
Args:
|
136
|
+
node: 关键字节点
|
137
|
+
file_path: 资源文件路径
|
138
|
+
"""
|
139
|
+
# 提取关键字信息
|
140
|
+
keyword_name = node.value
|
141
|
+
params_node = node.children[0]
|
142
|
+
body_node = node.children[1]
|
143
|
+
|
144
|
+
# 构建参数列表
|
145
|
+
parameters = []
|
146
|
+
param_mapping = {}
|
147
|
+
param_defaults = {} # 存储参数默认值
|
148
|
+
|
149
|
+
for param in params_node if params_node else []:
|
150
|
+
param_name = param.value
|
151
|
+
param_default = None
|
152
|
+
|
153
|
+
# 检查是否有默认值
|
154
|
+
if param.children and param.children[0]:
|
155
|
+
param_default = param.children[0].value
|
156
|
+
param_defaults[param_name] = param_default # 保存默认值
|
157
|
+
|
158
|
+
# 添加参数定义
|
159
|
+
parameters.append({
|
160
|
+
'name': param_name,
|
161
|
+
'mapping': param_name, # 中文参数名和内部参数名相同
|
162
|
+
'description': f'自定义关键字参数 {param_name}'
|
163
|
+
})
|
164
|
+
|
165
|
+
param_mapping[param_name] = param_name
|
166
|
+
|
167
|
+
# 注册自定义关键字到关键字管理器
|
168
|
+
@keyword_manager.register(keyword_name, parameters)
|
169
|
+
def custom_keyword_executor(**kwargs):
|
170
|
+
"""自定义关键字执行器"""
|
171
|
+
# 创建一个新的DSL执行器
|
172
|
+
executor = DSLExecutor()
|
173
|
+
|
174
|
+
# 获取传递的上下文
|
175
|
+
context = kwargs.get('context')
|
176
|
+
if context:
|
177
|
+
executor.test_context = context
|
178
|
+
|
179
|
+
# 先应用默认值
|
180
|
+
for param_name, default_value in param_defaults.items():
|
181
|
+
executor.variables[param_name] = default_value
|
182
|
+
executor.test_context.set(param_name, default_value)
|
183
|
+
|
184
|
+
# 然后应用传入的参数值(覆盖默认值)
|
185
|
+
for param_name, param_mapping_name in param_mapping.items():
|
186
|
+
if param_mapping_name in kwargs:
|
187
|
+
# 确保参数值在标准变量和测试上下文中都可用
|
188
|
+
executor.variables[param_name] = kwargs[param_mapping_name]
|
189
|
+
executor.test_context.set(param_name, kwargs[param_mapping_name])
|
190
|
+
|
191
|
+
# 执行关键字体中的语句
|
192
|
+
result = None
|
193
|
+
try:
|
194
|
+
for stmt in body_node.children:
|
195
|
+
# 检查是否是return语句
|
196
|
+
if stmt.type == 'Return':
|
197
|
+
# 对表达式求值
|
198
|
+
result = executor.eval_expression(stmt.children[0])
|
199
|
+
break
|
200
|
+
else:
|
201
|
+
# 执行普通语句
|
202
|
+
executor.execute(stmt)
|
203
|
+
except Exception as e:
|
204
|
+
print(f"执行自定义关键字 {keyword_name} 时发生错误: {str(e)}")
|
205
|
+
raise
|
206
|
+
|
207
|
+
return result
|
208
|
+
|
209
|
+
print(f"已注册自定义关键字: {keyword_name} 来自文件: {file_path}")
|
210
|
+
|
211
|
+
|
212
|
+
# 创建全局自定义关键字管理器实例
|
213
|
+
custom_keyword_manager = CustomKeywordManager()
|
pytest_dsl/core/dsl_executor.py
CHANGED
@@ -26,6 +26,7 @@ class DSLExecutor:
|
|
26
26
|
self.test_context = TestContext()
|
27
27
|
self.test_context.executor = self # 让 test_context 能够访问到 executor
|
28
28
|
self.variable_replacer = VariableReplacer(self.variables, self.test_context)
|
29
|
+
self.imported_files = set() # 跟踪已导入的文件,避免循环导入
|
29
30
|
|
30
31
|
def set_current_data(self, data):
|
31
32
|
"""设置当前测试数据集"""
|
@@ -92,6 +93,12 @@ class DSLExecutor:
|
|
92
93
|
elif expr_node.type == 'BooleanExpr':
|
93
94
|
# 处理布尔值表达式
|
94
95
|
return expr_node.value
|
96
|
+
elif expr_node.type == 'ComparisonExpr':
|
97
|
+
# 处理比较表达式
|
98
|
+
return self._eval_comparison_expr(expr_node)
|
99
|
+
elif expr_node.type == 'ArithmeticExpr':
|
100
|
+
# 处理算术表达式
|
101
|
+
return self._eval_arithmetic_expr(expr_node)
|
95
102
|
else:
|
96
103
|
raise Exception(f"无法求值的表达式类型: {expr_node.type}")
|
97
104
|
|
@@ -100,8 +107,12 @@ class DSLExecutor:
|
|
100
107
|
if isinstance(value, Node):
|
101
108
|
return self.eval_expression(value)
|
102
109
|
elif isinstance(value, str):
|
110
|
+
# 如果是ID类型的变量名
|
111
|
+
if value in self.variable_replacer.local_variables:
|
112
|
+
return self.variable_replacer.local_variables[value]
|
113
|
+
|
103
114
|
# 定义变量引用模式
|
104
|
-
pattern = r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
|
115
|
+
pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)\}'
|
105
116
|
# 检查整个字符串是否完全匹配单一变量引用模式
|
106
117
|
match = re.fullmatch(pattern, value)
|
107
118
|
if match:
|
@@ -112,6 +123,86 @@ class DSLExecutor:
|
|
112
123
|
return self.variable_replacer.replace_in_string(value)
|
113
124
|
return value
|
114
125
|
|
126
|
+
def _eval_comparison_expr(self, expr_node):
|
127
|
+
"""
|
128
|
+
对比较表达式进行求值
|
129
|
+
|
130
|
+
:param expr_node: 比较表达式节点
|
131
|
+
:return: 比较结果(布尔值)
|
132
|
+
"""
|
133
|
+
left_value = self.eval_expression(expr_node.children[0])
|
134
|
+
right_value = self.eval_expression(expr_node.children[1])
|
135
|
+
operator = expr_node.value # 操作符: >, <, >=, <=, ==, !=
|
136
|
+
|
137
|
+
# 尝试类型转换
|
138
|
+
if isinstance(left_value, str) and str(left_value).isdigit():
|
139
|
+
left_value = int(left_value)
|
140
|
+
if isinstance(right_value, str) and str(right_value).isdigit():
|
141
|
+
right_value = int(right_value)
|
142
|
+
|
143
|
+
# 根据操作符执行相应的比较操作
|
144
|
+
if operator == '>':
|
145
|
+
return left_value > right_value
|
146
|
+
elif operator == '<':
|
147
|
+
return left_value < right_value
|
148
|
+
elif operator == '>=':
|
149
|
+
return left_value >= right_value
|
150
|
+
elif operator == '<=':
|
151
|
+
return left_value <= right_value
|
152
|
+
elif operator == '==':
|
153
|
+
return left_value == right_value
|
154
|
+
elif operator == '!=':
|
155
|
+
return left_value != right_value
|
156
|
+
else:
|
157
|
+
raise Exception(f"未知的比较操作符: {operator}")
|
158
|
+
|
159
|
+
def _eval_arithmetic_expr(self, expr_node):
|
160
|
+
"""
|
161
|
+
对算术表达式进行求值
|
162
|
+
|
163
|
+
:param expr_node: 算术表达式节点
|
164
|
+
:return: 计算结果
|
165
|
+
"""
|
166
|
+
left_value = self.eval_expression(expr_node.children[0])
|
167
|
+
right_value = self.eval_expression(expr_node.children[1])
|
168
|
+
operator = expr_node.value # 操作符: +, -, *, /
|
169
|
+
|
170
|
+
# 尝试类型转换 - 如果是字符串数字则转为数字
|
171
|
+
if isinstance(left_value, str) and str(left_value).replace('.', '', 1).isdigit():
|
172
|
+
left_value = float(left_value)
|
173
|
+
# 如果是整数则转为整数
|
174
|
+
if left_value.is_integer():
|
175
|
+
left_value = int(left_value)
|
176
|
+
|
177
|
+
if isinstance(right_value, str) and str(right_value).replace('.', '', 1).isdigit():
|
178
|
+
right_value = float(right_value)
|
179
|
+
# 如果是整数则转为整数
|
180
|
+
if right_value.is_integer():
|
181
|
+
right_value = int(right_value)
|
182
|
+
|
183
|
+
# 进行相应的算术运算
|
184
|
+
if operator == '+':
|
185
|
+
# 对于字符串,+是连接操作
|
186
|
+
if isinstance(left_value, str) or isinstance(right_value, str):
|
187
|
+
return str(left_value) + str(right_value)
|
188
|
+
return left_value + right_value
|
189
|
+
elif operator == '-':
|
190
|
+
return left_value - right_value
|
191
|
+
elif operator == '*':
|
192
|
+
# 如果其中一个是字符串,另一个是数字,则进行字符串重复
|
193
|
+
if isinstance(left_value, str) and isinstance(right_value, (int, float)):
|
194
|
+
return left_value * int(right_value)
|
195
|
+
elif isinstance(right_value, str) and isinstance(left_value, (int, float)):
|
196
|
+
return right_value * int(left_value)
|
197
|
+
return left_value * right_value
|
198
|
+
elif operator == '/':
|
199
|
+
# 除法时检查除数是否为0
|
200
|
+
if right_value == 0:
|
201
|
+
raise Exception("除法错误: 除数不能为0")
|
202
|
+
return left_value / right_value
|
203
|
+
else:
|
204
|
+
raise Exception(f"未知的算术操作符: {operator}")
|
205
|
+
|
115
206
|
def _get_variable(self, var_name):
|
116
207
|
"""获取变量值,优先从本地变量获取,如果不存在则尝试从全局上下文获取"""
|
117
208
|
return self.variable_replacer.get_variable(var_name)
|
@@ -119,6 +210,21 @@ class DSLExecutor:
|
|
119
210
|
def _replace_variables_in_string(self, value):
|
120
211
|
"""替换字符串中的变量引用"""
|
121
212
|
return self.variable_replacer.replace_in_string(value)
|
213
|
+
|
214
|
+
def _handle_custom_keywords_in_file(self, node):
|
215
|
+
"""处理文件中的自定义关键字定义
|
216
|
+
|
217
|
+
Args:
|
218
|
+
node: Start节点
|
219
|
+
"""
|
220
|
+
if len(node.children) > 1 and node.children[1].type == 'Statements':
|
221
|
+
statements_node = node.children[1]
|
222
|
+
for stmt in statements_node.children:
|
223
|
+
if stmt.type == 'CustomKeyword':
|
224
|
+
# 导入自定义关键字管理器
|
225
|
+
from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
|
226
|
+
# 注册自定义关键字
|
227
|
+
custom_keyword_manager._register_custom_keyword(stmt, "current_file")
|
122
228
|
|
123
229
|
def _handle_start(self, node):
|
124
230
|
"""处理开始节点"""
|
@@ -133,9 +239,14 @@ class DSLExecutor:
|
|
133
239
|
if child.type == 'Metadata':
|
134
240
|
for item in child.children:
|
135
241
|
metadata[item.type] = item.value
|
242
|
+
# 处理导入指令
|
243
|
+
if item.type == '@import':
|
244
|
+
self._handle_import(item.value)
|
136
245
|
elif child.type == 'Teardown':
|
137
246
|
teardown_node = child
|
138
247
|
|
248
|
+
# 在_execute_test_iteration之前添加
|
249
|
+
self._handle_custom_keywords_in_file(node)
|
139
250
|
# 执行测试
|
140
251
|
self._execute_test_iteration(metadata, node, teardown_node)
|
141
252
|
|
@@ -154,6 +265,25 @@ class DSLExecutor:
|
|
154
265
|
# 测试用例执行完成后清空上下文
|
155
266
|
self.test_context.clear()
|
156
267
|
|
268
|
+
def _handle_import(self, file_path):
|
269
|
+
"""处理导入指令
|
270
|
+
|
271
|
+
Args:
|
272
|
+
file_path: 资源文件路径
|
273
|
+
"""
|
274
|
+
# 防止循环导入
|
275
|
+
if file_path in self.imported_files:
|
276
|
+
return
|
277
|
+
|
278
|
+
try:
|
279
|
+
# 导入自定义关键字文件
|
280
|
+
from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
|
281
|
+
custom_keyword_manager.load_resource_file(file_path)
|
282
|
+
self.imported_files.add(file_path)
|
283
|
+
except Exception as e:
|
284
|
+
print(f"导入资源文件失败: {file_path}, 错误: {str(e)}")
|
285
|
+
raise
|
286
|
+
|
157
287
|
def _execute_test_iteration(self, metadata, node, teardown_node):
|
158
288
|
"""执行测试迭代"""
|
159
289
|
try:
|
@@ -305,6 +435,41 @@ class DSLExecutor:
|
|
305
435
|
"""处理清理操作"""
|
306
436
|
self.execute(node.children[0])
|
307
437
|
|
438
|
+
@allure.step("执行返回语句")
|
439
|
+
def _handle_return(self, node):
|
440
|
+
"""处理return语句
|
441
|
+
|
442
|
+
Args:
|
443
|
+
node: Return节点
|
444
|
+
|
445
|
+
Returns:
|
446
|
+
表达式求值结果
|
447
|
+
"""
|
448
|
+
expr_node = node.children[0]
|
449
|
+
return self.eval_expression(expr_node)
|
450
|
+
|
451
|
+
@allure.step("执行条件语句")
|
452
|
+
def _handle_if_statement(self, node):
|
453
|
+
"""处理if-else语句
|
454
|
+
|
455
|
+
Args:
|
456
|
+
node: IfStatement节点,包含条件表达式、if分支和可选的else分支
|
457
|
+
"""
|
458
|
+
condition = self.eval_expression(node.children[0])
|
459
|
+
|
460
|
+
# 将条件转换为布尔值进行评估
|
461
|
+
if condition:
|
462
|
+
# 执行if分支
|
463
|
+
with allure.step("执行if分支"):
|
464
|
+
return self.execute(node.children[1])
|
465
|
+
elif len(node.children) > 2:
|
466
|
+
# 如果存在else分支且条件为假,则执行else分支
|
467
|
+
with allure.step("执行else分支"):
|
468
|
+
return self.execute(node.children[2])
|
469
|
+
|
470
|
+
# 如果条件为假且没有else分支,则不执行任何操作
|
471
|
+
return None
|
472
|
+
|
308
473
|
def execute(self, node):
|
309
474
|
"""执行AST节点"""
|
310
475
|
handlers = {
|
@@ -315,7 +480,10 @@ class DSLExecutor:
|
|
315
480
|
'AssignmentKeywordCall': self._handle_assignment_keyword_call,
|
316
481
|
'ForLoop': self._handle_for_loop,
|
317
482
|
'KeywordCall': self._execute_keyword_call,
|
318
|
-
'Teardown': self._handle_teardown
|
483
|
+
'Teardown': self._handle_teardown,
|
484
|
+
'Return': self._handle_return,
|
485
|
+
'IfStatement': self._handle_if_statement,
|
486
|
+
'CustomKeyword': lambda _: None # 添加对CustomKeyword节点的处理,只需注册不需执行
|
319
487
|
}
|
320
488
|
|
321
489
|
handler = handlers.get(node.type)
|
@@ -326,4 +494,4 @@ class DSLExecutor:
|
|
326
494
|
def read_file(filename):
|
327
495
|
"""读取 DSL 文件内容"""
|
328
496
|
with open(filename, 'r', encoding='utf-8') as f:
|
329
|
-
return f.read()
|
497
|
+
return f.read()
|