pytest-dsl 0.12.1__py3-none-any.whl → 0.14.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 +59 -4
- pytest_dsl/core/custom_keyword_manager.py +250 -14
- pytest_dsl/core/dsl_executor.py +251 -25
- pytest_dsl/core/hook_manager.py +87 -0
- pytest_dsl/core/hookable_executor.py +134 -0
- pytest_dsl/core/hookable_keyword_manager.py +106 -0
- pytest_dsl/core/hookspecs.py +175 -0
- pytest_dsl/core/parser.py +7 -2
- pytest_dsl/core/parsetab.py +90 -89
- pytest_dsl/core/yaml_loader.py +142 -42
- pytest_dsl/core/yaml_vars.py +90 -7
- pytest_dsl/plugin.py +19 -2
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/METADATA +1 -1
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/RECORD +18 -14
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/top_level.txt +0 -0
pytest_dsl/cli.py
CHANGED
@@ -78,7 +78,7 @@ def parse_args():
|
|
78
78
|
list_parser.add_argument(
|
79
79
|
'--category',
|
80
80
|
choices=[
|
81
|
-
'builtin', 'plugin', 'custom',
|
81
|
+
'builtin', 'plugin', 'custom',
|
82
82
|
'project_custom', 'remote', 'all'
|
83
83
|
],
|
84
84
|
default='all',
|
@@ -108,7 +108,7 @@ def parse_args():
|
|
108
108
|
parser.add_argument(
|
109
109
|
'--category',
|
110
110
|
choices=[
|
111
|
-
'builtin', 'plugin', 'custom',
|
111
|
+
'builtin', 'plugin', 'custom',
|
112
112
|
'project_custom', 'remote', 'all'
|
113
113
|
],
|
114
114
|
default='all'
|
@@ -157,6 +157,29 @@ def load_all_keywords(include_remote=False):
|
|
157
157
|
# 扫描本地关键字
|
158
158
|
scan_local_keywords()
|
159
159
|
|
160
|
+
# 自动导入项目中的resources目录
|
161
|
+
try:
|
162
|
+
from pytest_dsl.core.custom_keyword_manager import (
|
163
|
+
custom_keyword_manager)
|
164
|
+
|
165
|
+
# 尝试从多个可能的项目根目录位置导入resources
|
166
|
+
possible_roots = [
|
167
|
+
os.getcwd(), # 当前工作目录
|
168
|
+
os.path.dirname(os.getcwd()), # 上级目录
|
169
|
+
]
|
170
|
+
|
171
|
+
# 尝试每个可能的根目录
|
172
|
+
for project_root in possible_roots:
|
173
|
+
if project_root and os.path.exists(project_root):
|
174
|
+
resources_dir = os.path.join(project_root, "resources")
|
175
|
+
if (os.path.exists(resources_dir) and
|
176
|
+
os.path.isdir(resources_dir)):
|
177
|
+
custom_keyword_manager.auto_import_resources_directory(
|
178
|
+
project_root)
|
179
|
+
break
|
180
|
+
except Exception as e:
|
181
|
+
print(f"自动导入resources目录时出现警告: {str(e)}")
|
182
|
+
|
160
183
|
# 扫描项目中的自定义关键字(.resource文件中定义的)
|
161
184
|
project_custom_keywords = scan_project_custom_keywords()
|
162
185
|
if project_custom_keywords:
|
@@ -701,12 +724,17 @@ def list_keywords(output_format='json', name_filter=None,
|
|
701
724
|
|
702
725
|
def load_yaml_variables(args):
|
703
726
|
"""从命令行参数加载YAML变量"""
|
704
|
-
#
|
727
|
+
# 使用统一的加载函数,包含远程服务器自动连接功能和hook支持
|
705
728
|
try:
|
729
|
+
# 尝试从环境变量获取环境名称
|
730
|
+
environment = (os.environ.get('PYTEST_DSL_ENVIRONMENT') or
|
731
|
+
os.environ.get('ENVIRONMENT'))
|
732
|
+
|
706
733
|
load_yaml_variables_from_args(
|
707
734
|
yaml_files=args.yaml_vars,
|
708
735
|
yaml_vars_dir=args.yaml_vars_dir,
|
709
|
-
project_root=os.getcwd() # CLI模式下使用当前工作目录作为项目根目录
|
736
|
+
project_root=os.getcwd(), # CLI模式下使用当前工作目录作为项目根目录
|
737
|
+
environment=environment
|
710
738
|
)
|
711
739
|
except Exception as e:
|
712
740
|
print(f"加载YAML变量失败: {str(e)}")
|
@@ -751,6 +779,33 @@ def run_dsl_tests(args):
|
|
751
779
|
# 加载YAML变量(包括远程服务器自动连接)
|
752
780
|
load_yaml_variables(args)
|
753
781
|
|
782
|
+
# 支持hook机制的执行
|
783
|
+
from pytest_dsl.core.hookable_executor import hookable_executor
|
784
|
+
|
785
|
+
# 检查是否有hook提供的用例列表
|
786
|
+
hook_cases = hookable_executor.list_dsl_cases()
|
787
|
+
if hook_cases:
|
788
|
+
# 如果有hook提供的用例,优先执行这些用例
|
789
|
+
print(f"通过Hook发现 {len(hook_cases)} 个DSL用例")
|
790
|
+
failures = 0
|
791
|
+
for case in hook_cases:
|
792
|
+
case_id = case.get('id') or case.get('name', 'unknown')
|
793
|
+
try:
|
794
|
+
print(f"执行用例: {case.get('name', case_id)}")
|
795
|
+
hookable_executor.execute_dsl(str(case_id))
|
796
|
+
print(f"✓ 用例 {case.get('name', case_id)} 执行成功")
|
797
|
+
except Exception as e:
|
798
|
+
print(f"✗ 用例 {case.get('name', case_id)} 执行失败: {e}")
|
799
|
+
failures += 1
|
800
|
+
|
801
|
+
if failures > 0:
|
802
|
+
print(f"总计 {failures}/{len(hook_cases)} 个测试失败")
|
803
|
+
sys.exit(1)
|
804
|
+
else:
|
805
|
+
print(f"所有 {len(hook_cases)} 个测试成功完成")
|
806
|
+
return
|
807
|
+
|
808
|
+
# 如果没有hook用例,使用传统的文件执行方式
|
754
809
|
lexer = get_lexer()
|
755
810
|
parser = get_parser()
|
756
811
|
executor = DSLExecutor()
|
@@ -15,6 +15,7 @@ class CustomKeywordManager:
|
|
15
15
|
"""初始化自定义关键字管理器"""
|
16
16
|
self.resource_cache = {} # 缓存已加载的资源文件
|
17
17
|
self.resource_paths = [] # 资源文件搜索路径
|
18
|
+
self.auto_imported_resources = set() # 记录已自动导入的资源文件
|
18
19
|
|
19
20
|
def add_resource_path(self, path: str) -> None:
|
20
21
|
"""添加资源文件搜索路径
|
@@ -25,6 +26,141 @@ class CustomKeywordManager:
|
|
25
26
|
if path not in self.resource_paths:
|
26
27
|
self.resource_paths.append(path)
|
27
28
|
|
29
|
+
def auto_import_resources_directory(
|
30
|
+
self, project_root: str = None) -> None:
|
31
|
+
"""自动导入项目中的resources目录
|
32
|
+
|
33
|
+
Args:
|
34
|
+
project_root: 项目根目录,默认为当前工作目录
|
35
|
+
"""
|
36
|
+
if project_root is None:
|
37
|
+
project_root = os.getcwd()
|
38
|
+
|
39
|
+
# 查找resources目录
|
40
|
+
resources_dir = os.path.join(project_root, "resources")
|
41
|
+
|
42
|
+
if (not os.path.exists(resources_dir) or
|
43
|
+
not os.path.isdir(resources_dir)):
|
44
|
+
# 如果没有resources目录,静默返回
|
45
|
+
return
|
46
|
+
|
47
|
+
print(f"发现resources目录: {resources_dir}")
|
48
|
+
|
49
|
+
# 递归查找所有.resource文件
|
50
|
+
resource_files = []
|
51
|
+
for root, dirs, files in os.walk(resources_dir):
|
52
|
+
for file in files:
|
53
|
+
if file.endswith('.resource'):
|
54
|
+
resource_files.append(os.path.join(root, file))
|
55
|
+
|
56
|
+
if not resource_files:
|
57
|
+
print("resources目录中没有找到.resource文件")
|
58
|
+
return
|
59
|
+
|
60
|
+
print(f"在resources目录中发现 {len(resource_files)} 个资源文件")
|
61
|
+
|
62
|
+
# 按照依赖关系排序并加载资源文件
|
63
|
+
sorted_files = self._sort_resources_by_dependencies(resource_files)
|
64
|
+
|
65
|
+
for resource_file in sorted_files:
|
66
|
+
try:
|
67
|
+
# 检查是否已经自动导入过
|
68
|
+
absolute_path = os.path.abspath(resource_file)
|
69
|
+
if absolute_path not in self.auto_imported_resources:
|
70
|
+
self.load_resource_file(resource_file)
|
71
|
+
self.auto_imported_resources.add(absolute_path)
|
72
|
+
print(f"自动导入资源文件: {resource_file}")
|
73
|
+
except Exception as e:
|
74
|
+
print(f"自动导入资源文件失败 {resource_file}: {e}")
|
75
|
+
|
76
|
+
def _sort_resources_by_dependencies(self, resource_files):
|
77
|
+
"""根据依赖关系对资源文件进行排序
|
78
|
+
|
79
|
+
Args:
|
80
|
+
resource_files: 资源文件列表
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
list: 按依赖关系排序后的资源文件列表
|
84
|
+
"""
|
85
|
+
# 简单的拓扑排序实现
|
86
|
+
dependencies = {}
|
87
|
+
all_files = set()
|
88
|
+
|
89
|
+
# 分析每个文件的依赖关系
|
90
|
+
for file_path in resource_files:
|
91
|
+
all_files.add(file_path)
|
92
|
+
dependencies[file_path] = self._extract_dependencies(file_path)
|
93
|
+
|
94
|
+
# 拓扑排序
|
95
|
+
sorted_files = []
|
96
|
+
visited = set()
|
97
|
+
temp_visited = set()
|
98
|
+
|
99
|
+
def visit(file_path):
|
100
|
+
if file_path in temp_visited:
|
101
|
+
# 检测到循环依赖,跳过
|
102
|
+
return
|
103
|
+
if file_path in visited:
|
104
|
+
return
|
105
|
+
|
106
|
+
temp_visited.add(file_path)
|
107
|
+
|
108
|
+
# 访问依赖的文件
|
109
|
+
for dep in dependencies.get(file_path, []):
|
110
|
+
if dep in all_files: # 只处理在当前文件列表中的依赖
|
111
|
+
visit(dep)
|
112
|
+
|
113
|
+
temp_visited.remove(file_path)
|
114
|
+
visited.add(file_path)
|
115
|
+
sorted_files.append(file_path)
|
116
|
+
|
117
|
+
# 访问所有文件
|
118
|
+
for file_path in resource_files:
|
119
|
+
if file_path not in visited:
|
120
|
+
visit(file_path)
|
121
|
+
|
122
|
+
return sorted_files
|
123
|
+
|
124
|
+
def _extract_dependencies(self, file_path):
|
125
|
+
"""提取资源文件的依赖关系
|
126
|
+
|
127
|
+
Args:
|
128
|
+
file_path: 资源文件路径
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
list: 依赖的文件路径列表
|
132
|
+
"""
|
133
|
+
dependencies = []
|
134
|
+
|
135
|
+
try:
|
136
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
137
|
+
content = f.read()
|
138
|
+
|
139
|
+
# 解析文件获取导入信息
|
140
|
+
lexer = get_lexer()
|
141
|
+
parser = get_parser()
|
142
|
+
ast = parser.parse(content, lexer=lexer)
|
143
|
+
|
144
|
+
if ast.type == 'Start' and ast.children:
|
145
|
+
metadata_node = ast.children[0]
|
146
|
+
if metadata_node.type == 'Metadata':
|
147
|
+
for item in metadata_node.children:
|
148
|
+
if item.type == '@import':
|
149
|
+
imported_file = item.value
|
150
|
+
# 处理相对路径
|
151
|
+
if not os.path.isabs(imported_file):
|
152
|
+
imported_file = os.path.join(
|
153
|
+
os.path.dirname(file_path), imported_file)
|
154
|
+
# 规范化路径
|
155
|
+
imported_file = os.path.normpath(imported_file)
|
156
|
+
dependencies.append(imported_file)
|
157
|
+
|
158
|
+
except Exception as e:
|
159
|
+
# 如果解析失败,返回空依赖列表
|
160
|
+
print(f"解析资源文件依赖失败 {file_path}: {e}")
|
161
|
+
|
162
|
+
return dependencies
|
163
|
+
|
28
164
|
def load_resource_file(self, file_path: str) -> None:
|
29
165
|
"""加载资源文件
|
30
166
|
|
@@ -64,23 +200,37 @@ class CustomKeywordManager:
|
|
64
200
|
with open(file_path, 'r', encoding='utf-8') as f:
|
65
201
|
content = f.read()
|
66
202
|
|
67
|
-
#
|
68
|
-
lexer = get_lexer()
|
69
|
-
parser = get_parser()
|
70
|
-
ast = parser.parse(content, lexer=lexer)
|
71
|
-
|
72
|
-
# 标记为已加载
|
203
|
+
# 标记为已加载(在解析前标记,避免循环导入)
|
73
204
|
self.resource_cache[absolute_path] = True
|
74
205
|
|
75
|
-
#
|
76
|
-
self.
|
206
|
+
# 使用公共方法解析和处理资源文件内容
|
207
|
+
self._process_resource_file_content(content, file_path)
|
77
208
|
|
78
|
-
# 注册关键字
|
79
|
-
self._register_keywords(ast, file_path)
|
80
209
|
except Exception as e:
|
210
|
+
# 如果处理失败,移除缓存标记
|
211
|
+
self.resource_cache.pop(absolute_path, None)
|
81
212
|
print(f"资源文件 {file_path} 加载失败: {str(e)}")
|
82
213
|
raise
|
83
214
|
|
215
|
+
def _process_resource_file_content(self, content: str,
|
216
|
+
file_path: str) -> None:
|
217
|
+
"""处理资源文件内容
|
218
|
+
|
219
|
+
Args:
|
220
|
+
content: 文件内容
|
221
|
+
file_path: 文件路径
|
222
|
+
"""
|
223
|
+
# 解析资源文件
|
224
|
+
lexer = get_lexer()
|
225
|
+
parser = get_parser()
|
226
|
+
ast = parser.parse(content, lexer=lexer)
|
227
|
+
|
228
|
+
# 处理导入指令
|
229
|
+
self._process_imports(ast, os.path.dirname(file_path))
|
230
|
+
|
231
|
+
# 注册关键字
|
232
|
+
self._register_keywords_from_ast(ast, file_path)
|
233
|
+
|
84
234
|
def _process_imports(self, ast: Node, base_dir: str) -> None:
|
85
235
|
"""处理资源文件中的导入指令
|
86
236
|
|
@@ -108,12 +258,13 @@ class CustomKeywordManager:
|
|
108
258
|
# 递归加载导入的资源文件
|
109
259
|
self.load_resource_file(imported_file)
|
110
260
|
|
111
|
-
def
|
112
|
-
|
261
|
+
def _register_keywords_from_ast(self, ast: Node,
|
262
|
+
source_name: str) -> None:
|
263
|
+
"""从AST中注册关键字(重构后的版本)
|
113
264
|
|
114
265
|
Args:
|
115
266
|
ast: 抽象语法树
|
116
|
-
|
267
|
+
source_name: 来源名称
|
117
268
|
"""
|
118
269
|
if ast.type != 'Start' or len(ast.children) < 2:
|
119
270
|
return
|
@@ -125,7 +276,7 @@ class CustomKeywordManager:
|
|
125
276
|
|
126
277
|
for node in statements_node.children:
|
127
278
|
if node.type in ['CustomKeyword', 'Function']:
|
128
|
-
self._register_custom_keyword(node,
|
279
|
+
self._register_custom_keyword(node, source_name)
|
129
280
|
|
130
281
|
def _register_custom_keyword(self, node: Node, file_path: str) -> None:
|
131
282
|
"""注册自定义关键字
|
@@ -206,6 +357,91 @@ class CustomKeywordManager:
|
|
206
357
|
|
207
358
|
print(f"已注册自定义关键字: {keyword_name} 来自文件: {file_path}")
|
208
359
|
|
360
|
+
def register_keyword_from_dsl_content(self, dsl_content: str,
|
361
|
+
source_name: str = "DSL内容") -> list:
|
362
|
+
"""从DSL内容注册关键字(公共方法)
|
363
|
+
|
364
|
+
Args:
|
365
|
+
dsl_content: DSL文本内容
|
366
|
+
source_name: 来源名称,用于日志显示
|
367
|
+
|
368
|
+
Returns:
|
369
|
+
list: 注册成功的关键字名称列表
|
370
|
+
|
371
|
+
Raises:
|
372
|
+
Exception: 解析或注册失败时抛出异常
|
373
|
+
"""
|
374
|
+
try:
|
375
|
+
# 解析DSL内容
|
376
|
+
lexer = get_lexer()
|
377
|
+
parser = get_parser()
|
378
|
+
ast = parser.parse(dsl_content, lexer=lexer)
|
379
|
+
|
380
|
+
# 收集注册前的关键字列表
|
381
|
+
existing_keywords = (
|
382
|
+
set(keyword_manager._keywords.keys())
|
383
|
+
if hasattr(keyword_manager, '_keywords')
|
384
|
+
else set()
|
385
|
+
)
|
386
|
+
|
387
|
+
# 使用统一的注册方法
|
388
|
+
self._register_keywords_from_ast(ast, source_name)
|
389
|
+
|
390
|
+
# 计算新注册的关键字
|
391
|
+
new_keywords = (
|
392
|
+
set(keyword_manager._keywords.keys())
|
393
|
+
if hasattr(keyword_manager, '_keywords')
|
394
|
+
else set()
|
395
|
+
)
|
396
|
+
registered_keywords = list(new_keywords - existing_keywords)
|
397
|
+
|
398
|
+
if not registered_keywords:
|
399
|
+
raise ValueError("在DSL内容中未找到任何关键字定义")
|
400
|
+
|
401
|
+
return registered_keywords
|
402
|
+
|
403
|
+
except Exception as e:
|
404
|
+
print(f"从DSL内容注册关键字失败(来源:{source_name}): {e}")
|
405
|
+
raise
|
406
|
+
|
407
|
+
def register_specific_keyword_from_dsl_content(
|
408
|
+
self, keyword_name: str, dsl_content: str,
|
409
|
+
source_name: str = "DSL内容") -> bool:
|
410
|
+
"""从DSL内容注册指定的关键字(公共方法)
|
411
|
+
|
412
|
+
Args:
|
413
|
+
keyword_name: 要注册的关键字名称
|
414
|
+
dsl_content: DSL文本内容
|
415
|
+
source_name: 来源名称,用于日志显示
|
416
|
+
|
417
|
+
Returns:
|
418
|
+
bool: 是否注册成功
|
419
|
+
|
420
|
+
Raises:
|
421
|
+
Exception: 解析失败或未找到指定关键字时抛出异常
|
422
|
+
"""
|
423
|
+
try:
|
424
|
+
# 解析DSL内容
|
425
|
+
lexer = get_lexer()
|
426
|
+
parser = get_parser()
|
427
|
+
ast = parser.parse(dsl_content, lexer=lexer)
|
428
|
+
|
429
|
+
# 查找指定的关键字定义
|
430
|
+
if ast.type == 'Start' and len(ast.children) >= 2:
|
431
|
+
statements_node = ast.children[1]
|
432
|
+
if statements_node.type == 'Statements':
|
433
|
+
for node in statements_node.children:
|
434
|
+
if (node.type in ['CustomKeyword', 'Function'] and
|
435
|
+
node.value == keyword_name):
|
436
|
+
self._register_custom_keyword(node, source_name)
|
437
|
+
return True
|
438
|
+
|
439
|
+
raise ValueError(f"在DSL内容中未找到关键字定义: {keyword_name}")
|
440
|
+
|
441
|
+
except Exception as e:
|
442
|
+
print(f"从DSL内容注册关键字失败 {keyword_name}(来源:{source_name}): {e}")
|
443
|
+
raise
|
444
|
+
|
209
445
|
|
210
446
|
# 创建全局自定义关键字管理器实例
|
211
447
|
custom_keyword_manager = CustomKeywordManager()
|