pytest-dsl 0.8.0__py3-none-any.whl → 0.9.1__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 +362 -15
- pytest_dsl/core/custom_keyword_manager.py +49 -48
- pytest_dsl/core/dsl_executor.py +41 -11
- pytest_dsl/remote/__init__.py +7 -0
- pytest_dsl/remote/hook_manager.py +155 -0
- pytest_dsl/remote/keyword_client.py +376 -0
- pytest_dsl/remote/keyword_server.py +593 -0
- pytest_dsl/remote/variable_bridge.py +164 -0
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.1.dist-info}/METADATA +1 -1
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.1.dist-info}/RECORD +14 -9
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.1.dist-info}/entry_points.txt +1 -0
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.1.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.8.0.dist-info → pytest_dsl-0.9.1.dist-info}/top_level.txt +0 -0
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:
|
@@ -0,0 +1,155 @@
|
|
1
|
+
"""远程服务器Hook管理器
|
2
|
+
|
3
|
+
该模块提供了远程服务器的hook机制,支持在服务器生命周期的关键点执行自定义逻辑。
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import Dict, List, Callable, Any
|
8
|
+
from enum import Enum
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class HookType(Enum):
|
14
|
+
"""Hook类型枚举"""
|
15
|
+
SERVER_STARTUP = "server_startup"
|
16
|
+
SERVER_SHUTDOWN = "server_shutdown"
|
17
|
+
BEFORE_KEYWORD_EXECUTION = "before_keyword_execution"
|
18
|
+
AFTER_KEYWORD_EXECUTION = "after_keyword_execution"
|
19
|
+
|
20
|
+
|
21
|
+
class HookContext:
|
22
|
+
"""Hook执行上下文"""
|
23
|
+
|
24
|
+
def __init__(self, hook_type: HookType, **kwargs):
|
25
|
+
self.hook_type = hook_type
|
26
|
+
self.data = kwargs
|
27
|
+
self.shared_variables = kwargs.get('shared_variables', {})
|
28
|
+
|
29
|
+
def get(self, key: str, default=None):
|
30
|
+
"""获取上下文数据"""
|
31
|
+
return self.data.get(key, default)
|
32
|
+
|
33
|
+
def set(self, key: str, value: Any):
|
34
|
+
"""设置上下文数据"""
|
35
|
+
self.data[key] = value
|
36
|
+
|
37
|
+
def get_shared_variable(self, name: str, default=None):
|
38
|
+
"""获取共享变量"""
|
39
|
+
return self.shared_variables.get(name, default)
|
40
|
+
|
41
|
+
|
42
|
+
class HookManager:
|
43
|
+
"""Hook管理器"""
|
44
|
+
|
45
|
+
def __init__(self):
|
46
|
+
self._hooks: Dict[HookType, List[Callable]] = {
|
47
|
+
HookType.SERVER_STARTUP: [],
|
48
|
+
HookType.SERVER_SHUTDOWN: [],
|
49
|
+
HookType.BEFORE_KEYWORD_EXECUTION: [],
|
50
|
+
HookType.AFTER_KEYWORD_EXECUTION: []
|
51
|
+
}
|
52
|
+
|
53
|
+
def register_hook(self, hook_type: HookType, hook_func: Callable):
|
54
|
+
"""注册hook函数
|
55
|
+
|
56
|
+
Args:
|
57
|
+
hook_type: Hook类型
|
58
|
+
hook_func: Hook函数,接收HookContext参数
|
59
|
+
"""
|
60
|
+
if hook_type not in self._hooks:
|
61
|
+
raise ValueError(f"不支持的hook类型: {hook_type}")
|
62
|
+
|
63
|
+
self._hooks[hook_type].append(hook_func)
|
64
|
+
logger.info(f"注册hook: {hook_type.value} -> {hook_func.__name__}")
|
65
|
+
|
66
|
+
def execute_hooks(self, hook_type: HookType, **context_data) -> HookContext:
|
67
|
+
"""执行指定类型的所有hook
|
68
|
+
|
69
|
+
Args:
|
70
|
+
hook_type: Hook类型
|
71
|
+
**context_data: 传递给hook的上下文数据
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
HookContext: 执行后的上下文
|
75
|
+
"""
|
76
|
+
context = HookContext(hook_type, **context_data)
|
77
|
+
|
78
|
+
hooks = self._hooks.get(hook_type, [])
|
79
|
+
if not hooks:
|
80
|
+
logger.debug(f"没有注册的hook: {hook_type.value}")
|
81
|
+
return context
|
82
|
+
|
83
|
+
logger.debug(f"执行{len(hooks)}个hook: {hook_type.value}")
|
84
|
+
|
85
|
+
for hook_func in hooks:
|
86
|
+
try:
|
87
|
+
logger.debug(f"执行hook: {hook_func.__name__}")
|
88
|
+
hook_func(context)
|
89
|
+
except Exception as e:
|
90
|
+
logger.error(f"Hook执行失败 {hook_func.__name__}: {str(e)}")
|
91
|
+
# 继续执行其他hook,不因为一个hook失败而中断
|
92
|
+
|
93
|
+
return context
|
94
|
+
|
95
|
+
def get_registered_hooks(self, hook_type: HookType = None) -> Dict[HookType, List[str]]:
|
96
|
+
"""获取已注册的hook信息
|
97
|
+
|
98
|
+
Args:
|
99
|
+
hook_type: 指定hook类型,None表示获取所有
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Dict: hook类型到函数名列表的映射
|
103
|
+
"""
|
104
|
+
result = {}
|
105
|
+
|
106
|
+
if hook_type:
|
107
|
+
hooks = self._hooks.get(hook_type, [])
|
108
|
+
result[hook_type] = [hook.__name__ for hook in hooks]
|
109
|
+
else:
|
110
|
+
for ht, hooks in self._hooks.items():
|
111
|
+
result[ht] = [hook.__name__ for hook in hooks]
|
112
|
+
|
113
|
+
return result
|
114
|
+
|
115
|
+
def clear_hooks(self, hook_type: HookType = None):
|
116
|
+
"""清除hook
|
117
|
+
|
118
|
+
Args:
|
119
|
+
hook_type: 指定hook类型,None表示清除所有
|
120
|
+
"""
|
121
|
+
if hook_type:
|
122
|
+
self._hooks[hook_type] = []
|
123
|
+
logger.info(f"清除hook: {hook_type.value}")
|
124
|
+
else:
|
125
|
+
for ht in self._hooks:
|
126
|
+
self._hooks[ht] = []
|
127
|
+
logger.info("清除所有hook")
|
128
|
+
|
129
|
+
|
130
|
+
# 全局hook管理器实例
|
131
|
+
hook_manager = HookManager()
|
132
|
+
|
133
|
+
|
134
|
+
def register_startup_hook(func: Callable):
|
135
|
+
"""装饰器:注册服务器启动hook"""
|
136
|
+
hook_manager.register_hook(HookType.SERVER_STARTUP, func)
|
137
|
+
return func
|
138
|
+
|
139
|
+
|
140
|
+
def register_shutdown_hook(func: Callable):
|
141
|
+
"""装饰器:注册服务器关闭hook"""
|
142
|
+
hook_manager.register_hook(HookType.SERVER_SHUTDOWN, func)
|
143
|
+
return func
|
144
|
+
|
145
|
+
|
146
|
+
def register_before_keyword_hook(func: Callable):
|
147
|
+
"""装饰器:注册关键字执行前hook"""
|
148
|
+
hook_manager.register_hook(HookType.BEFORE_KEYWORD_EXECUTION, func)
|
149
|
+
return func
|
150
|
+
|
151
|
+
|
152
|
+
def register_after_keyword_hook(func: Callable):
|
153
|
+
"""装饰器:注册关键字执行后hook"""
|
154
|
+
hook_manager.register_hook(HookType.AFTER_KEYWORD_EXECUTION, func)
|
155
|
+
return func
|
@@ -0,0 +1,376 @@
|
|
1
|
+
import xmlrpc.client
|
2
|
+
from functools import partial
|
3
|
+
import logging
|
4
|
+
|
5
|
+
from pytest_dsl.core.keyword_manager import keyword_manager, Parameter
|
6
|
+
|
7
|
+
# 配置日志
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
class RemoteKeywordClient:
|
11
|
+
"""远程关键字客户端,用于连接远程关键字服务器并执行关键字"""
|
12
|
+
|
13
|
+
def __init__(self, url='http://localhost:8270/', api_key=None, alias=None, sync_config=None):
|
14
|
+
self.url = url
|
15
|
+
self.server = xmlrpc.client.ServerProxy(url, allow_none=True)
|
16
|
+
self.keyword_cache = {}
|
17
|
+
self.param_mappings = {} # 存储每个关键字的参数映射
|
18
|
+
self.api_key = api_key
|
19
|
+
self.alias = alias or url.replace('http://', '').replace('https://', '').split(':')[0]
|
20
|
+
|
21
|
+
# 变量传递配置(简化版)
|
22
|
+
self.sync_config = sync_config or {
|
23
|
+
'sync_global_vars': True, # 连接时传递全局变量(g_开头)
|
24
|
+
'sync_yaml_vars': True, # 连接时传递YAML配置变量
|
25
|
+
'yaml_sync_keys': None, # 指定要同步的YAML键列表,None表示同步所有(除了排除的)
|
26
|
+
'yaml_exclude_patterns': [ # 排除包含这些模式的YAML变量
|
27
|
+
'password', 'secret', 'key', 'token', 'credential', 'auth',
|
28
|
+
'private', 'remote_servers' # 排除远程服务器配置避免循环
|
29
|
+
]
|
30
|
+
}
|
31
|
+
|
32
|
+
def connect(self):
|
33
|
+
"""连接到远程服务器并获取可用关键字"""
|
34
|
+
try:
|
35
|
+
print(f"RemoteKeywordClient: 正在连接到远程服务器 {self.url}")
|
36
|
+
keyword_names = self.server.get_keyword_names()
|
37
|
+
print(f"RemoteKeywordClient: 获取到 {len(keyword_names)} 个关键字")
|
38
|
+
for name in keyword_names:
|
39
|
+
self._register_remote_keyword(name)
|
40
|
+
|
41
|
+
# 连接时传递变量到远程服务器
|
42
|
+
self._send_initial_variables()
|
43
|
+
|
44
|
+
logger.info(f"已连接到远程关键字服务器: {self.url}, 别名: {self.alias}")
|
45
|
+
print(f"RemoteKeywordClient: 成功连接到远程服务器 {self.url}, 别名: {self.alias}")
|
46
|
+
return True
|
47
|
+
except Exception as e:
|
48
|
+
error_msg = f"连接远程关键字服务器失败: {str(e)}"
|
49
|
+
logger.error(error_msg)
|
50
|
+
print(f"RemoteKeywordClient: {error_msg}")
|
51
|
+
return False
|
52
|
+
|
53
|
+
def _register_remote_keyword(self, name):
|
54
|
+
"""注册远程关键字到本地关键字管理器"""
|
55
|
+
# 获取关键字参数信息
|
56
|
+
try:
|
57
|
+
param_names = self.server.get_keyword_arguments(name)
|
58
|
+
doc = self.server.get_keyword_documentation(name)
|
59
|
+
|
60
|
+
print(f"注册远程关键字: {name}, 参数: {param_names}")
|
61
|
+
|
62
|
+
# 创建参数列表
|
63
|
+
parameters = []
|
64
|
+
param_mapping = {} # 为每个关键字创建参数映射
|
65
|
+
|
66
|
+
for param_name in param_names:
|
67
|
+
# 确保参数名称正确映射
|
68
|
+
# 这里我们保持原始参数名称,但在执行时会进行正确映射
|
69
|
+
parameters.append({
|
70
|
+
'name': param_name,
|
71
|
+
'mapping': param_name, # 保持原始参数名称
|
72
|
+
'description': f'远程关键字参数: {param_name}'
|
73
|
+
})
|
74
|
+
# 添加到参数映射
|
75
|
+
param_mapping[param_name] = param_name
|
76
|
+
|
77
|
+
# 添加步骤名称参数,这是所有关键字都应该有的
|
78
|
+
if not any(p['name'] == '步骤名称' for p in parameters):
|
79
|
+
parameters.append({
|
80
|
+
'name': '步骤名称',
|
81
|
+
'mapping': 'step_name',
|
82
|
+
'description': '自定义的步骤名称,用于在报告中显示'
|
83
|
+
})
|
84
|
+
param_mapping['步骤名称'] = 'step_name'
|
85
|
+
|
86
|
+
# 创建远程关键字执行函数
|
87
|
+
remote_func = partial(self._execute_remote_keyword, name=name)
|
88
|
+
remote_func.__doc__ = doc
|
89
|
+
|
90
|
+
# 注册到关键字管理器,使用别名前缀
|
91
|
+
remote_keyword_name = f"{self.alias}|{name}"
|
92
|
+
keyword_manager._keywords[remote_keyword_name] = {
|
93
|
+
'func': remote_func,
|
94
|
+
'mapping': {p['name']: p['mapping'] for p in parameters},
|
95
|
+
'parameters': [Parameter(**p) for p in parameters],
|
96
|
+
'remote': True, # 标记为远程关键字
|
97
|
+
'alias': self.alias,
|
98
|
+
'original_name': name
|
99
|
+
}
|
100
|
+
|
101
|
+
# 缓存关键字信息
|
102
|
+
self.keyword_cache[name] = {
|
103
|
+
'parameters': param_names, # 注意这里只缓存原始参数,不包括步骤名称
|
104
|
+
'doc': doc
|
105
|
+
}
|
106
|
+
|
107
|
+
# 保存参数映射
|
108
|
+
self.param_mappings[name] = param_mapping
|
109
|
+
|
110
|
+
logger.debug(f"已注册远程关键字: {remote_keyword_name}")
|
111
|
+
except Exception as e:
|
112
|
+
logger.error(f"注册远程关键字 {name} 失败: {str(e)}")
|
113
|
+
|
114
|
+
def _execute_remote_keyword(self, **kwargs):
|
115
|
+
"""执行远程关键字"""
|
116
|
+
name = kwargs.pop('name')
|
117
|
+
|
118
|
+
# 移除context参数,因为它不能被序列化
|
119
|
+
if 'context' in kwargs:
|
120
|
+
kwargs.pop('context', None)
|
121
|
+
|
122
|
+
# 移除step_name参数,这是自动添加的,不需要传递给远程服务器
|
123
|
+
if 'step_name' in kwargs:
|
124
|
+
kwargs.pop('step_name', None)
|
125
|
+
|
126
|
+
# 打印调试信息
|
127
|
+
print(f"远程关键字调用: {name}, 参数: {kwargs}")
|
128
|
+
|
129
|
+
# 创建反向映射字典,用于检查参数是否已经映射
|
130
|
+
reverse_mapping = {}
|
131
|
+
|
132
|
+
# 使用动态注册的参数映射
|
133
|
+
if name in self.param_mappings:
|
134
|
+
param_mapping = self.param_mappings[name]
|
135
|
+
print(f"使用动态参数映射: {param_mapping}")
|
136
|
+
for cn_name, en_name in param_mapping.items():
|
137
|
+
reverse_mapping[en_name] = cn_name
|
138
|
+
else:
|
139
|
+
# 如果没有任何映射,使用原始参数名
|
140
|
+
param_mapping = None
|
141
|
+
print(f"没有找到参数映射,使用原始参数名")
|
142
|
+
|
143
|
+
# 映射参数名称
|
144
|
+
mapped_kwargs = {}
|
145
|
+
if param_mapping:
|
146
|
+
for k, v in kwargs.items():
|
147
|
+
if k in param_mapping:
|
148
|
+
mapped_key = param_mapping[k]
|
149
|
+
mapped_kwargs[mapped_key] = v
|
150
|
+
print(f"参数映射: {k} -> {mapped_key} = {v}")
|
151
|
+
else:
|
152
|
+
mapped_kwargs[k] = v
|
153
|
+
else:
|
154
|
+
mapped_kwargs = kwargs
|
155
|
+
|
156
|
+
# 确保参数名称正确映射
|
157
|
+
# 获取关键字的参数信息
|
158
|
+
if name in self.keyword_cache:
|
159
|
+
param_names = self.keyword_cache[name]['parameters']
|
160
|
+
print(f"远程关键字 {name} 的参数列表: {param_names}")
|
161
|
+
|
162
|
+
# 不再显示警告信息,因为参数已经在服务器端正确处理
|
163
|
+
# 服务器端会使用默认值或者报错,客户端不需要重复警告
|
164
|
+
|
165
|
+
# 执行远程调用
|
166
|
+
# 检查是否需要传递API密钥
|
167
|
+
if self.api_key:
|
168
|
+
result = self.server.run_keyword(name, mapped_kwargs, self.api_key)
|
169
|
+
else:
|
170
|
+
result = self.server.run_keyword(name, mapped_kwargs)
|
171
|
+
|
172
|
+
print(f"远程关键字执行结果: {result}")
|
173
|
+
|
174
|
+
if result['status'] == 'PASS':
|
175
|
+
return_data = result['return']
|
176
|
+
|
177
|
+
# 处理新的返回格式
|
178
|
+
if isinstance(return_data, dict):
|
179
|
+
# 处理捕获的变量 - 这里需要访问本地上下文
|
180
|
+
if 'captures' in return_data and return_data['captures']:
|
181
|
+
print(f"远程关键字捕获的变量: {return_data['captures']}")
|
182
|
+
|
183
|
+
# 处理会话状态
|
184
|
+
if 'session_state' in return_data and return_data['session_state']:
|
185
|
+
print(f"远程关键字会话状态: {return_data['session_state']}")
|
186
|
+
|
187
|
+
# 处理响应数据
|
188
|
+
if 'response' in return_data and return_data['response']:
|
189
|
+
print(f"远程关键字响应数据: 已接收")
|
190
|
+
|
191
|
+
# 检查是否为新的统一返回格式(包含captures等字段)
|
192
|
+
if 'captures' in return_data or 'session_state' in return_data or 'metadata' in return_data:
|
193
|
+
# 返回完整的新格式,让DSL执行器处理变量捕获
|
194
|
+
return return_data
|
195
|
+
elif 'result' in return_data:
|
196
|
+
# 返回主要结果,保持向后兼容
|
197
|
+
return return_data['result']
|
198
|
+
else:
|
199
|
+
return return_data
|
200
|
+
|
201
|
+
return return_data
|
202
|
+
else:
|
203
|
+
error_msg = result.get('error', '未知错误')
|
204
|
+
traceback = '\n'.join(result.get('traceback', []))
|
205
|
+
raise Exception(f"远程关键字执行失败: {error_msg}\n{traceback}")
|
206
|
+
|
207
|
+
def _send_initial_variables(self):
|
208
|
+
"""连接时发送初始变量到远程服务器"""
|
209
|
+
try:
|
210
|
+
variables_to_send = {}
|
211
|
+
|
212
|
+
# 收集全局变量
|
213
|
+
if self.sync_config.get('sync_global_vars', True):
|
214
|
+
variables_to_send.update(self._collect_global_variables())
|
215
|
+
|
216
|
+
# 收集YAML变量
|
217
|
+
if self.sync_config.get('sync_yaml_vars', True):
|
218
|
+
variables_to_send.update(self._collect_yaml_variables())
|
219
|
+
|
220
|
+
if variables_to_send:
|
221
|
+
try:
|
222
|
+
# 调用远程服务器的变量接收接口
|
223
|
+
result = self.server.sync_variables_from_client(variables_to_send, self.api_key)
|
224
|
+
if result.get('status') == 'success':
|
225
|
+
print(f"成功传递 {len(variables_to_send)} 个变量到远程服务器")
|
226
|
+
else:
|
227
|
+
print(f"传递变量到远程服务器失败: {result.get('error', '未知错误')}")
|
228
|
+
except Exception as e:
|
229
|
+
print(f"调用远程变量接口失败: {str(e)}")
|
230
|
+
else:
|
231
|
+
print("没有需要传递的变量")
|
232
|
+
|
233
|
+
except Exception as e:
|
234
|
+
logger.warning(f"初始变量传递失败: {str(e)}")
|
235
|
+
print(f"初始变量传递失败: {str(e)}")
|
236
|
+
|
237
|
+
def _collect_global_variables(self):
|
238
|
+
"""收集全局变量"""
|
239
|
+
from pytest_dsl.core.global_context import global_context
|
240
|
+
variables = {}
|
241
|
+
|
242
|
+
# 获取所有全局变量(包括g_开头的变量)
|
243
|
+
try:
|
244
|
+
# 这里需要访问全局上下文的内部存储
|
245
|
+
# 由于GlobalContext使用文件存储,我们需要直接读取
|
246
|
+
import json
|
247
|
+
import os
|
248
|
+
from filelock import FileLock
|
249
|
+
|
250
|
+
storage_file = global_context._storage_file
|
251
|
+
lock_file = global_context._lock_file
|
252
|
+
|
253
|
+
if os.path.exists(storage_file):
|
254
|
+
with FileLock(lock_file):
|
255
|
+
with open(storage_file, 'r', encoding='utf-8') as f:
|
256
|
+
stored_vars = json.load(f)
|
257
|
+
# 只同步g_开头的全局变量
|
258
|
+
for name, value in stored_vars.items():
|
259
|
+
if name.startswith('g_'):
|
260
|
+
variables[name] = value
|
261
|
+
except Exception as e:
|
262
|
+
logger.warning(f"收集全局变量失败: {str(e)}")
|
263
|
+
|
264
|
+
return variables
|
265
|
+
|
266
|
+
def _collect_yaml_variables(self):
|
267
|
+
"""收集YAML配置变量"""
|
268
|
+
from pytest_dsl.core.yaml_vars import yaml_vars
|
269
|
+
variables = {}
|
270
|
+
|
271
|
+
try:
|
272
|
+
# 获取所有YAML变量
|
273
|
+
yaml_data = yaml_vars._variables
|
274
|
+
if yaml_data:
|
275
|
+
# 检查同步配置中是否指定了特定的键
|
276
|
+
sync_keys = self.sync_config.get('yaml_sync_keys', None)
|
277
|
+
exclude_patterns = self.sync_config.get('yaml_exclude_patterns', [
|
278
|
+
'password', 'secret', 'key', 'token', 'credential', 'auth',
|
279
|
+
'private', 'remote_servers' # 排除远程服务器配置避免循环
|
280
|
+
])
|
281
|
+
|
282
|
+
if sync_keys:
|
283
|
+
# 如果指定了特定键,只传递这些键,直接使用原始变量名
|
284
|
+
for key in sync_keys:
|
285
|
+
if key in yaml_data:
|
286
|
+
variables[key] = yaml_data[key]
|
287
|
+
else:
|
288
|
+
# 传递所有YAML变量,但排除敏感信息
|
289
|
+
for key, value in yaml_data.items():
|
290
|
+
# 检查是否包含敏感信息
|
291
|
+
key_lower = key.lower()
|
292
|
+
should_exclude = False
|
293
|
+
|
294
|
+
for pattern in exclude_patterns:
|
295
|
+
if pattern.lower() in key_lower:
|
296
|
+
should_exclude = True
|
297
|
+
break
|
298
|
+
|
299
|
+
# 如果值是字符串,也检查是否包含敏感信息
|
300
|
+
if not should_exclude and isinstance(value, str):
|
301
|
+
value_lower = value.lower()
|
302
|
+
for pattern in exclude_patterns:
|
303
|
+
if pattern.lower() in value_lower and len(value) < 100: # 只检查短字符串
|
304
|
+
should_exclude = True
|
305
|
+
break
|
306
|
+
|
307
|
+
if not should_exclude:
|
308
|
+
# 直接使用原始变量名,不添加yaml_前缀,实现无缝传递
|
309
|
+
variables[key] = value
|
310
|
+
print(f"传递YAML变量: {key}")
|
311
|
+
else:
|
312
|
+
print(f"跳过敏感YAML变量: {key}")
|
313
|
+
|
314
|
+
except Exception as e:
|
315
|
+
logger.warning(f"收集YAML变量失败: {str(e)}")
|
316
|
+
|
317
|
+
return variables
|
318
|
+
|
319
|
+
|
320
|
+
|
321
|
+
# 远程关键字客户端管理器
|
322
|
+
class RemoteKeywordManager:
|
323
|
+
"""远程关键字客户端管理器,管理多个远程服务器连接"""
|
324
|
+
|
325
|
+
def __init__(self):
|
326
|
+
self.clients = {} # 别名 -> 客户端实例
|
327
|
+
|
328
|
+
def register_remote_server(self, url, alias, api_key=None, sync_config=None):
|
329
|
+
"""注册远程关键字服务器
|
330
|
+
|
331
|
+
Args:
|
332
|
+
url: 服务器URL
|
333
|
+
alias: 服务器别名
|
334
|
+
api_key: API密钥(可选)
|
335
|
+
sync_config: 变量同步配置(可选)
|
336
|
+
|
337
|
+
Returns:
|
338
|
+
bool: 是否成功连接
|
339
|
+
"""
|
340
|
+
print(f"RemoteKeywordManager: 正在注册远程服务器 {url} 别名 {alias}")
|
341
|
+
client = RemoteKeywordClient(url=url, api_key=api_key, alias=alias, sync_config=sync_config)
|
342
|
+
success = client.connect()
|
343
|
+
|
344
|
+
if success:
|
345
|
+
print(f"RemoteKeywordManager: 成功连接到远程服务器 {url}")
|
346
|
+
self.clients[alias] = client
|
347
|
+
else:
|
348
|
+
print(f"RemoteKeywordManager: 连接远程服务器 {url} 失败")
|
349
|
+
|
350
|
+
return success
|
351
|
+
|
352
|
+
def get_client(self, alias):
|
353
|
+
"""获取指定别名的客户端实例"""
|
354
|
+
return self.clients.get(alias)
|
355
|
+
|
356
|
+
def execute_remote_keyword(self, alias, keyword_name, **kwargs):
|
357
|
+
"""执行远程关键字
|
358
|
+
|
359
|
+
Args:
|
360
|
+
alias: 服务器别名
|
361
|
+
keyword_name: 关键字名称
|
362
|
+
**kwargs: 关键字参数
|
363
|
+
|
364
|
+
Returns:
|
365
|
+
执行结果
|
366
|
+
"""
|
367
|
+
client = self.get_client(alias)
|
368
|
+
if not client:
|
369
|
+
raise Exception(f"未找到别名为 {alias} 的远程服务器")
|
370
|
+
|
371
|
+
return client._execute_remote_keyword(name=keyword_name, **kwargs)
|
372
|
+
|
373
|
+
|
374
|
+
|
375
|
+
# 创建全局远程关键字管理器实例
|
376
|
+
remote_keyword_manager = RemoteKeywordManager()
|