pytest-dsl 0.13.0__py3-none-any.whl → 0.15.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 +190 -3
- pytest_dsl/cli.py +37 -260
- pytest_dsl/core/custom_keyword_manager.py +114 -14
- pytest_dsl/core/dsl_executor.py +549 -166
- pytest_dsl/core/dsl_executor_utils.py +21 -22
- pytest_dsl/core/execution_tracker.py +291 -0
- 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/keyword_loader.py +402 -0
- pytest_dsl/core/keyword_manager.py +29 -23
- pytest_dsl/core/parser.py +94 -18
- pytest_dsl/core/validator.py +417 -0
- pytest_dsl/core/yaml_loader.py +142 -42
- pytest_dsl/core/yaml_vars.py +90 -7
- pytest_dsl/plugin.py +10 -1
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/METADATA +1 -1
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/RECORD +23 -16
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,417 @@
|
|
1
|
+
"""
|
2
|
+
pytest-dsl DSL格式校验模块
|
3
|
+
|
4
|
+
提供DSL语法验证、语义验证、关键字验证等功能
|
5
|
+
"""
|
6
|
+
|
7
|
+
import re
|
8
|
+
from typing import List, Dict, Optional, Tuple
|
9
|
+
from pytest_dsl.core.lexer import get_lexer
|
10
|
+
from pytest_dsl.core.parser import get_parser, Node, parse_with_error_handling
|
11
|
+
from pytest_dsl.core.keyword_manager import keyword_manager
|
12
|
+
|
13
|
+
|
14
|
+
class DSLValidationError:
|
15
|
+
"""DSL验证错误"""
|
16
|
+
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
error_type: str,
|
20
|
+
message: str,
|
21
|
+
line: Optional[int] = None,
|
22
|
+
column: Optional[int] = None,
|
23
|
+
suggestion: Optional[str] = None):
|
24
|
+
self.error_type = error_type
|
25
|
+
self.message = message
|
26
|
+
self.line = line
|
27
|
+
self.column = column
|
28
|
+
self.suggestion = suggestion
|
29
|
+
|
30
|
+
def __str__(self):
|
31
|
+
location = ""
|
32
|
+
if self.line is not None:
|
33
|
+
location = f"第{self.line}行"
|
34
|
+
if self.column is not None:
|
35
|
+
location += f"第{self.column}列"
|
36
|
+
location += ": "
|
37
|
+
|
38
|
+
result = f"{location}{self.error_type}: {self.message}"
|
39
|
+
if self.suggestion:
|
40
|
+
result += f"\n建议: {self.suggestion}"
|
41
|
+
return result
|
42
|
+
|
43
|
+
|
44
|
+
class DSLValidator:
|
45
|
+
"""DSL格式校验器"""
|
46
|
+
|
47
|
+
def __init__(self):
|
48
|
+
self.errors: List[DSLValidationError] = []
|
49
|
+
self.warnings: List[DSLValidationError] = []
|
50
|
+
|
51
|
+
def validate(self, content: str, dsl_id: Optional[str] = None
|
52
|
+
) -> Tuple[bool, List[DSLValidationError]]:
|
53
|
+
"""验证DSL内容
|
54
|
+
|
55
|
+
Args:
|
56
|
+
content: DSL内容
|
57
|
+
dsl_id: DSL标识符(可选)
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
(是否验证通过, 错误列表)
|
61
|
+
"""
|
62
|
+
self.errors = []
|
63
|
+
self.warnings = []
|
64
|
+
|
65
|
+
# 基础验证
|
66
|
+
self._validate_basic_format(content)
|
67
|
+
|
68
|
+
# 语法验证
|
69
|
+
ast = self._validate_syntax(content)
|
70
|
+
|
71
|
+
# 如果语法验证通过,进行语义验证
|
72
|
+
if ast and not self.errors:
|
73
|
+
self._validate_semantics(ast)
|
74
|
+
|
75
|
+
# 元数据验证
|
76
|
+
if ast and not self.errors:
|
77
|
+
self._validate_metadata(ast)
|
78
|
+
|
79
|
+
# 关键字验证
|
80
|
+
if ast and not self.errors:
|
81
|
+
self._validate_keywords(ast)
|
82
|
+
|
83
|
+
return len(self.errors) == 0, self.errors + self.warnings
|
84
|
+
|
85
|
+
def _validate_basic_format(self, content: str) -> None:
|
86
|
+
"""基础格式验证"""
|
87
|
+
if not content or not content.strip():
|
88
|
+
self.errors.append(DSLValidationError(
|
89
|
+
"格式错误", "DSL内容不能为空"
|
90
|
+
))
|
91
|
+
return
|
92
|
+
|
93
|
+
lines = content.split('\n')
|
94
|
+
|
95
|
+
# 检查编码
|
96
|
+
try:
|
97
|
+
content.encode('utf-8')
|
98
|
+
except UnicodeEncodeError as e:
|
99
|
+
self.errors.append(DSLValidationError(
|
100
|
+
"编码错误", f"DSL内容包含无效字符: {str(e)}"
|
101
|
+
))
|
102
|
+
|
103
|
+
# 检查行长度
|
104
|
+
for i, line in enumerate(lines, 1):
|
105
|
+
if len(line) > 1000:
|
106
|
+
self.warnings.append(DSLValidationError(
|
107
|
+
"格式警告", f"第{i}行过长,建议控制在1000字符以内", line=i
|
108
|
+
))
|
109
|
+
|
110
|
+
# 检查嵌套层级
|
111
|
+
max_indent = 0
|
112
|
+
for i, line in enumerate(lines, 1):
|
113
|
+
if line.strip():
|
114
|
+
indent = len(line) - len(line.lstrip())
|
115
|
+
if indent > max_indent:
|
116
|
+
max_indent = indent
|
117
|
+
|
118
|
+
if max_indent > 40: # 假设每层缩进4个空格,最多10层
|
119
|
+
self.warnings.append(DSLValidationError(
|
120
|
+
"格式警告", f"嵌套层级过深({max_indent//4}层),建议简化结构"
|
121
|
+
))
|
122
|
+
|
123
|
+
def _validate_syntax(self, content: str) -> Optional[Node]:
|
124
|
+
"""语法验证"""
|
125
|
+
try:
|
126
|
+
lexer = get_lexer()
|
127
|
+
ast, parse_errors = parse_with_error_handling(content, lexer)
|
128
|
+
|
129
|
+
# 如果有解析错误,添加到错误列表
|
130
|
+
if parse_errors:
|
131
|
+
for error in parse_errors:
|
132
|
+
self.errors.append(DSLValidationError(
|
133
|
+
"语法错误",
|
134
|
+
error['message'],
|
135
|
+
line=error['line'],
|
136
|
+
suggestion=self._suggest_syntax_fix(error['message'])
|
137
|
+
))
|
138
|
+
return None
|
139
|
+
|
140
|
+
return ast
|
141
|
+
|
142
|
+
except Exception as e:
|
143
|
+
error_msg = str(e)
|
144
|
+
line_num = self._extract_line_number(error_msg)
|
145
|
+
|
146
|
+
self.errors.append(DSLValidationError(
|
147
|
+
"语法错误",
|
148
|
+
error_msg,
|
149
|
+
line=line_num,
|
150
|
+
suggestion=self._suggest_syntax_fix(error_msg)
|
151
|
+
))
|
152
|
+
return None
|
153
|
+
|
154
|
+
def _validate_semantics(self, ast: Node) -> None:
|
155
|
+
"""语义验证"""
|
156
|
+
self._check_node_semantics(ast)
|
157
|
+
|
158
|
+
def _check_node_semantics(self, node: Node) -> None:
|
159
|
+
"""检查节点语义"""
|
160
|
+
if node.type == 'Assignment':
|
161
|
+
# 检查变量名
|
162
|
+
var_name = node.value
|
163
|
+
if not self._is_valid_variable_name(var_name):
|
164
|
+
self.errors.append(DSLValidationError(
|
165
|
+
"语义错误",
|
166
|
+
f"无效的变量名: {var_name}",
|
167
|
+
suggestion="变量名应以字母或下划线开头,只包含字母、数字、下划线或中文字符"
|
168
|
+
))
|
169
|
+
|
170
|
+
elif node.type == 'ForLoop':
|
171
|
+
# 检查循环变量名
|
172
|
+
loop_var = node.value
|
173
|
+
if not self._is_valid_variable_name(loop_var):
|
174
|
+
self.errors.append(DSLValidationError(
|
175
|
+
"语义错误",
|
176
|
+
f"无效的循环变量名: {loop_var}",
|
177
|
+
suggestion="循环变量名应以字母或下划线开头,只包含字母、数字、下划线或中文字符"
|
178
|
+
))
|
179
|
+
|
180
|
+
elif node.type == 'Expression':
|
181
|
+
# 检查表达式中的变量引用
|
182
|
+
if isinstance(node.value, str):
|
183
|
+
self._validate_variable_references(node.value)
|
184
|
+
|
185
|
+
# 递归检查子节点
|
186
|
+
for child in node.children:
|
187
|
+
if isinstance(child, Node):
|
188
|
+
self._check_node_semantics(child)
|
189
|
+
|
190
|
+
def _validate_metadata(self, ast: Node) -> None:
|
191
|
+
"""验证元数据"""
|
192
|
+
metadata_node = None
|
193
|
+
for child in ast.children:
|
194
|
+
if child.type == 'Metadata':
|
195
|
+
metadata_node = child
|
196
|
+
break
|
197
|
+
|
198
|
+
if not metadata_node:
|
199
|
+
self.warnings.append(DSLValidationError(
|
200
|
+
"元数据警告", "建议添加@name元数据以描述测试用例名称"
|
201
|
+
))
|
202
|
+
return
|
203
|
+
|
204
|
+
has_name = False
|
205
|
+
has_description = False
|
206
|
+
|
207
|
+
for item in metadata_node.children:
|
208
|
+
if item.type == '@name':
|
209
|
+
has_name = True
|
210
|
+
if not item.value or not item.value.strip():
|
211
|
+
self.errors.append(DSLValidationError(
|
212
|
+
"元数据错误", "@name不能为空"
|
213
|
+
))
|
214
|
+
elif item.type == '@description':
|
215
|
+
has_description = True
|
216
|
+
if not item.value or not item.value.strip():
|
217
|
+
self.warnings.append(DSLValidationError(
|
218
|
+
"元数据警告", "@description不应为空"
|
219
|
+
))
|
220
|
+
elif item.type == '@tags':
|
221
|
+
# 验证标签格式
|
222
|
+
if not item.value or len(item.value) == 0:
|
223
|
+
self.warnings.append(DSLValidationError(
|
224
|
+
"元数据警告", "@tags不应为空列表"
|
225
|
+
))
|
226
|
+
|
227
|
+
if not has_name:
|
228
|
+
self.warnings.append(DSLValidationError(
|
229
|
+
"元数据警告", "建议添加@name元数据以描述测试用例名称"
|
230
|
+
))
|
231
|
+
|
232
|
+
if not has_description:
|
233
|
+
self.warnings.append(DSLValidationError(
|
234
|
+
"元数据警告", "建议添加@description元数据以描述测试用例功能"
|
235
|
+
))
|
236
|
+
|
237
|
+
def _validate_keywords(self, ast: Node) -> None:
|
238
|
+
"""验证关键字"""
|
239
|
+
self._check_node_keywords(ast)
|
240
|
+
|
241
|
+
def _check_node_keywords(self, node: Node) -> None:
|
242
|
+
"""检查节点中的关键字"""
|
243
|
+
if node.type == 'KeywordCall':
|
244
|
+
keyword_name = node.value
|
245
|
+
keyword_info = keyword_manager.get_keyword_info(keyword_name)
|
246
|
+
|
247
|
+
if not keyword_info:
|
248
|
+
self.errors.append(DSLValidationError(
|
249
|
+
"关键字错误",
|
250
|
+
f"未注册的关键字: {keyword_name}",
|
251
|
+
suggestion=self._suggest_similar_keyword(keyword_name)
|
252
|
+
))
|
253
|
+
else:
|
254
|
+
# 验证参数
|
255
|
+
self._validate_keyword_parameters(node, keyword_info)
|
256
|
+
|
257
|
+
# 递归检查子节点
|
258
|
+
for child in node.children:
|
259
|
+
if isinstance(child, Node):
|
260
|
+
self._check_node_keywords(child)
|
261
|
+
|
262
|
+
def _validate_keyword_parameters(self, keyword_node: Node,
|
263
|
+
keyword_info: Dict) -> None:
|
264
|
+
"""验证关键字参数"""
|
265
|
+
if not keyword_node.children or not keyword_node.children[0]:
|
266
|
+
return
|
267
|
+
|
268
|
+
provided_params = set()
|
269
|
+
for param in keyword_node.children[0]:
|
270
|
+
param_name = param.value
|
271
|
+
provided_params.add(param_name)
|
272
|
+
|
273
|
+
# 检查参数名是否有效
|
274
|
+
mapping = keyword_info.get('mapping', {})
|
275
|
+
if param_name not in mapping:
|
276
|
+
self.errors.append(DSLValidationError(
|
277
|
+
"参数错误",
|
278
|
+
f"关键字 {keyword_node.value} 不支持参数: {param_name}",
|
279
|
+
suggestion=f"支持的参数: {', '.join(mapping.keys())}"
|
280
|
+
))
|
281
|
+
|
282
|
+
# 检查必需参数(这里简化处理,实际可能需要更复杂的逻辑)
|
283
|
+
required_params = set()
|
284
|
+
parameters = keyword_info.get('parameters', [])
|
285
|
+
for param in parameters:
|
286
|
+
if not hasattr(param, 'default') or param.default is None:
|
287
|
+
required_params.add(param.name)
|
288
|
+
|
289
|
+
missing_params = required_params - provided_params
|
290
|
+
if missing_params:
|
291
|
+
self.warnings.append(DSLValidationError(
|
292
|
+
"参数警告",
|
293
|
+
f"关键字 {keyword_node.value} 缺少建议参数: "
|
294
|
+
f"{', '.join(missing_params)}"
|
295
|
+
))
|
296
|
+
|
297
|
+
def _is_valid_variable_name(self, name: str) -> bool:
|
298
|
+
"""检查变量名是否有效"""
|
299
|
+
if not name:
|
300
|
+
return False
|
301
|
+
# 支持中文、英文、数字、下划线,以字母或下划线开头
|
302
|
+
pattern = r'^[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*$'
|
303
|
+
return bool(re.match(pattern, name))
|
304
|
+
|
305
|
+
def _validate_variable_references(self, text: str) -> None:
|
306
|
+
"""验证文本中的变量引用"""
|
307
|
+
# 匹配 ${变量名} 格式
|
308
|
+
pattern = r'\$\{([^}]+)\}'
|
309
|
+
matches = re.findall(pattern, text)
|
310
|
+
|
311
|
+
for var_ref in matches:
|
312
|
+
# 检查变量引用格式是否正确
|
313
|
+
if not self._is_valid_variable_reference(var_ref):
|
314
|
+
self.errors.append(DSLValidationError(
|
315
|
+
"变量引用错误",
|
316
|
+
f"无效的变量引用格式: ${{{var_ref}}}",
|
317
|
+
suggestion="变量引用应为 ${变量名} 格式,支持点号访问和数组索引"
|
318
|
+
))
|
319
|
+
|
320
|
+
def _is_valid_variable_reference(self, var_ref: str) -> bool:
|
321
|
+
"""检查变量引用是否有效"""
|
322
|
+
# 支持: variable, obj.prop, arr[0], dict["key"] 等格式
|
323
|
+
pattern = (r'^[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
|
324
|
+
r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
|
325
|
+
r'|(?:\[[^\]]+\]))*$')
|
326
|
+
return bool(re.match(pattern, var_ref))
|
327
|
+
|
328
|
+
def _extract_line_number(self, error_msg: str) -> Optional[int]:
|
329
|
+
"""从错误消息中提取行号"""
|
330
|
+
# 尝试匹配常见的行号模式
|
331
|
+
patterns = [
|
332
|
+
r'line (\d+)',
|
333
|
+
r'第(\d+)行',
|
334
|
+
r'在行 (\d+)',
|
335
|
+
r'at line (\d+)'
|
336
|
+
]
|
337
|
+
|
338
|
+
for pattern in patterns:
|
339
|
+
match = re.search(pattern, error_msg)
|
340
|
+
if match:
|
341
|
+
return int(match.group(1))
|
342
|
+
return None
|
343
|
+
|
344
|
+
def _suggest_syntax_fix(self, error_msg: str) -> Optional[str]:
|
345
|
+
"""根据错误消息建议语法修复"""
|
346
|
+
suggestions = {
|
347
|
+
"Syntax error": "检查语法是否正确,特别是括号、引号的匹配",
|
348
|
+
"unexpected token": "检查是否有多余或缺失的符号",
|
349
|
+
"Unexpected end of input": "检查是否缺少end关键字或右括号",
|
350
|
+
"illegal character": "检查是否有非法字符,确保使用UTF-8编码"
|
351
|
+
}
|
352
|
+
|
353
|
+
for key, suggestion in suggestions.items():
|
354
|
+
if key.lower() in error_msg.lower():
|
355
|
+
return suggestion
|
356
|
+
return None
|
357
|
+
|
358
|
+
def _suggest_similar_keyword(self, keyword_name: str) -> Optional[str]:
|
359
|
+
"""建议相似的关键字"""
|
360
|
+
all_keywords = list(keyword_manager._keywords.keys())
|
361
|
+
|
362
|
+
# 简单的相似度匹配(可以使用更复杂的算法)
|
363
|
+
similar_keywords = []
|
364
|
+
for kw in all_keywords:
|
365
|
+
similarity = self._calculate_similarity(
|
366
|
+
keyword_name.lower(), kw.lower())
|
367
|
+
if similarity > 0.6:
|
368
|
+
similar_keywords.append(kw)
|
369
|
+
|
370
|
+
if similar_keywords:
|
371
|
+
return f"您是否想使用: {', '.join(similar_keywords[:3])}"
|
372
|
+
return None
|
373
|
+
|
374
|
+
def _calculate_similarity(self, s1: str, s2: str) -> float:
|
375
|
+
"""计算字符串相似度(简单的Jaccard相似度)"""
|
376
|
+
if not s1 or not s2:
|
377
|
+
return 0.0
|
378
|
+
|
379
|
+
set1 = set(s1)
|
380
|
+
set2 = set(s2)
|
381
|
+
intersection = len(set1.intersection(set2))
|
382
|
+
union = len(set1.union(set2))
|
383
|
+
|
384
|
+
return intersection / union if union > 0 else 0.0
|
385
|
+
|
386
|
+
|
387
|
+
def validate_dsl(content: str, dsl_id: Optional[str] = None
|
388
|
+
) -> Tuple[bool, List[DSLValidationError]]:
|
389
|
+
"""验证DSL内容的便捷函数
|
390
|
+
|
391
|
+
Args:
|
392
|
+
content: DSL内容
|
393
|
+
dsl_id: DSL标识符(可选)
|
394
|
+
|
395
|
+
Returns:
|
396
|
+
(是否验证通过, 错误列表)
|
397
|
+
"""
|
398
|
+
validator = DSLValidator()
|
399
|
+
return validator.validate(content, dsl_id)
|
400
|
+
|
401
|
+
|
402
|
+
def check_dsl_syntax(content: str) -> bool:
|
403
|
+
"""快速检查DSL语法是否正确
|
404
|
+
|
405
|
+
Args:
|
406
|
+
content: DSL内容
|
407
|
+
|
408
|
+
Returns:
|
409
|
+
语法是否正确
|
410
|
+
"""
|
411
|
+
try:
|
412
|
+
lexer = get_lexer()
|
413
|
+
parser = get_parser()
|
414
|
+
parser.parse(content, lexer=lexer)
|
415
|
+
return True
|
416
|
+
except Exception:
|
417
|
+
return False
|
pytest_dsl/core/yaml_loader.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""YAML变量加载器模块
|
2
2
|
|
3
3
|
该模块负责处理YAML变量文件的加载和管理,支持从命令行参数加载单个文件或目录。
|
4
|
+
同时支持通过hook机制从外部系统动态加载变量。
|
4
5
|
"""
|
5
6
|
|
6
7
|
import os
|
@@ -29,14 +30,25 @@ def add_yaml_options(parser):
|
|
29
30
|
)
|
30
31
|
|
31
32
|
|
32
|
-
def load_yaml_variables_from_args(yaml_files=None, yaml_vars_dir=None,
|
33
|
+
def load_yaml_variables_from_args(yaml_files=None, yaml_vars_dir=None,
|
34
|
+
project_root=None, environment=None):
|
33
35
|
"""从参数加载YAML变量文件(通用函数)
|
34
36
|
|
35
37
|
Args:
|
36
38
|
yaml_files: YAML文件列表
|
37
39
|
yaml_vars_dir: YAML变量目录路径
|
38
40
|
project_root: 项目根目录(用于默认config目录)
|
41
|
+
environment: 环境名称(用于hook加载)
|
39
42
|
"""
|
43
|
+
# 首先尝试通过hook加载变量
|
44
|
+
hook_variables = _load_variables_through_hooks(
|
45
|
+
project_root=project_root, environment=environment)
|
46
|
+
|
47
|
+
if hook_variables:
|
48
|
+
print(f"通过Hook加载了 {len(hook_variables)} 个变量")
|
49
|
+
# 将hook变量加载到yaml_vars中
|
50
|
+
yaml_vars._variables.update(hook_variables)
|
51
|
+
|
40
52
|
# 加载单个YAML文件
|
41
53
|
if yaml_files:
|
42
54
|
yaml_vars.load_yaml_files(yaml_files)
|
@@ -55,7 +67,8 @@ def load_yaml_variables_from_args(yaml_files=None, yaml_vars_dir=None, project_r
|
|
55
67
|
if loaded_files:
|
56
68
|
# 过滤出当前目录的文件
|
57
69
|
if yaml_vars_dir:
|
58
|
-
dir_files = [f for f in loaded_files if Path(
|
70
|
+
dir_files = [f for f in loaded_files if Path(
|
71
|
+
f).parent == Path(yaml_vars_dir)]
|
59
72
|
if dir_files:
|
60
73
|
print(f"目录中加载的文件: {', '.join(dir_files)}")
|
61
74
|
else:
|
@@ -67,6 +80,103 @@ def load_yaml_variables_from_args(yaml_files=None, yaml_vars_dir=None, project_r
|
|
67
80
|
load_remote_servers_from_yaml()
|
68
81
|
|
69
82
|
|
83
|
+
def _load_variables_through_hooks(project_root=None, environment=None):
|
84
|
+
"""通过hook机制加载变量
|
85
|
+
|
86
|
+
Args:
|
87
|
+
project_root: 项目根目录
|
88
|
+
environment: 环境名称
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
dict: 通过hook加载的变量字典
|
92
|
+
"""
|
93
|
+
try:
|
94
|
+
from .hook_manager import hook_manager
|
95
|
+
|
96
|
+
# 确保hook管理器已初始化
|
97
|
+
hook_manager.initialize()
|
98
|
+
|
99
|
+
# 如果没有已注册的插件,直接返回
|
100
|
+
if not hook_manager.get_plugins():
|
101
|
+
return {}
|
102
|
+
|
103
|
+
# 提取project_id(如果可以从项目根目录推断)
|
104
|
+
project_id = None
|
105
|
+
if project_root:
|
106
|
+
# 可以根据项目结构推断project_id,这里暂时不实现
|
107
|
+
pass
|
108
|
+
|
109
|
+
# 通过hook加载变量
|
110
|
+
hook_variables = {}
|
111
|
+
|
112
|
+
# 调用dsl_load_variables hook
|
113
|
+
try:
|
114
|
+
variable_results = hook_manager.pm.hook.dsl_load_variables(
|
115
|
+
project_id=project_id,
|
116
|
+
environment=environment,
|
117
|
+
filters={}
|
118
|
+
)
|
119
|
+
|
120
|
+
# 合并所有hook返回的变量
|
121
|
+
for result in variable_results:
|
122
|
+
if result and isinstance(result, dict):
|
123
|
+
hook_variables.update(result)
|
124
|
+
|
125
|
+
except Exception as e:
|
126
|
+
print(f"通过Hook加载变量时出现警告: {e}")
|
127
|
+
|
128
|
+
# 列出变量源(用于调试)
|
129
|
+
try:
|
130
|
+
source_results = hook_manager.pm.hook.dsl_list_variable_sources(
|
131
|
+
project_id=project_id
|
132
|
+
)
|
133
|
+
|
134
|
+
sources = []
|
135
|
+
for result in source_results:
|
136
|
+
if result and isinstance(result, list):
|
137
|
+
sources.extend(result)
|
138
|
+
|
139
|
+
if sources:
|
140
|
+
print(f"发现 {len(sources)} 个变量源")
|
141
|
+
for source in sources:
|
142
|
+
source_name = source.get('name', '未知')
|
143
|
+
source_type = source.get('type', '未知')
|
144
|
+
print(f" - {source_name} ({source_type})")
|
145
|
+
|
146
|
+
except Exception as e:
|
147
|
+
print(f"列出变量源时出现警告: {e}")
|
148
|
+
|
149
|
+
# 验证变量(如果有变量的话)
|
150
|
+
if hook_variables:
|
151
|
+
try:
|
152
|
+
validation_results = hook_manager.pm.hook.dsl_validate_variables(
|
153
|
+
variables=hook_variables,
|
154
|
+
project_id=project_id
|
155
|
+
)
|
156
|
+
|
157
|
+
validation_errors = []
|
158
|
+
for result in validation_results:
|
159
|
+
if result and isinstance(result, list):
|
160
|
+
validation_errors.extend(result)
|
161
|
+
|
162
|
+
if validation_errors:
|
163
|
+
print(f"变量验证发现 {len(validation_errors)} 个问题:")
|
164
|
+
for error in validation_errors:
|
165
|
+
print(f" - {error}")
|
166
|
+
|
167
|
+
except Exception as e:
|
168
|
+
print(f"验证变量时出现警告: {e}")
|
169
|
+
|
170
|
+
return hook_variables
|
171
|
+
|
172
|
+
except ImportError:
|
173
|
+
# 如果没有安装pluggy或hook_manager不可用,跳过hook加载
|
174
|
+
return {}
|
175
|
+
except Exception as e:
|
176
|
+
print(f"Hook变量加载失败: {e}")
|
177
|
+
return {}
|
178
|
+
|
179
|
+
|
70
180
|
def load_yaml_variables(config):
|
71
181
|
"""加载YAML变量文件(pytest插件接口)
|
72
182
|
|
@@ -80,60 +190,50 @@ def load_yaml_variables(config):
|
|
80
190
|
yaml_vars_dir = config.getoption('--yaml-vars-dir')
|
81
191
|
project_root = config.rootdir
|
82
192
|
|
193
|
+
# 尝试从环境变量获取环境名称
|
194
|
+
environment = os.environ.get(
|
195
|
+
'PYTEST_DSL_ENVIRONMENT') or os.environ.get('ENVIRONMENT')
|
196
|
+
|
83
197
|
# 调用通用加载函数
|
84
198
|
load_yaml_variables_from_args(
|
85
199
|
yaml_files=yaml_files,
|
86
200
|
yaml_vars_dir=yaml_vars_dir,
|
87
|
-
project_root=project_root
|
201
|
+
project_root=project_root,
|
202
|
+
environment=environment
|
88
203
|
)
|
89
204
|
|
90
205
|
|
91
206
|
def load_remote_servers_from_yaml():
|
92
|
-
"""从YAML
|
93
|
-
|
94
|
-
检查YAML变量中是否包含remote_servers配置,如果有则自动连接这些服务器。
|
95
|
-
"""
|
207
|
+
"""从YAML变量中加载远程服务器配置"""
|
96
208
|
try:
|
209
|
+
from pytest_dsl.remote.keyword_client import remote_keyword_manager
|
210
|
+
|
97
211
|
# 获取远程服务器配置
|
98
|
-
|
99
|
-
if not
|
100
|
-
|
212
|
+
remote_servers = yaml_vars.get_variable('remote_servers')
|
213
|
+
if not remote_servers:
|
101
214
|
return
|
102
215
|
|
103
|
-
print(f"发现 {len(
|
104
|
-
|
105
|
-
# 导入远程关键字管理器
|
106
|
-
from pytest_dsl.remote.keyword_client import remote_keyword_manager
|
216
|
+
print(f"发现 {len(remote_servers)} 个远程服务器配置")
|
107
217
|
|
108
|
-
#
|
109
|
-
for
|
110
|
-
|
218
|
+
# 注册远程服务器
|
219
|
+
for server_config in remote_servers:
|
220
|
+
if isinstance(server_config, dict):
|
111
221
|
url = server_config.get('url')
|
112
|
-
alias = server_config.get('alias'
|
222
|
+
alias = server_config.get('alias')
|
113
223
|
api_key = server_config.get('api_key')
|
114
|
-
sync_config = server_config.get('sync_config')
|
115
|
-
|
116
|
-
if not url:
|
117
|
-
print(f"跳过服务器 {server_name}: 缺少URL配置")
|
118
|
-
continue
|
119
|
-
|
120
|
-
print(f"正在连接远程服务器: {server_name} ({url}) 别名: {alias}")
|
121
|
-
|
122
|
-
# 注册远程服务器
|
123
|
-
success = remote_keyword_manager.register_remote_server(
|
124
|
-
url=url,
|
125
|
-
alias=alias,
|
126
|
-
api_key=api_key,
|
127
|
-
sync_config=sync_config
|
128
|
-
)
|
129
|
-
|
130
|
-
if success:
|
131
|
-
print(f"成功连接到远程服务器: {server_name} ({url})")
|
132
|
-
else:
|
133
|
-
print(f"连接远程服务器失败: {server_name} ({url})")
|
134
|
-
|
135
|
-
except Exception as e:
|
136
|
-
print(f"连接远程服务器 {server_name} 时发生错误: {str(e)}")
|
137
224
|
|
225
|
+
if url and alias:
|
226
|
+
print(f"自动连接远程服务器: {alias} -> {url}")
|
227
|
+
success = remote_keyword_manager.register_remote_server(
|
228
|
+
url, alias, api_key=api_key
|
229
|
+
)
|
230
|
+
if success:
|
231
|
+
print(f"✓ 远程服务器 {alias} 连接成功")
|
232
|
+
else:
|
233
|
+
print(f"✗ 远程服务器 {alias} 连接失败")
|
234
|
+
|
235
|
+
except ImportError:
|
236
|
+
# 如果远程功能不可用,跳过
|
237
|
+
pass
|
138
238
|
except Exception as e:
|
139
|
-
print(f"
|
239
|
+
print(f"自动连接远程服务器时出现警告: {e}")
|