pytest-dsl 0.8.0__py3-none-any.whl → 0.9.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 +334 -15
- pytest_dsl/core/custom_keyword_manager.py +49 -48
- pytest_dsl/core/dsl_executor.py +41 -11
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.0.dist-info}/METADATA +1 -1
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.0.dist-info}/RECORD +9 -9
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.0.dist-info}/entry_points.txt +1 -0
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.0.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.0.dist-info}/top_level.txt +0 -0
pytest_dsl/cli.py
CHANGED
@@ -6,7 +6,6 @@ pytest-dsl命令行入口
|
|
6
6
|
|
7
7
|
import sys
|
8
8
|
import argparse
|
9
|
-
import pytest
|
10
9
|
import os
|
11
10
|
from pathlib import Path
|
12
11
|
|
@@ -14,8 +13,13 @@ from pytest_dsl.core.lexer import get_lexer
|
|
14
13
|
from pytest_dsl.core.parser import get_parser
|
15
14
|
from pytest_dsl.core.dsl_executor import DSLExecutor
|
16
15
|
from pytest_dsl.core.yaml_loader import load_yaml_variables_from_args
|
17
|
-
from pytest_dsl.core.auto_directory import
|
18
|
-
|
16
|
+
from pytest_dsl.core.auto_directory import (
|
17
|
+
SETUP_FILE_NAME, TEARDOWN_FILE_NAME, execute_hook_file
|
18
|
+
)
|
19
|
+
from pytest_dsl.core.plugin_discovery import (
|
20
|
+
load_all_plugins, scan_local_keywords
|
21
|
+
)
|
22
|
+
from pytest_dsl.core.keyword_manager import keyword_manager
|
19
23
|
|
20
24
|
|
21
25
|
def read_file(filename):
|
@@ -26,14 +30,296 @@ def read_file(filename):
|
|
26
30
|
|
27
31
|
def parse_args():
|
28
32
|
"""解析命令行参数"""
|
29
|
-
|
30
|
-
|
31
|
-
parser.add_argument('--yaml-vars', action='append', default=[],
|
32
|
-
help='YAML变量文件路径,可以指定多个文件 (例如: --yaml-vars vars1.yaml --yaml-vars vars2.yaml)')
|
33
|
-
parser.add_argument('--yaml-vars-dir', default=None,
|
34
|
-
help='YAML变量文件目录路径,将加载该目录下所有.yaml文件')
|
33
|
+
import sys
|
34
|
+
argv = sys.argv[1:] # 去掉脚本名
|
35
35
|
|
36
|
-
|
36
|
+
# 检查是否使用了子命令格式
|
37
|
+
if argv and argv[0] in ['run', 'list-keywords']:
|
38
|
+
# 使用新的子命令格式
|
39
|
+
parser = argparse.ArgumentParser(description='执行DSL测试文件')
|
40
|
+
subparsers = parser.add_subparsers(dest='command', help='可用命令')
|
41
|
+
|
42
|
+
# 执行命令
|
43
|
+
run_parser = subparsers.add_parser('run', help='执行DSL文件')
|
44
|
+
run_parser.add_argument(
|
45
|
+
'path',
|
46
|
+
help='要执行的DSL文件路径或包含DSL文件的目录'
|
47
|
+
)
|
48
|
+
run_parser.add_argument(
|
49
|
+
'--yaml-vars', action='append', default=[],
|
50
|
+
help='YAML变量文件路径,可以指定多个文件 '
|
51
|
+
'(例如: --yaml-vars vars1.yaml '
|
52
|
+
'--yaml-vars vars2.yaml)'
|
53
|
+
)
|
54
|
+
run_parser.add_argument(
|
55
|
+
'--yaml-vars-dir', default=None,
|
56
|
+
help='YAML变量文件目录路径,'
|
57
|
+
'将加载该目录下所有.yaml文件'
|
58
|
+
)
|
59
|
+
|
60
|
+
# 关键字列表命令
|
61
|
+
list_parser = subparsers.add_parser(
|
62
|
+
'list-keywords',
|
63
|
+
help='罗列所有可用关键字和参数信息'
|
64
|
+
)
|
65
|
+
list_parser.add_argument(
|
66
|
+
'--format', choices=['text', 'json'],
|
67
|
+
default='text',
|
68
|
+
help='输出格式:text(默认) 或 json'
|
69
|
+
)
|
70
|
+
list_parser.add_argument(
|
71
|
+
'--filter', type=str, default=None,
|
72
|
+
help='过滤关键字名称(支持部分匹配)'
|
73
|
+
)
|
74
|
+
list_parser.add_argument(
|
75
|
+
'--category',
|
76
|
+
choices=['builtin', 'custom', 'remote', 'all'],
|
77
|
+
default='all',
|
78
|
+
help='关键字类别:builtin(内置)、custom(自定义)、'
|
79
|
+
'remote(远程)、all(全部,默认)'
|
80
|
+
)
|
81
|
+
|
82
|
+
return parser.parse_args(argv)
|
83
|
+
else:
|
84
|
+
# 向后兼容模式
|
85
|
+
parser = argparse.ArgumentParser(description='执行DSL测试文件')
|
86
|
+
|
87
|
+
# 检查是否是list-keywords的旧格式
|
88
|
+
if '--list-keywords' in argv:
|
89
|
+
parser.add_argument('--list-keywords', action='store_true')
|
90
|
+
parser.add_argument(
|
91
|
+
'--format', choices=['text', 'json'], default='text'
|
92
|
+
)
|
93
|
+
parser.add_argument('--filter', type=str, default=None)
|
94
|
+
parser.add_argument(
|
95
|
+
'--category',
|
96
|
+
choices=['builtin', 'custom', 'remote', 'all'],
|
97
|
+
default='all'
|
98
|
+
)
|
99
|
+
parser.add_argument('path', nargs='?') # 可选的路径参数
|
100
|
+
parser.add_argument(
|
101
|
+
'--yaml-vars', action='append', default=[]
|
102
|
+
)
|
103
|
+
parser.add_argument('--yaml-vars-dir', default=None)
|
104
|
+
|
105
|
+
args = parser.parse_args(argv)
|
106
|
+
args.command = 'list-keywords-compat' # 标记为兼容模式
|
107
|
+
else:
|
108
|
+
# 默认为run命令的向后兼容模式
|
109
|
+
parser.add_argument('path', nargs='?')
|
110
|
+
parser.add_argument(
|
111
|
+
'--yaml-vars', action='append', default=[]
|
112
|
+
)
|
113
|
+
parser.add_argument('--yaml-vars-dir', default=None)
|
114
|
+
|
115
|
+
args = parser.parse_args(argv)
|
116
|
+
args.command = 'run-compat' # 标记为兼容模式
|
117
|
+
|
118
|
+
return args
|
119
|
+
|
120
|
+
|
121
|
+
def load_all_keywords():
|
122
|
+
"""加载所有可用的关键字"""
|
123
|
+
# 首先导入内置关键字模块,确保内置关键字被注册
|
124
|
+
try:
|
125
|
+
import pytest_dsl.keywords # noqa: F401
|
126
|
+
print("内置关键字模块加载完成")
|
127
|
+
except ImportError as e:
|
128
|
+
print(f"加载内置关键字模块失败: {e}")
|
129
|
+
|
130
|
+
# 加载已安装的关键字插件
|
131
|
+
load_all_plugins()
|
132
|
+
|
133
|
+
# 扫描本地关键字
|
134
|
+
scan_local_keywords()
|
135
|
+
|
136
|
+
|
137
|
+
def categorize_keyword(keyword_name, keyword_info):
|
138
|
+
"""判断关键字的类别"""
|
139
|
+
if keyword_info.get('remote', False):
|
140
|
+
return 'remote'
|
141
|
+
|
142
|
+
# 检查是否是内置关键字(通过检查函数所在模块)
|
143
|
+
func = keyword_info.get('func')
|
144
|
+
if func and hasattr(func, '__module__'):
|
145
|
+
module_name = func.__module__
|
146
|
+
if module_name and module_name.startswith('pytest_dsl.keywords'):
|
147
|
+
return 'builtin'
|
148
|
+
|
149
|
+
return 'custom'
|
150
|
+
|
151
|
+
|
152
|
+
def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
|
153
|
+
"""格式化关键字信息为文本格式"""
|
154
|
+
lines = []
|
155
|
+
|
156
|
+
# 关键字名称和类别
|
157
|
+
category = categorize_keyword(keyword_name, keyword_info)
|
158
|
+
category_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
|
159
|
+
|
160
|
+
if show_category:
|
161
|
+
category_display = category_names.get(category, '未知')
|
162
|
+
lines.append(f"关键字: {keyword_name} [{category_display}]")
|
163
|
+
else:
|
164
|
+
lines.append(f"关键字: {keyword_name}")
|
165
|
+
|
166
|
+
# 远程关键字特殊标识
|
167
|
+
if keyword_info.get('remote', False):
|
168
|
+
alias = keyword_info.get('alias', '未知')
|
169
|
+
original_name = keyword_info.get('original_name', keyword_name)
|
170
|
+
lines.append(f" 远程服务器: {alias}")
|
171
|
+
lines.append(f" 原始名称: {original_name}")
|
172
|
+
|
173
|
+
# 参数信息
|
174
|
+
parameters = keyword_info.get('parameters', [])
|
175
|
+
if parameters:
|
176
|
+
lines.append(" 参数:")
|
177
|
+
for param in parameters:
|
178
|
+
param_name = getattr(param, 'name', str(param))
|
179
|
+
param_mapping = getattr(param, 'mapping', '')
|
180
|
+
param_desc = getattr(param, 'description', '')
|
181
|
+
|
182
|
+
if param_mapping and param_mapping != param_name:
|
183
|
+
lines.append(
|
184
|
+
f" {param_name} ({param_mapping}): {param_desc}"
|
185
|
+
)
|
186
|
+
else:
|
187
|
+
lines.append(f" {param_name}: {param_desc}")
|
188
|
+
else:
|
189
|
+
lines.append(" 参数: 无")
|
190
|
+
|
191
|
+
# 函数文档
|
192
|
+
func = keyword_info.get('func')
|
193
|
+
if func and hasattr(func, '__doc__') and func.__doc__:
|
194
|
+
lines.append(f" 说明: {func.__doc__.strip()}")
|
195
|
+
|
196
|
+
return '\n'.join(lines)
|
197
|
+
|
198
|
+
|
199
|
+
def format_keyword_info_json(keyword_name, keyword_info):
|
200
|
+
"""格式化关键字信息为JSON格式"""
|
201
|
+
category = categorize_keyword(keyword_name, keyword_info)
|
202
|
+
|
203
|
+
keyword_data = {
|
204
|
+
'name': keyword_name,
|
205
|
+
'category': category,
|
206
|
+
'parameters': []
|
207
|
+
}
|
208
|
+
|
209
|
+
# 远程关键字特殊信息
|
210
|
+
if keyword_info.get('remote', False):
|
211
|
+
keyword_data['remote'] = {
|
212
|
+
'alias': keyword_info.get('alias', ''),
|
213
|
+
'original_name': keyword_info.get('original_name', keyword_name)
|
214
|
+
}
|
215
|
+
|
216
|
+
# 参数信息
|
217
|
+
parameters = keyword_info.get('parameters', [])
|
218
|
+
for param in parameters:
|
219
|
+
param_data = {
|
220
|
+
'name': getattr(param, 'name', str(param)),
|
221
|
+
'mapping': getattr(param, 'mapping', ''),
|
222
|
+
'description': getattr(param, 'description', '')
|
223
|
+
}
|
224
|
+
keyword_data['parameters'].append(param_data)
|
225
|
+
|
226
|
+
# 函数文档
|
227
|
+
func = keyword_info.get('func')
|
228
|
+
if func and hasattr(func, '__doc__') and func.__doc__:
|
229
|
+
keyword_data['documentation'] = func.__doc__.strip()
|
230
|
+
|
231
|
+
return keyword_data
|
232
|
+
|
233
|
+
|
234
|
+
def list_keywords(output_format='text', name_filter=None,
|
235
|
+
category_filter='all'):
|
236
|
+
"""罗列所有关键字信息"""
|
237
|
+
import json
|
238
|
+
|
239
|
+
print("正在加载关键字...")
|
240
|
+
load_all_keywords()
|
241
|
+
|
242
|
+
# 获取所有注册的关键字
|
243
|
+
all_keywords = keyword_manager._keywords
|
244
|
+
|
245
|
+
if not all_keywords:
|
246
|
+
print("未发现任何关键字")
|
247
|
+
return
|
248
|
+
|
249
|
+
# 过滤关键字
|
250
|
+
filtered_keywords = {}
|
251
|
+
|
252
|
+
for name, info in all_keywords.items():
|
253
|
+
# 名称过滤
|
254
|
+
if name_filter and name_filter.lower() not in name.lower():
|
255
|
+
continue
|
256
|
+
|
257
|
+
# 类别过滤
|
258
|
+
if category_filter != 'all':
|
259
|
+
keyword_category = categorize_keyword(name, info)
|
260
|
+
if keyword_category != category_filter:
|
261
|
+
continue
|
262
|
+
|
263
|
+
filtered_keywords[name] = info
|
264
|
+
|
265
|
+
if not filtered_keywords:
|
266
|
+
if name_filter:
|
267
|
+
print(f"未找到包含 '{name_filter}' 的关键字")
|
268
|
+
else:
|
269
|
+
print(f"未找到 {category_filter} 类别的关键字")
|
270
|
+
return
|
271
|
+
|
272
|
+
# 输出统计信息
|
273
|
+
total_count = len(filtered_keywords)
|
274
|
+
category_counts = {}
|
275
|
+
for name, info in filtered_keywords.items():
|
276
|
+
cat = categorize_keyword(name, info)
|
277
|
+
category_counts[cat] = category_counts.get(cat, 0) + 1
|
278
|
+
|
279
|
+
if output_format == 'text':
|
280
|
+
print(f"\n找到 {total_count} 个关键字:")
|
281
|
+
for cat, count in category_counts.items():
|
282
|
+
cat_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
|
283
|
+
print(f" {cat_names.get(cat, cat)}: {count} 个")
|
284
|
+
print("-" * 60)
|
285
|
+
|
286
|
+
# 按类别分组显示
|
287
|
+
for category in ['builtin', 'custom', 'remote']:
|
288
|
+
cat_keywords = {
|
289
|
+
name: info for name, info in filtered_keywords.items()
|
290
|
+
if categorize_keyword(name, info) == category
|
291
|
+
}
|
292
|
+
|
293
|
+
if cat_keywords:
|
294
|
+
cat_names = {
|
295
|
+
'builtin': '内置关键字',
|
296
|
+
'custom': '自定义关键字',
|
297
|
+
'remote': '远程关键字'
|
298
|
+
}
|
299
|
+
print(f"\n=== {cat_names[category]} ===")
|
300
|
+
|
301
|
+
for name in sorted(cat_keywords.keys()):
|
302
|
+
info = cat_keywords[name]
|
303
|
+
print()
|
304
|
+
print(format_keyword_info_text(
|
305
|
+
name, info, show_category=False
|
306
|
+
))
|
307
|
+
|
308
|
+
elif output_format == 'json':
|
309
|
+
keywords_data = {
|
310
|
+
'summary': {
|
311
|
+
'total_count': total_count,
|
312
|
+
'category_counts': category_counts
|
313
|
+
},
|
314
|
+
'keywords': []
|
315
|
+
}
|
316
|
+
|
317
|
+
for name in sorted(filtered_keywords.keys()):
|
318
|
+
info = filtered_keywords[name]
|
319
|
+
keyword_data = format_keyword_info_json(name, info)
|
320
|
+
keywords_data['keywords'].append(keyword_data)
|
321
|
+
|
322
|
+
print(json.dumps(keywords_data, ensure_ascii=False, indent=2))
|
37
323
|
|
38
324
|
|
39
325
|
def load_yaml_variables(args):
|
@@ -68,18 +354,22 @@ def find_dsl_files(directory):
|
|
68
354
|
dsl_files = []
|
69
355
|
for root, _, files in os.walk(directory):
|
70
356
|
for file in files:
|
71
|
-
if file.endswith(('.dsl', '.auto')) and
|
357
|
+
if (file.endswith(('.dsl', '.auto')) and
|
358
|
+
file not in [SETUP_FILE_NAME, TEARDOWN_FILE_NAME]):
|
72
359
|
dsl_files.append(os.path.join(root, file))
|
73
360
|
return dsl_files
|
74
361
|
|
75
362
|
|
76
|
-
def
|
77
|
-
"""
|
78
|
-
args = parse_args()
|
363
|
+
def run_dsl_tests(args):
|
364
|
+
"""执行DSL测试的主函数"""
|
79
365
|
path = args.path
|
80
366
|
|
367
|
+
if not path:
|
368
|
+
print("错误: 必须指定要执行的DSL文件路径或目录")
|
369
|
+
sys.exit(1)
|
370
|
+
|
81
371
|
# 加载内置关键字插件
|
82
|
-
|
372
|
+
load_all_keywords()
|
83
373
|
|
84
374
|
# 加载YAML变量(包括远程服务器自动连接)
|
85
375
|
load_yaml_variables(args)
|
@@ -134,5 +424,34 @@ def main():
|
|
134
424
|
sys.exit(1)
|
135
425
|
|
136
426
|
|
427
|
+
def main():
|
428
|
+
"""命令行入口点"""
|
429
|
+
args = parse_args()
|
430
|
+
|
431
|
+
# 处理子命令
|
432
|
+
if args.command == 'list-keywords':
|
433
|
+
list_keywords(
|
434
|
+
output_format=args.format,
|
435
|
+
name_filter=args.filter,
|
436
|
+
category_filter=args.category
|
437
|
+
)
|
438
|
+
elif args.command == 'run':
|
439
|
+
run_dsl_tests(args)
|
440
|
+
elif args.command == 'list-keywords-compat':
|
441
|
+
# 向后兼容:旧的--list-keywords格式
|
442
|
+
list_keywords(
|
443
|
+
output_format=args.format,
|
444
|
+
name_filter=args.filter,
|
445
|
+
category_filter=args.category
|
446
|
+
)
|
447
|
+
elif args.command == 'run-compat':
|
448
|
+
# 向后兼容:默认执行DSL测试
|
449
|
+
run_dsl_tests(args)
|
450
|
+
else:
|
451
|
+
# 如果没有匹配的命令,显示帮助
|
452
|
+
print("错误: 未知命令")
|
453
|
+
sys.exit(1)
|
454
|
+
|
455
|
+
|
137
456
|
if __name__ == '__main__':
|
138
457
|
main()
|
@@ -10,38 +10,38 @@ from pytest_dsl.core.context import TestContext
|
|
10
10
|
|
11
11
|
class CustomKeywordManager:
|
12
12
|
"""自定义关键字管理器
|
13
|
-
|
13
|
+
|
14
14
|
负责加载和注册自定义关键字
|
15
15
|
"""
|
16
|
-
|
16
|
+
|
17
17
|
def __init__(self):
|
18
18
|
"""初始化自定义关键字管理器"""
|
19
19
|
self.resource_cache = {} # 缓存已加载的资源文件
|
20
20
|
self.resource_paths = [] # 资源文件搜索路径
|
21
|
-
|
21
|
+
|
22
22
|
def add_resource_path(self, path: str) -> None:
|
23
23
|
"""添加资源文件搜索路径
|
24
|
-
|
24
|
+
|
25
25
|
Args:
|
26
26
|
path: 资源文件路径
|
27
27
|
"""
|
28
28
|
if path not in self.resource_paths:
|
29
29
|
self.resource_paths.append(path)
|
30
|
-
|
30
|
+
|
31
31
|
def load_resource_file(self, file_path: str) -> None:
|
32
32
|
"""加载资源文件
|
33
|
-
|
33
|
+
|
34
34
|
Args:
|
35
35
|
file_path: 资源文件路径
|
36
36
|
"""
|
37
37
|
# 规范化路径,解决路径叠加的问题
|
38
38
|
file_path = os.path.normpath(file_path)
|
39
|
-
|
39
|
+
|
40
40
|
# 如果已经缓存,则跳过
|
41
41
|
absolute_path = os.path.abspath(file_path)
|
42
42
|
if absolute_path in self.resource_cache:
|
43
43
|
return
|
44
|
-
|
44
|
+
|
45
45
|
# 读取文件内容
|
46
46
|
if not os.path.exists(file_path):
|
47
47
|
# 尝试在资源路径中查找
|
@@ -54,84 +54,85 @@ class CustomKeywordManager:
|
|
54
54
|
else:
|
55
55
|
# 如果文件不存在,尝试在根项目目录中查找
|
56
56
|
# 一般情况下文件路径可能是相对于项目根目录的
|
57
|
-
project_root = os.path.dirname(
|
57
|
+
project_root = os.path.dirname(
|
58
|
+
os.path.dirname(os.path.dirname(__file__)))
|
58
59
|
full_path = os.path.join(project_root, file_path)
|
59
60
|
if os.path.exists(full_path):
|
60
61
|
file_path = full_path
|
61
62
|
absolute_path = os.path.abspath(file_path)
|
62
63
|
else:
|
63
64
|
raise FileNotFoundError(f"资源文件不存在: {file_path}")
|
64
|
-
|
65
|
+
|
65
66
|
try:
|
66
67
|
with open(file_path, 'r', encoding='utf-8') as f:
|
67
68
|
content = f.read()
|
68
|
-
|
69
|
+
|
69
70
|
# 解析资源文件
|
70
71
|
lexer = get_lexer()
|
71
72
|
parser = get_parser()
|
72
73
|
ast = parser.parse(content, lexer=lexer)
|
73
|
-
|
74
|
+
|
74
75
|
# 标记为已加载
|
75
76
|
self.resource_cache[absolute_path] = True
|
76
|
-
|
77
|
+
|
77
78
|
# 处理导入指令
|
78
79
|
self._process_imports(ast, os.path.dirname(file_path))
|
79
|
-
|
80
|
+
|
80
81
|
# 注册关键字
|
81
82
|
self._register_keywords(ast, file_path)
|
82
83
|
except Exception as e:
|
83
84
|
print(f"资源文件 {file_path} 加载失败: {str(e)}")
|
84
85
|
raise
|
85
|
-
|
86
|
+
|
86
87
|
def _process_imports(self, ast: Node, base_dir: str) -> None:
|
87
88
|
"""处理资源文件中的导入指令
|
88
|
-
|
89
|
+
|
89
90
|
Args:
|
90
91
|
ast: 抽象语法树
|
91
92
|
base_dir: 基础目录
|
92
93
|
"""
|
93
94
|
if ast.type != 'Start' or not ast.children:
|
94
95
|
return
|
95
|
-
|
96
|
+
|
96
97
|
metadata_node = ast.children[0]
|
97
98
|
if metadata_node.type != 'Metadata':
|
98
99
|
return
|
99
|
-
|
100
|
+
|
100
101
|
for item in metadata_node.children:
|
101
102
|
if item.type == '@import':
|
102
103
|
imported_file = item.value
|
103
104
|
# 处理相对路径
|
104
105
|
if not os.path.isabs(imported_file):
|
105
106
|
imported_file = os.path.join(base_dir, imported_file)
|
106
|
-
|
107
|
+
|
107
108
|
# 规范化路径,避免路径叠加问题
|
108
109
|
imported_file = os.path.normpath(imported_file)
|
109
|
-
|
110
|
+
|
110
111
|
# 递归加载导入的资源文件
|
111
112
|
self.load_resource_file(imported_file)
|
112
|
-
|
113
|
+
|
113
114
|
def _register_keywords(self, ast: Node, file_path: str) -> None:
|
114
115
|
"""从AST中注册关键字
|
115
|
-
|
116
|
+
|
116
117
|
Args:
|
117
118
|
ast: 抽象语法树
|
118
119
|
file_path: 文件路径
|
119
120
|
"""
|
120
121
|
if ast.type != 'Start' or len(ast.children) < 2:
|
121
122
|
return
|
122
|
-
|
123
|
+
|
123
124
|
# 遍历语句节点
|
124
125
|
statements_node = ast.children[1]
|
125
126
|
if statements_node.type != 'Statements':
|
126
127
|
return
|
127
|
-
|
128
|
+
|
128
129
|
for node in statements_node.children:
|
129
130
|
if node.type == 'CustomKeyword':
|
130
131
|
self._register_custom_keyword(node, file_path)
|
131
|
-
|
132
|
+
|
132
133
|
def _register_custom_keyword(self, node: Node, file_path: str) -> None:
|
133
134
|
"""注册自定义关键字
|
134
|
-
|
135
|
+
|
135
136
|
Args:
|
136
137
|
node: 关键字节点
|
137
138
|
file_path: 资源文件路径
|
@@ -140,74 +141,74 @@ class CustomKeywordManager:
|
|
140
141
|
keyword_name = node.value
|
141
142
|
params_node = node.children[0]
|
142
143
|
body_node = node.children[1]
|
143
|
-
|
144
|
+
|
144
145
|
# 构建参数列表
|
145
146
|
parameters = []
|
146
147
|
param_mapping = {}
|
147
148
|
param_defaults = {} # 存储参数默认值
|
148
|
-
|
149
|
+
|
149
150
|
for param in params_node if params_node else []:
|
150
151
|
param_name = param.value
|
151
152
|
param_default = None
|
152
|
-
|
153
|
+
|
153
154
|
# 检查是否有默认值
|
154
155
|
if param.children and param.children[0]:
|
155
156
|
param_default = param.children[0].value
|
156
157
|
param_defaults[param_name] = param_default # 保存默认值
|
157
|
-
|
158
|
+
|
158
159
|
# 添加参数定义
|
159
160
|
parameters.append({
|
160
161
|
'name': param_name,
|
161
162
|
'mapping': param_name, # 中文参数名和内部参数名相同
|
162
163
|
'description': f'自定义关键字参数 {param_name}'
|
163
164
|
})
|
164
|
-
|
165
|
+
|
165
166
|
param_mapping[param_name] = param_name
|
166
|
-
|
167
|
+
|
167
168
|
# 注册自定义关键字到关键字管理器
|
168
169
|
@keyword_manager.register(keyword_name, parameters)
|
169
170
|
def custom_keyword_executor(**kwargs):
|
170
171
|
"""自定义关键字执行器"""
|
171
172
|
# 创建一个新的DSL执行器
|
172
173
|
executor = DSLExecutor()
|
173
|
-
|
174
|
+
|
175
|
+
# 导入ReturnException以避免循环导入
|
176
|
+
from pytest_dsl.core.dsl_executor import ReturnException
|
177
|
+
|
174
178
|
# 获取传递的上下文
|
175
179
|
context = kwargs.get('context')
|
176
180
|
if context:
|
177
181
|
executor.test_context = context
|
178
|
-
|
182
|
+
|
179
183
|
# 先应用默认值
|
180
184
|
for param_name, default_value in param_defaults.items():
|
181
185
|
executor.variables[param_name] = default_value
|
182
186
|
executor.test_context.set(param_name, default_value)
|
183
|
-
|
187
|
+
|
184
188
|
# 然后应用传入的参数值(覆盖默认值)
|
185
189
|
for param_name, param_mapping_name in param_mapping.items():
|
186
190
|
if param_mapping_name in kwargs:
|
187
191
|
# 确保参数值在标准变量和测试上下文中都可用
|
188
192
|
executor.variables[param_name] = kwargs[param_mapping_name]
|
189
|
-
executor.test_context.set(
|
190
|
-
|
193
|
+
executor.test_context.set(
|
194
|
+
param_name, kwargs[param_mapping_name])
|
195
|
+
|
191
196
|
# 执行关键字体中的语句
|
192
197
|
result = None
|
193
198
|
try:
|
194
199
|
for stmt in body_node.children:
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
break
|
200
|
-
else:
|
201
|
-
# 执行普通语句
|
202
|
-
executor.execute(stmt)
|
200
|
+
executor.execute(stmt)
|
201
|
+
except ReturnException as e:
|
202
|
+
# 捕获return异常,提取返回值
|
203
|
+
result = e.return_value
|
203
204
|
except Exception as e:
|
204
205
|
print(f"执行自定义关键字 {keyword_name} 时发生错误: {str(e)}")
|
205
206
|
raise
|
206
|
-
|
207
|
+
|
207
208
|
return result
|
208
|
-
|
209
|
+
|
209
210
|
print(f"已注册自定义关键字: {keyword_name} 来自文件: {file_path}")
|
210
211
|
|
211
212
|
|
212
213
|
# 创建全局自定义关键字管理器实例
|
213
|
-
custom_keyword_manager = CustomKeywordManager()
|
214
|
+
custom_keyword_manager = CustomKeywordManager()
|
pytest_dsl/core/dsl_executor.py
CHANGED
@@ -23,6 +23,14 @@ class ContinueException(Exception):
|
|
23
23
|
pass
|
24
24
|
|
25
25
|
|
26
|
+
class ReturnException(Exception):
|
27
|
+
"""Return控制流异常"""
|
28
|
+
|
29
|
+
def __init__(self, return_value=None):
|
30
|
+
self.return_value = return_value
|
31
|
+
super().__init__(f"Return with value: {return_value}")
|
32
|
+
|
33
|
+
|
26
34
|
class DSLExecutor:
|
27
35
|
"""DSL执行器,负责执行解析后的AST
|
28
36
|
|
@@ -30,12 +38,14 @@ class DSLExecutor:
|
|
30
38
|
- PYTEST_DSL_KEEP_VARIABLES=1: 执行完成后保留变量,用于单元测试中检查变量值
|
31
39
|
- PYTEST_DSL_KEEP_VARIABLES=0: (默认) 执行完成后清空变量,用于正常DSL执行
|
32
40
|
"""
|
41
|
+
|
33
42
|
def __init__(self):
|
34
43
|
"""初始化DSL执行器"""
|
35
44
|
self.variables = {}
|
36
45
|
self.test_context = TestContext()
|
37
46
|
self.test_context.executor = self # 让 test_context 能够访问到 executor
|
38
|
-
self.variable_replacer = VariableReplacer(
|
47
|
+
self.variable_replacer = VariableReplacer(
|
48
|
+
self.variables, self.test_context)
|
39
49
|
self.imported_files = set() # 跟踪已导入的文件,避免循环导入
|
40
50
|
|
41
51
|
def set_current_data(self, data):
|
@@ -279,7 +289,8 @@ class DSLExecutor:
|
|
279
289
|
# 导入自定义关键字管理器
|
280
290
|
from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
|
281
291
|
# 注册自定义关键字
|
282
|
-
custom_keyword_manager._register_custom_keyword(
|
292
|
+
custom_keyword_manager._register_custom_keyword(
|
293
|
+
stmt, "current_file")
|
283
294
|
|
284
295
|
def _handle_start(self, node):
|
285
296
|
"""处理开始节点"""
|
@@ -377,7 +388,8 @@ class DSLExecutor:
|
|
377
388
|
# 当 PYTEST_DSL_KEEP_VARIABLES=1 时,保留变量(用于单元测试)
|
378
389
|
# 否则清空变量(用于正常DSL执行)
|
379
390
|
import os
|
380
|
-
keep_variables = os.environ.get(
|
391
|
+
keep_variables = os.environ.get(
|
392
|
+
'PYTEST_DSL_KEEP_VARIABLES', '0') == '1'
|
381
393
|
|
382
394
|
if not keep_variables:
|
383
395
|
self.variables.clear()
|
@@ -387,7 +399,11 @@ class DSLExecutor:
|
|
387
399
|
def _handle_statements(self, node):
|
388
400
|
"""处理语句列表"""
|
389
401
|
for stmt in node.children:
|
390
|
-
|
402
|
+
try:
|
403
|
+
self.execute(stmt)
|
404
|
+
except ReturnException as e:
|
405
|
+
# 将return异常向上传递,不在这里处理
|
406
|
+
raise e
|
391
407
|
|
392
408
|
@allure.step("变量赋值")
|
393
409
|
def _handle_assignment(self, node):
|
@@ -491,6 +507,14 @@ class DSLExecutor:
|
|
491
507
|
attachment_type=allure.attachment_type.TEXT
|
492
508
|
)
|
493
509
|
continue
|
510
|
+
except ReturnException as e:
|
511
|
+
# 遇到return语句,将异常向上传递
|
512
|
+
allure.attach(
|
513
|
+
f"在 {var_name} = {i} 时遇到return语句,退出函数",
|
514
|
+
name="循环Return",
|
515
|
+
attachment_type=allure.attachment_type.TEXT
|
516
|
+
)
|
517
|
+
raise e
|
494
518
|
|
495
519
|
def _execute_keyword_call(self, node):
|
496
520
|
"""执行关键字调用"""
|
@@ -537,11 +561,12 @@ class DSLExecutor:
|
|
537
561
|
Args:
|
538
562
|
node: Return节点
|
539
563
|
|
540
|
-
|
541
|
-
|
564
|
+
Raises:
|
565
|
+
ReturnException: 抛出异常来实现return控制流
|
542
566
|
"""
|
543
567
|
expr_node = node.children[0]
|
544
|
-
|
568
|
+
return_value = self.eval_expression(expr_node)
|
569
|
+
raise ReturnException(return_value)
|
545
570
|
|
546
571
|
@allure.step("执行break语句")
|
547
572
|
def _handle_break(self, node):
|
@@ -580,7 +605,8 @@ class DSLExecutor:
|
|
580
605
|
if condition:
|
581
606
|
# 执行if分支
|
582
607
|
with allure.step("执行if分支"):
|
583
|
-
|
608
|
+
self.execute(node.children[1])
|
609
|
+
return
|
584
610
|
|
585
611
|
# 如果if条件为假,检查elif分支
|
586
612
|
for i in range(2, len(node.children)):
|
@@ -591,13 +617,15 @@ class DSLExecutor:
|
|
591
617
|
elif_condition = self.eval_expression(child.children[0])
|
592
618
|
if elif_condition:
|
593
619
|
with allure.step(f"执行elif分支 {i-1}"):
|
594
|
-
|
620
|
+
self.execute(child.children[1])
|
621
|
+
return
|
595
622
|
|
596
623
|
# 如果是普通的statements节点(else分支)
|
597
624
|
elif not hasattr(child, 'type') or child.type == 'Statements':
|
598
625
|
# 这是else分支,只有在所有前面的条件都为假时才执行
|
599
626
|
with allure.step("执行else分支"):
|
600
|
-
|
627
|
+
self.execute(child)
|
628
|
+
return
|
601
629
|
|
602
630
|
# 如果所有条件都为假且没有else分支,则不执行任何操作
|
603
631
|
return None
|
@@ -634,7 +662,8 @@ class DSLExecutor:
|
|
634
662
|
with allure.step(f"执行远程关键字: {alias}|{keyword_name}"):
|
635
663
|
try:
|
636
664
|
# 执行远程关键字
|
637
|
-
result = remote_keyword_manager.execute_remote_keyword(
|
665
|
+
result = remote_keyword_manager.execute_remote_keyword(
|
666
|
+
alias, keyword_name, **kwargs)
|
638
667
|
allure.attach(
|
639
668
|
f"远程关键字参数: {kwargs}\n"
|
640
669
|
f"远程关键字结果: {result}",
|
@@ -730,6 +759,7 @@ class DSLExecutor:
|
|
730
759
|
return handler(node)
|
731
760
|
raise Exception(f"未知的节点类型: {node.type}")
|
732
761
|
|
762
|
+
|
733
763
|
def read_file(filename):
|
734
764
|
"""读取 DSL 文件内容"""
|
735
765
|
with open(filename, 'r', encoding='utf-8') as f:
|
@@ -1,5 +1,5 @@
|
|
1
1
|
pytest_dsl/__init__.py,sha256=FzwXGvmuvMhRBKxvCdh1h-yJ2wUOnDxcTbU4Nt5fHn8,301
|
2
|
-
pytest_dsl/cli.py,sha256=
|
2
|
+
pytest_dsl/cli.py,sha256=4X-nq7TdBgFGN9oDBhgNHaoLBmTV0WQ_uxNY6cdRS4M,14944
|
3
3
|
pytest_dsl/conftest_adapter.py,sha256=cevEb0oEZKTZfUrGe1-CmkFByxKhUtjuurBJP7kpLc0,149
|
4
4
|
pytest_dsl/main_adapter.py,sha256=pUIPN_EzY3JCDlYK7yF_OeLDVqni8vtG15G7gVzPJXg,181
|
5
5
|
pytest_dsl/plugin.py,sha256=MEQcdK0xdxwxCxPEDLNHX_kGF9Jc7bNxlNi4mx588DU,1190
|
@@ -8,8 +8,8 @@ pytest_dsl/core/auth_provider.py,sha256=IZfXXrr4Uuc8QHwRPvhHSzNa2fwrqhjYts1xh78D
|
|
8
8
|
pytest_dsl/core/auto_decorator.py,sha256=9Mga-GB4AzV5nkB6zpfaq8IuHa0KOH8LlFvnWyH_tnU,6623
|
9
9
|
pytest_dsl/core/auto_directory.py,sha256=egyTnVxtGs4P75EIivRauLRPJfN9aZpoGVvp_Ma72AM,2714
|
10
10
|
pytest_dsl/core/context.py,sha256=fXpVQon666Lz_PCexjkWMBBr-Xcd1SkDMp2vHar6eIs,664
|
11
|
-
pytest_dsl/core/custom_keyword_manager.py,sha256=
|
12
|
-
pytest_dsl/core/dsl_executor.py,sha256
|
11
|
+
pytest_dsl/core/custom_keyword_manager.py,sha256=Y3MftSklqGSS3FfeZjmyfcGHfQw6FrhFTwgS_O8zQh4,7606
|
12
|
+
pytest_dsl/core/dsl_executor.py,sha256=aEEfocFCFxNDN1puBFhQzL5fzw9eZx8BAyYJI5XSt4Y,30472
|
13
13
|
pytest_dsl/core/dsl_executor_utils.py,sha256=cFoR2p3qQ2pb-UhkoefleK-zbuFqf0aBLh2Rlp8Ebs4,2180
|
14
14
|
pytest_dsl/core/global_context.py,sha256=NcEcS2V61MT70tgAsGsFWQq0P3mKjtHQr1rgT3yTcyY,3535
|
15
15
|
pytest_dsl/core/http_client.py,sha256=1AHqtM_fkXf8JrM0ljMsJwUkyt-ysjR16NoyCckHfGc,15810
|
@@ -59,9 +59,9 @@ pytest_dsl/keywords/assertion_keywords.py,sha256=WOCGP7WX2wZ6mQPDGmi38LWdG2NaThH
|
|
59
59
|
pytest_dsl/keywords/global_keywords.py,sha256=4yw5yeXoGf_4W26F39EA2Pp-mH9GiKGy2jKgFO9a_wM,2509
|
60
60
|
pytest_dsl/keywords/http_keywords.py,sha256=DY1SvUgOziN6Ga-QgE0q4XFd2qGGwvbv1B_k2gTdPLw,25554
|
61
61
|
pytest_dsl/keywords/system_keywords.py,sha256=n_jRrMvSv2v6Pm_amokfyLNVOLYP7CFWbBE3_dlO7h4,11299
|
62
|
-
pytest_dsl-0.
|
63
|
-
pytest_dsl-0.
|
64
|
-
pytest_dsl-0.
|
65
|
-
pytest_dsl-0.
|
66
|
-
pytest_dsl-0.
|
67
|
-
pytest_dsl-0.
|
62
|
+
pytest_dsl-0.9.0.dist-info/licenses/LICENSE,sha256=Rguy8cb9sYhK6cmrBdXvwh94rKVDh2tVZEWptsHIsVM,1071
|
63
|
+
pytest_dsl-0.9.0.dist-info/METADATA,sha256=it5pFZgvhb4OBllvC5vPxn-rezLeMsvGVtjPrtEDw6g,26712
|
64
|
+
pytest_dsl-0.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
65
|
+
pytest_dsl-0.9.0.dist-info/entry_points.txt,sha256=vuYIReCjHhS_cpaPAbQxOnFavQAwW0K66dKxUQSsRvc,190
|
66
|
+
pytest_dsl-0.9.0.dist-info/top_level.txt,sha256=4CrSx4uNqxj7NvK6k1y2JZrSrJSzi-UvPZdqpUhumWM,11
|
67
|
+
pytest_dsl-0.9.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|