pytest-dsl 0.15.4__py3-none-any.whl → 0.15.5__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 +1 -1
- pytest_dsl/core/context.py +23 -0
- pytest_dsl/core/dsl_executor.py +132 -28
- pytest_dsl/core/serialization_utils.py +231 -0
- pytest_dsl/remote/__init__.py +1 -1
- pytest_dsl/remote/keyword_client.py +139 -55
- pytest_dsl/remote/keyword_server.py +32 -16
- {pytest_dsl-0.15.4.dist-info → pytest_dsl-0.15.5.dist-info}/METADATA +1 -1
- {pytest_dsl-0.15.4.dist-info → pytest_dsl-0.15.5.dist-info}/RECORD +13 -12
- {pytest_dsl-0.15.4.dist-info → pytest_dsl-0.15.5.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.15.4.dist-info → pytest_dsl-0.15.5.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.15.4.dist-info → pytest_dsl-0.15.5.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.15.4.dist-info → pytest_dsl-0.15.5.dist-info}/top_level.txt +0 -0
pytest_dsl/__init__.py
CHANGED
pytest_dsl/core/context.py
CHANGED
@@ -46,6 +46,29 @@ class TestContext:
|
|
46
46
|
"""获取所有本地变量"""
|
47
47
|
return self._data
|
48
48
|
|
49
|
+
def get_all_context_variables(self) -> dict:
|
50
|
+
"""获取所有上下文变量,包括本地变量和外部提供者变量
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
包含所有上下文变量的字典,本地变量优先级高于外部变量
|
54
|
+
"""
|
55
|
+
all_variables = {}
|
56
|
+
|
57
|
+
# 1. 先添加外部提供者的变量
|
58
|
+
for provider in self._external_providers:
|
59
|
+
if hasattr(provider, 'get_all_variables'):
|
60
|
+
try:
|
61
|
+
external_vars = provider.get_all_variables()
|
62
|
+
if isinstance(external_vars, dict):
|
63
|
+
all_variables.update(external_vars)
|
64
|
+
except Exception as e:
|
65
|
+
print(f"警告:获取外部变量提供者变量时发生错误: {e}")
|
66
|
+
|
67
|
+
# 2. 再添加本地变量(覆盖同名的外部变量)
|
68
|
+
all_variables.update(self._data)
|
69
|
+
|
70
|
+
return all_variables
|
71
|
+
|
49
72
|
def register_external_variable_provider(self, provider) -> None:
|
50
73
|
"""注册外部变量提供者
|
51
74
|
|
pytest_dsl/core/dsl_executor.py
CHANGED
@@ -34,8 +34,8 @@ class ReturnException(Exception):
|
|
34
34
|
class DSLExecutionError(Exception):
|
35
35
|
"""DSL执行异常,包含行号信息"""
|
36
36
|
|
37
|
-
def __init__(self, message: str, line_number: int = None,
|
38
|
-
original_exception: Exception = None):
|
37
|
+
def __init__(self, message: str, line_number: int = None,
|
38
|
+
node_type: str = None, original_exception: Exception = None):
|
39
39
|
self.line_number = line_number
|
40
40
|
self.node_type = node_type
|
41
41
|
self.original_exception = original_exception
|
@@ -48,7 +48,9 @@ class DSLExecutionError(Exception):
|
|
48
48
|
error_parts.append(f"节点类型: {node_type}")
|
49
49
|
if original_exception:
|
50
50
|
error_parts.append(
|
51
|
-
f"原始异常: {type(original_exception).__name__}:
|
51
|
+
f"原始异常: {type(original_exception).__name__}: "
|
52
|
+
f"{str(original_exception)}"
|
53
|
+
)
|
52
54
|
|
53
55
|
super().__init__(" | ".join(error_parts))
|
54
56
|
|
@@ -108,7 +110,8 @@ class DSLExecutor:
|
|
108
110
|
target_node = node or self._current_node
|
109
111
|
|
110
112
|
# 尝试从当前节点获取行号
|
111
|
-
if target_node and hasattr(target_node, 'line_number') and
|
113
|
+
if (target_node and hasattr(target_node, 'line_number') and
|
114
|
+
target_node.line_number):
|
112
115
|
return f"\n行号: {target_node.line_number}"
|
113
116
|
|
114
117
|
# 如果当前节点没有行号,从节点栈中查找最近的有行号的节点
|
@@ -117,12 +120,16 @@ class DSLExecutor:
|
|
117
120
|
return f"\n行号: {stack_node.line_number}"
|
118
121
|
|
119
122
|
# 如果当前节点没有行号,尝试从当前执行的节点获取
|
120
|
-
if
|
123
|
+
if (self._current_node and
|
124
|
+
hasattr(self._current_node, 'line_number') and
|
125
|
+
self._current_node.line_number):
|
121
126
|
return f"\n行号: {self._current_node.line_number}"
|
122
127
|
|
123
128
|
return ""
|
124
129
|
|
125
|
-
def _handle_exception_with_line_info(self, e: Exception, node=None,
|
130
|
+
def _handle_exception_with_line_info(self, e: Exception, node=None,
|
131
|
+
context_info: str = "",
|
132
|
+
skip_allure_logging: bool = False):
|
126
133
|
"""统一处理异常并记录行号信息
|
127
134
|
|
128
135
|
Args:
|
@@ -279,7 +286,8 @@ class DSLExecutor:
|
|
279
286
|
elif expr_node.type == 'StringLiteral':
|
280
287
|
# 字符串字面量,如果包含变量占位符则进行替换,否则直接返回
|
281
288
|
if '${' in expr_node.value:
|
282
|
-
return self.variable_replacer.replace_in_string(
|
289
|
+
return self.variable_replacer.replace_in_string(
|
290
|
+
expr_node.value)
|
283
291
|
else:
|
284
292
|
return expr_node.value
|
285
293
|
elif expr_node.type == 'NumberLiteral':
|
@@ -294,7 +302,8 @@ class DSLExecutor:
|
|
294
302
|
raise KeyError(f"变量 '{var_name}' 不存在")
|
295
303
|
elif expr_node.type == 'PlaceholderRef':
|
296
304
|
# 变量占位符 ${var},进行变量替换
|
297
|
-
return self.variable_replacer.replace_in_string(
|
305
|
+
return self.variable_replacer.replace_in_string(
|
306
|
+
expr_node.value)
|
298
307
|
elif expr_node.type == 'KeywordCall':
|
299
308
|
return self.execute(expr_node)
|
300
309
|
elif expr_node.type == 'ListExpr':
|
@@ -325,7 +334,8 @@ class DSLExecutor:
|
|
325
334
|
else:
|
326
335
|
raise Exception(f"无法求值的表达式类型: {expr_node.type}")
|
327
336
|
|
328
|
-
return self._execute_with_error_handling(
|
337
|
+
return self._execute_with_error_handling(
|
338
|
+
_eval_expression_impl, expr_node)
|
329
339
|
|
330
340
|
def _eval_expression_value(self, value):
|
331
341
|
"""处理表达式值的具体逻辑"""
|
@@ -335,8 +345,10 @@ class DSLExecutor:
|
|
335
345
|
elif isinstance(value, str):
|
336
346
|
# 定义扩展的变量引用模式,支持数组索引和字典键访问
|
337
347
|
pattern = (
|
338
|
-
r'\$\{([a-zA-Z_\u4e00-\u9fa5]
|
339
|
-
r'
|
348
|
+
r'\$\{([a-zA-Z_\u4e00-\u9fa5]'
|
349
|
+
r'[a-zA-Z0-9_\u4e00-\u9fa5]*'
|
350
|
+
r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5]'
|
351
|
+
r'[a-zA-Z0-9_\u4e00-\u9fa5]*)'
|
340
352
|
r'|(?:\[[^\]]+\]))*)\}'
|
341
353
|
)
|
342
354
|
# 检查整个字符串是否完全匹配单一变量引用模式
|
@@ -351,7 +363,9 @@ class DSLExecutor:
|
|
351
363
|
else:
|
352
364
|
# 对于不包含 ${} 的普通字符串,检查是否为单纯的变量名
|
353
365
|
# 只有当字符串是有效的变量名格式且确实存在该变量时,才当作变量处理
|
354
|
-
|
366
|
+
var_pattern = (r'^[a-zA-Z_\u4e00-\u9fa5]'
|
367
|
+
r'[a-zA-Z0-9_\u4e00-\u9fa5]*$')
|
368
|
+
if (re.match(var_pattern, value) and
|
355
369
|
value in self.variable_replacer.local_variables):
|
356
370
|
return self.variable_replacer.local_variables[value]
|
357
371
|
else:
|
@@ -361,7 +375,8 @@ class DSLExecutor:
|
|
361
375
|
except Exception as e:
|
362
376
|
# 为变量解析异常添加更多上下文信息
|
363
377
|
context_info = f"解析表达式值 '{value}'"
|
364
|
-
self._handle_exception_with_line_info(
|
378
|
+
self._handle_exception_with_line_info(
|
379
|
+
e, context_info=context_info)
|
365
380
|
|
366
381
|
def _eval_comparison_expr(self, expr_node):
|
367
382
|
"""
|
@@ -752,9 +767,15 @@ class DSLExecutor:
|
|
752
767
|
name="赋值详情",
|
753
768
|
attachment_type=allure.attachment_type.TEXT
|
754
769
|
)
|
770
|
+
|
771
|
+
# 通知远程服务器变量已更新
|
772
|
+
self._notify_remote_servers_variable_changed(
|
773
|
+
var_name, expr_value)
|
774
|
+
|
755
775
|
except Exception as e:
|
756
776
|
# 在步骤内部记录异常详情
|
757
|
-
error_details = f"执行Assignment节点: {str(e)}{line_info}\n
|
777
|
+
error_details = (f"执行Assignment节点: {str(e)}{line_info}\n"
|
778
|
+
f"上下文: 执行Assignment节点")
|
758
779
|
allure.attach(
|
759
780
|
error_details,
|
760
781
|
name="DSL执行异常",
|
@@ -795,9 +816,15 @@ class DSLExecutor:
|
|
795
816
|
name="关键字赋值详情",
|
796
817
|
attachment_type=allure.attachment_type.TEXT
|
797
818
|
)
|
819
|
+
|
820
|
+
# 通知远程服务器变量已更新
|
821
|
+
self._notify_remote_servers_variable_changed(var_name, result)
|
822
|
+
|
798
823
|
except Exception as e:
|
799
824
|
# 在步骤内部记录异常详情
|
800
|
-
error_details = f"执行AssignmentKeywordCall节点: {str(e)}
|
825
|
+
error_details = (f"执行AssignmentKeywordCall节点: {str(e)}"
|
826
|
+
f"{line_info}\n"
|
827
|
+
f"上下文: 执行AssignmentKeywordCall节点")
|
801
828
|
allure.attach(
|
802
829
|
error_details,
|
803
830
|
name="DSL执行异常",
|
@@ -806,6 +833,51 @@ class DSLExecutor:
|
|
806
833
|
# 重新抛出异常,让外层的统一异常处理机制处理
|
807
834
|
raise
|
808
835
|
|
836
|
+
def _notify_remote_servers_variable_changed(self, var_name, var_value):
|
837
|
+
"""通知远程服务器变量已发生变化
|
838
|
+
|
839
|
+
Args:
|
840
|
+
var_name: 变量名
|
841
|
+
var_value: 变量值
|
842
|
+
"""
|
843
|
+
try:
|
844
|
+
# 使用统一的序列化工具进行变量过滤
|
845
|
+
from .serialization_utils import XMLRPCSerializer
|
846
|
+
|
847
|
+
variables_to_filter = {var_name: var_value}
|
848
|
+
filtered_variables = XMLRPCSerializer.filter_variables(
|
849
|
+
variables_to_filter)
|
850
|
+
|
851
|
+
if not filtered_variables:
|
852
|
+
# 变量被过滤掉了(敏感变量或不可序列化)
|
853
|
+
return
|
854
|
+
|
855
|
+
# 导入远程关键字管理器
|
856
|
+
from pytest_dsl.remote.keyword_client import remote_keyword_manager
|
857
|
+
|
858
|
+
# 获取所有已连接的远程服务器客户端
|
859
|
+
for alias, client in remote_keyword_manager.clients.items():
|
860
|
+
try:
|
861
|
+
# 调用远程服务器的变量同步接口
|
862
|
+
result = client.server.sync_variables_from_client(
|
863
|
+
filtered_variables, client.api_key)
|
864
|
+
|
865
|
+
if result.get('status') == 'success':
|
866
|
+
print(f"🔄 变量 {var_name} 已同步到远程服务器 {alias}")
|
867
|
+
else:
|
868
|
+
error_msg = result.get('error', '未知错误')
|
869
|
+
print(f"❌ 变量 {var_name} 同步到远程服务器 {alias} "
|
870
|
+
f"失败: {error_msg}")
|
871
|
+
|
872
|
+
except Exception as e:
|
873
|
+
print(f"❌ 通知远程服务器 {alias} 变量变化失败: {str(e)}")
|
874
|
+
|
875
|
+
except ImportError:
|
876
|
+
# 如果没有导入远程模块,跳过通知
|
877
|
+
pass
|
878
|
+
except Exception as e:
|
879
|
+
print(f"❌ 通知远程服务器变量变化时发生错误: {str(e)}")
|
880
|
+
|
809
881
|
def _handle_for_loop(self, node):
|
810
882
|
"""处理for循环"""
|
811
883
|
step_name = f"执行循环: {node.value}"
|
@@ -834,6 +906,9 @@ class DSLExecutor:
|
|
834
906
|
self.variable_replacer.local_variables[var_name] = i
|
835
907
|
self.test_context.set(var_name, i)
|
836
908
|
|
909
|
+
# 通知远程服务器循环变量已更新
|
910
|
+
self._notify_remote_servers_variable_changed(var_name, i)
|
911
|
+
|
837
912
|
with allure.step(f"循环轮次: {var_name} = {i}"):
|
838
913
|
try:
|
839
914
|
self.execute(node.children[2])
|
@@ -863,7 +938,9 @@ class DSLExecutor:
|
|
863
938
|
raise e
|
864
939
|
except Exception as e:
|
865
940
|
# 在循环轮次内部记录异常详情
|
866
|
-
error_details = f"循环执行异常 ({var_name} = {i}):
|
941
|
+
error_details = (f"循环执行异常 ({var_name} = {i}): "
|
942
|
+
f"{str(e)}{line_info}\n"
|
943
|
+
f"上下文: 执行ForLoop节点")
|
867
944
|
allure.attach(
|
868
945
|
error_details,
|
869
946
|
name="DSL执行异常",
|
@@ -876,7 +953,8 @@ class DSLExecutor:
|
|
876
953
|
raise
|
877
954
|
except Exception as e:
|
878
955
|
# 在步骤内部记录异常详情
|
879
|
-
error_details = f"执行ForLoop节点: {str(e)}{line_info}\n
|
956
|
+
error_details = (f"执行ForLoop节点: {str(e)}{line_info}\n"
|
957
|
+
f"上下文: 执行ForLoop节点")
|
880
958
|
allure.attach(
|
881
959
|
error_details,
|
882
960
|
name="DSL执行异常",
|
@@ -897,7 +975,8 @@ class DSLExecutor:
|
|
897
975
|
# 在步骤内部记录异常
|
898
976
|
with allure.step(f"调用关键字: {keyword_name}"):
|
899
977
|
allure.attach(
|
900
|
-
f"执行KeywordCall节点: 未注册的关键字: {keyword_name}
|
978
|
+
f"执行KeywordCall节点: 未注册的关键字: {keyword_name}"
|
979
|
+
f"{line_info}\n上下文: 执行KeywordCall节点",
|
901
980
|
name="DSL执行异常",
|
902
981
|
attachment_type=allure.attachment_type.TEXT
|
903
982
|
)
|
@@ -937,14 +1016,21 @@ class DSLExecutor:
|
|
937
1016
|
r'参数解析异常 \(([^)]+)\): (.+)', core_error)
|
938
1017
|
if match:
|
939
1018
|
param_name, detailed_error = match.groups()
|
940
|
-
error_details = f"参数解析失败 ({param_name}):
|
1019
|
+
error_details = (f"参数解析失败 ({param_name}): "
|
1020
|
+
f"{detailed_error}{line_info}\n"
|
1021
|
+
f"上下文: 执行KeywordCall节点")
|
941
1022
|
else:
|
942
|
-
error_details = f"参数解析失败: {core_error}
|
1023
|
+
error_details = (f"参数解析失败: {core_error}"
|
1024
|
+
f"{line_info}\n"
|
1025
|
+
f"上下文: 执行KeywordCall节点")
|
943
1026
|
else:
|
944
|
-
error_details = f"参数解析失败: {core_error}
|
1027
|
+
error_details = (f"参数解析失败: {core_error}"
|
1028
|
+
f"{line_info}\n"
|
1029
|
+
f"上下文: 执行KeywordCall节点")
|
945
1030
|
else:
|
946
1031
|
# 其他异常
|
947
|
-
error_details = f"执行KeywordCall节点: {str(e)}{line_info}\n
|
1032
|
+
error_details = (f"执行KeywordCall节点: {str(e)}{line_info}\n"
|
1033
|
+
f"上下文: 执行KeywordCall节点")
|
948
1034
|
|
949
1035
|
allure.attach(
|
950
1036
|
error_details,
|
@@ -958,7 +1044,6 @@ class DSLExecutor:
|
|
958
1044
|
"""准备关键字调用参数"""
|
959
1045
|
mapping = keyword_info.get('mapping', {})
|
960
1046
|
kwargs = {'context': self.test_context} # 默认传入context参数
|
961
|
-
line_info = self._get_line_info(node)
|
962
1047
|
|
963
1048
|
# 检查是否有参数列表
|
964
1049
|
if node.children[0]:
|
@@ -982,7 +1067,8 @@ class DSLExecutor:
|
|
982
1067
|
)
|
983
1068
|
except Exception as e:
|
984
1069
|
# 将异常重新包装,添加参数名信息,但不在这里记录到allure
|
985
|
-
raise Exception(
|
1070
|
+
raise Exception(
|
1071
|
+
f"参数解析异常 ({param_name}): {str(e)}")
|
986
1072
|
|
987
1073
|
return kwargs
|
988
1074
|
|
@@ -1111,7 +1197,8 @@ class DSLExecutor:
|
|
1111
1197
|
return result
|
1112
1198
|
except Exception as e:
|
1113
1199
|
# 在步骤内部记录异常详情
|
1114
|
-
error_details = f"执行RemoteKeywordCall节点: {str(e)}
|
1200
|
+
error_details = (f"执行RemoteKeywordCall节点: {str(e)}"
|
1201
|
+
f"{line_info}\n上下文: 执行RemoteKeywordCall节点")
|
1115
1202
|
allure.attach(
|
1116
1203
|
error_details,
|
1117
1204
|
name="DSL执行异常",
|
@@ -1178,12 +1265,26 @@ class DSLExecutor:
|
|
1178
1265
|
name="远程关键字赋值",
|
1179
1266
|
attachment_type=allure.attachment_type.TEXT
|
1180
1267
|
)
|
1268
|
+
|
1269
|
+
# 通知远程服务器变量已更新
|
1270
|
+
self._notify_remote_servers_variable_changed(
|
1271
|
+
var_name, actual_result)
|
1272
|
+
|
1273
|
+
# 同时处理captures中的变量同步
|
1274
|
+
if isinstance(result, dict) and 'captures' in result:
|
1275
|
+
captures = result.get('captures', {})
|
1276
|
+
for capture_var, capture_value in captures.items():
|
1277
|
+
# 通知远程服务器捕获的变量也已更新
|
1278
|
+
self._notify_remote_servers_variable_changed(
|
1279
|
+
capture_var, capture_value)
|
1181
1280
|
else:
|
1182
1281
|
error_msg = "远程关键字没有返回结果"
|
1183
1282
|
raise Exception(error_msg)
|
1184
1283
|
except Exception as e:
|
1185
1284
|
# 在步骤内部记录异常详情
|
1186
|
-
error_details = f"执行AssignmentRemoteKeywordCall节点: {str(e)}
|
1285
|
+
error_details = (f"执行AssignmentRemoteKeywordCall节点: {str(e)}"
|
1286
|
+
f"{line_info}\n"
|
1287
|
+
f"上下文: 执行AssignmentRemoteKeywordCall节点")
|
1187
1288
|
allure.attach(
|
1188
1289
|
error_details,
|
1189
1290
|
name="DSL执行异常",
|
@@ -1257,7 +1358,8 @@ class DSLExecutor:
|
|
1257
1358
|
self.execution_tracker.finish_current_step(error=error_msg)
|
1258
1359
|
|
1259
1360
|
# 如果是控制流异常或已经是DSLExecutionError,直接重抛
|
1260
|
-
if isinstance(e, (BreakException, ContinueException,
|
1361
|
+
if isinstance(e, (BreakException, ContinueException,
|
1362
|
+
ReturnException, DSLExecutionError)):
|
1261
1363
|
raise
|
1262
1364
|
|
1263
1365
|
# 如果是断言异常,保持原样但可能添加行号信息
|
@@ -1323,7 +1425,9 @@ class DSLExecutor:
|
|
1323
1425
|
def _setup_variable_providers(self):
|
1324
1426
|
"""设置变量提供者,将外部变量源注入到TestContext中"""
|
1325
1427
|
try:
|
1326
|
-
from .variable_providers import
|
1428
|
+
from .variable_providers import (
|
1429
|
+
setup_context_with_default_providers
|
1430
|
+
)
|
1327
1431
|
setup_context_with_default_providers(self.test_context)
|
1328
1432
|
|
1329
1433
|
# 同步常用变量到context中,提高访问性能
|
@@ -0,0 +1,231 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
"""
|
4
|
+
序列化工具模块
|
5
|
+
|
6
|
+
提供统一的XML-RPC序列化检查和转换功能,避免代码重复。
|
7
|
+
"""
|
8
|
+
|
9
|
+
import datetime
|
10
|
+
from typing import Any, Dict, List, Optional
|
11
|
+
|
12
|
+
|
13
|
+
class XMLRPCSerializer:
|
14
|
+
"""XML-RPC序列化工具类
|
15
|
+
|
16
|
+
统一处理XML-RPC序列化检查、转换和过滤逻辑,
|
17
|
+
避免在多个类中重复实现相同的序列化代码。
|
18
|
+
"""
|
19
|
+
|
20
|
+
# 敏感信息过滤模式
|
21
|
+
DEFAULT_EXCLUDE_PATTERNS = [
|
22
|
+
'password', 'secret', 'token', 'credential', 'auth', 'private'
|
23
|
+
]
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
def is_serializable(value: Any) -> bool:
|
27
|
+
"""检查值是否可以被XML-RPC序列化
|
28
|
+
|
29
|
+
XML-RPC支持的类型:
|
30
|
+
- None (需要allow_none=True)
|
31
|
+
- bool, int, float, str, bytes
|
32
|
+
- datetime.datetime
|
33
|
+
- list (元素也必须可序列化)
|
34
|
+
- dict (键必须是字符串,值必须可序列化)
|
35
|
+
|
36
|
+
Args:
|
37
|
+
value: 要检查的值
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
bool: 是否可序列化
|
41
|
+
"""
|
42
|
+
# 基本类型
|
43
|
+
if value is None:
|
44
|
+
return True
|
45
|
+
if isinstance(value, (bool, int, float, str, bytes)):
|
46
|
+
return True
|
47
|
+
if isinstance(value, datetime.datetime):
|
48
|
+
return True
|
49
|
+
|
50
|
+
# 严格检查:只允许内置的list和dict类型,不允许自定义类
|
51
|
+
value_type = type(value)
|
52
|
+
|
53
|
+
# 检查是否为内置list类型(不是子类)
|
54
|
+
if value_type is list:
|
55
|
+
try:
|
56
|
+
for item in value:
|
57
|
+
if not XMLRPCSerializer.is_serializable(item):
|
58
|
+
return False
|
59
|
+
return True
|
60
|
+
except Exception:
|
61
|
+
return False
|
62
|
+
|
63
|
+
# 检查是否为内置tuple类型
|
64
|
+
if value_type is tuple:
|
65
|
+
try:
|
66
|
+
for item in value:
|
67
|
+
if not XMLRPCSerializer.is_serializable(item):
|
68
|
+
return False
|
69
|
+
return True
|
70
|
+
except Exception:
|
71
|
+
return False
|
72
|
+
|
73
|
+
# 检查是否为内置dict类型(不是子类,如DotAccessDict)
|
74
|
+
if value_type is dict:
|
75
|
+
try:
|
76
|
+
for k, v in value.items():
|
77
|
+
# XML-RPC要求字典的键必须是字符串
|
78
|
+
if not isinstance(k, str):
|
79
|
+
return False
|
80
|
+
if not XMLRPCSerializer.is_serializable(v):
|
81
|
+
return False
|
82
|
+
return True
|
83
|
+
except Exception:
|
84
|
+
return False
|
85
|
+
|
86
|
+
# 其他类型都不可序列化
|
87
|
+
return False
|
88
|
+
|
89
|
+
@staticmethod
|
90
|
+
def convert_to_serializable(value: Any) -> Optional[Any]:
|
91
|
+
"""尝试将值转换为XML-RPC可序列化的格式
|
92
|
+
|
93
|
+
Args:
|
94
|
+
value: 要转换的值
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
转换后的值,如果无法转换则返回None
|
98
|
+
"""
|
99
|
+
# 如果已经可序列化,直接返回
|
100
|
+
if XMLRPCSerializer.is_serializable(value):
|
101
|
+
return value
|
102
|
+
|
103
|
+
# 尝试转换类字典对象为标准字典
|
104
|
+
if hasattr(value, 'keys') and hasattr(value, 'items'):
|
105
|
+
try:
|
106
|
+
converted_dict = {}
|
107
|
+
for k, v in value.items():
|
108
|
+
# 键必须是字符串
|
109
|
+
if not isinstance(k, str):
|
110
|
+
k = str(k)
|
111
|
+
|
112
|
+
# 递归转换值
|
113
|
+
converted_value = XMLRPCSerializer.convert_to_serializable(v)
|
114
|
+
if converted_value is not None or v is None:
|
115
|
+
converted_dict[k] = converted_value
|
116
|
+
else:
|
117
|
+
# 如果无法转换子值,跳过这个键值对
|
118
|
+
print(f"跳过无法转换的字典项: {k} "
|
119
|
+
f"(类型: {type(v).__name__})")
|
120
|
+
continue
|
121
|
+
|
122
|
+
return converted_dict
|
123
|
+
except Exception as e:
|
124
|
+
print(f"转换类字典对象失败: {e}")
|
125
|
+
return None
|
126
|
+
|
127
|
+
# 尝试转换类列表对象为标准列表
|
128
|
+
if hasattr(value, '__iter__') and not isinstance(value, (str, bytes)):
|
129
|
+
try:
|
130
|
+
converted_list = []
|
131
|
+
for item in value:
|
132
|
+
converted_item = XMLRPCSerializer.convert_to_serializable(item)
|
133
|
+
if converted_item is not None or item is None:
|
134
|
+
converted_list.append(converted_item)
|
135
|
+
else:
|
136
|
+
# 如果无法转换子项,跳过
|
137
|
+
print(f"跳过无法转换的列表项: "
|
138
|
+
f"(类型: {type(item).__name__})")
|
139
|
+
continue
|
140
|
+
|
141
|
+
return converted_list
|
142
|
+
except Exception as e:
|
143
|
+
print(f"转换类列表对象失败: {e}")
|
144
|
+
return None
|
145
|
+
|
146
|
+
# 尝试转换为字符串表示
|
147
|
+
try:
|
148
|
+
str_value = str(value)
|
149
|
+
# 避免转换过长的字符串或包含敏感信息的对象
|
150
|
+
if (len(str_value) < 1000 and
|
151
|
+
not any(pattern in str_value.lower()
|
152
|
+
for pattern in XMLRPCSerializer.DEFAULT_EXCLUDE_PATTERNS)):
|
153
|
+
return str_value
|
154
|
+
except Exception:
|
155
|
+
pass
|
156
|
+
|
157
|
+
# 无法转换
|
158
|
+
return None
|
159
|
+
|
160
|
+
@staticmethod
|
161
|
+
def filter_variables(variables: Dict[str, Any],
|
162
|
+
exclude_patterns: Optional[List[str]] = None) -> Dict[str, Any]:
|
163
|
+
"""过滤变量字典,移除敏感变量和不可序列化的变量
|
164
|
+
|
165
|
+
Args:
|
166
|
+
variables: 原始变量字典
|
167
|
+
exclude_patterns: 排除模式列表,如果为None则使用默认模式
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
Dict[str, Any]: 过滤后的变量字典
|
171
|
+
"""
|
172
|
+
if exclude_patterns is None:
|
173
|
+
exclude_patterns = XMLRPCSerializer.DEFAULT_EXCLUDE_PATTERNS
|
174
|
+
|
175
|
+
filtered_variables = {}
|
176
|
+
|
177
|
+
for var_name, var_value in variables.items():
|
178
|
+
# 检查是否需要排除
|
179
|
+
should_exclude = False
|
180
|
+
var_name_lower = var_name.lower()
|
181
|
+
|
182
|
+
# 检查变量名
|
183
|
+
for pattern in exclude_patterns:
|
184
|
+
if pattern.lower() in var_name_lower:
|
185
|
+
should_exclude = True
|
186
|
+
break
|
187
|
+
|
188
|
+
# 如果值是字符串,也检查是否包含敏感信息
|
189
|
+
if not should_exclude and isinstance(var_value, str):
|
190
|
+
value_lower = var_value.lower()
|
191
|
+
for pattern in exclude_patterns:
|
192
|
+
if (pattern.lower() in value_lower and
|
193
|
+
len(var_value) < 100): # 只检查短字符串
|
194
|
+
should_exclude = True
|
195
|
+
break
|
196
|
+
|
197
|
+
if not should_exclude:
|
198
|
+
# 尝试转换为可序列化的格式
|
199
|
+
serializable_value = XMLRPCSerializer.convert_to_serializable(var_value)
|
200
|
+
# 注意:None值转换后仍然是None,但这是有效的结果
|
201
|
+
if serializable_value is not None or var_value is None:
|
202
|
+
filtered_variables[var_name] = serializable_value
|
203
|
+
else:
|
204
|
+
print(f"跳过不可序列化的变量: {var_name} "
|
205
|
+
f"(类型: {type(var_value).__name__})")
|
206
|
+
else:
|
207
|
+
print(f"跳过敏感变量: {var_name}")
|
208
|
+
|
209
|
+
return filtered_variables
|
210
|
+
|
211
|
+
@staticmethod
|
212
|
+
def validate_xmlrpc_data(data: Any) -> bool:
|
213
|
+
"""验证数据是否可以通过XML-RPC传输
|
214
|
+
|
215
|
+
Args:
|
216
|
+
data: 要验证的数据
|
217
|
+
|
218
|
+
Returns:
|
219
|
+
bool: 是否可以传输
|
220
|
+
"""
|
221
|
+
try:
|
222
|
+
import xmlrpc.client
|
223
|
+
# 尝试序列化数据
|
224
|
+
xmlrpc.client.dumps((data,), allow_none=True)
|
225
|
+
return True
|
226
|
+
except Exception:
|
227
|
+
return False
|
228
|
+
|
229
|
+
|
230
|
+
# 创建全局序列化器实例,方便直接使用
|
231
|
+
xmlrpc_serializer = XMLRPCSerializer()
|
pytest_dsl/remote/__init__.py
CHANGED
@@ -59,7 +59,7 @@ class RemoteKeywordClient:
|
|
59
59
|
try:
|
60
60
|
param_names = self.server.get_keyword_arguments(name)
|
61
61
|
doc = self.server.get_keyword_documentation(name)
|
62
|
-
|
62
|
+
|
63
63
|
# 尝试获取参数详细信息(包括默认值)
|
64
64
|
param_details = []
|
65
65
|
try:
|
@@ -87,7 +87,7 @@ class RemoteKeywordClient:
|
|
87
87
|
param_desc = param_detail.get('description',
|
88
88
|
f'远程关键字参数: {param_name}')
|
89
89
|
param_default = param_detail.get('default')
|
90
|
-
|
90
|
+
|
91
91
|
# 确保参数名称正确映射
|
92
92
|
parameters.append({
|
93
93
|
'name': param_name,
|
@@ -118,8 +118,10 @@ class RemoteKeywordClient:
|
|
118
118
|
'func': remote_func,
|
119
119
|
'mapping': {p['name']: p['mapping'] for p in parameters},
|
120
120
|
'parameters': [Parameter(**p) for p in parameters],
|
121
|
-
'defaults': {
|
122
|
-
|
121
|
+
'defaults': {
|
122
|
+
p['mapping']: p['default'] for p in parameters
|
123
|
+
if p['default'] is not None
|
124
|
+
}, # 添加默认值支持
|
123
125
|
'remote': True, # 标记为远程关键字
|
124
126
|
'alias': self.alias,
|
125
127
|
'original_name': name
|
@@ -131,7 +133,6 @@ class RemoteKeywordClient:
|
|
131
133
|
'doc': doc,
|
132
134
|
'param_details': param_details # 缓存详细参数信息
|
133
135
|
}
|
134
|
-
|
135
136
|
# 保存参数映射
|
136
137
|
self.param_mappings[name] = param_mapping
|
137
138
|
|
@@ -143,6 +144,9 @@ class RemoteKeywordClient:
|
|
143
144
|
"""执行远程关键字"""
|
144
145
|
name = kwargs.pop('name')
|
145
146
|
|
147
|
+
# 在执行前同步最新的上下文变量
|
148
|
+
self._sync_context_variables_before_execution(kwargs.get('context'))
|
149
|
+
|
146
150
|
# 移除context参数,因为它不能被序列化
|
147
151
|
if 'context' in kwargs:
|
148
152
|
kwargs.pop('context', None)
|
@@ -186,7 +190,6 @@ class RemoteKeywordClient:
|
|
186
190
|
if name in self.keyword_cache:
|
187
191
|
param_names = self.keyword_cache[name]['parameters']
|
188
192
|
print(f"远程关键字 {name} 的参数列表: {param_names}")
|
189
|
-
|
190
193
|
# 不再显示警告信息,因为参数已经在服务器端正确处理
|
191
194
|
# 服务器端会使用默认值或者报错,客户端不需要重复警告
|
192
195
|
|
@@ -209,7 +212,8 @@ class RemoteKeywordClient:
|
|
209
212
|
print(f"远程关键字捕获的变量: {return_data['captures']}")
|
210
213
|
|
211
214
|
# 处理会话状态
|
212
|
-
if 'session_state' in return_data and
|
215
|
+
if ('session_state' in return_data and
|
216
|
+
return_data['session_state']):
|
213
217
|
print(f"远程关键字会话状态: {return_data['session_state']}")
|
214
218
|
|
215
219
|
# 处理响应数据
|
@@ -217,7 +221,8 @@ class RemoteKeywordClient:
|
|
217
221
|
print("远程关键字响应数据: 已接收")
|
218
222
|
|
219
223
|
# 检查是否为新的统一返回格式(包含captures等字段)
|
220
|
-
if ('captures' in return_data or
|
224
|
+
if ('captures' in return_data or
|
225
|
+
'session_state' in return_data or
|
221
226
|
'metadata' in return_data):
|
222
227
|
# 返回完整的新格式,让DSL执行器处理变量捕获
|
223
228
|
return return_data
|
@@ -233,6 +238,72 @@ class RemoteKeywordClient:
|
|
233
238
|
traceback = '\n'.join(result.get('traceback', []))
|
234
239
|
raise Exception(f"远程关键字执行失败: {error_msg}\n{traceback}")
|
235
240
|
|
241
|
+
def _sync_context_variables_before_execution(self, context):
|
242
|
+
"""在执行远程关键字前同步最新的上下文变量
|
243
|
+
|
244
|
+
Args:
|
245
|
+
context: TestContext实例,如果为None则跳过同步
|
246
|
+
"""
|
247
|
+
if context is None:
|
248
|
+
return
|
249
|
+
|
250
|
+
try:
|
251
|
+
# 获取所有上下文变量
|
252
|
+
context_variables = context.get_all_context_variables()
|
253
|
+
|
254
|
+
if not context_variables:
|
255
|
+
print("没有上下文变量需要同步")
|
256
|
+
return
|
257
|
+
|
258
|
+
# 使用统一的序列化工具进行变量过滤
|
259
|
+
from pytest_dsl.core.serialization_utils import XMLRPCSerializer
|
260
|
+
|
261
|
+
# 扩展排除模式
|
262
|
+
exclude_patterns = self.sync_config.get('yaml_exclude_patterns', [
|
263
|
+
'password', 'secret', 'token', 'credential', 'auth',
|
264
|
+
'private', 'remote_servers'
|
265
|
+
])
|
266
|
+
|
267
|
+
variables_to_sync = XMLRPCSerializer.filter_variables(
|
268
|
+
context_variables, exclude_patterns)
|
269
|
+
|
270
|
+
if variables_to_sync:
|
271
|
+
# 调用远程服务器的变量同步接口
|
272
|
+
try:
|
273
|
+
result = self.server.sync_variables_from_client(
|
274
|
+
variables_to_sync, self.api_key)
|
275
|
+
if result.get('status') == 'success':
|
276
|
+
print(f"✅ 实时同步 {len(variables_to_sync)} 个上下文变量到远程服务器")
|
277
|
+
else:
|
278
|
+
print(f"❌ 实时同步变量失败: {result.get('error', '未知错误')}")
|
279
|
+
except Exception as e:
|
280
|
+
print(f"❌ 调用远程变量同步接口失败: {str(e)}")
|
281
|
+
else:
|
282
|
+
print("没有需要实时同步的变量")
|
283
|
+
|
284
|
+
except Exception as e:
|
285
|
+
logger.warning(f"实时变量同步失败: {str(e)}")
|
286
|
+
print(f"❌ 实时变量同步失败: {str(e)}")
|
287
|
+
|
288
|
+
def _collect_context_variables(self, context):
|
289
|
+
"""从TestContext收集所有变量(包括外部提供者变量)
|
290
|
+
|
291
|
+
Args:
|
292
|
+
context: TestContext实例
|
293
|
+
|
294
|
+
Returns:
|
295
|
+
dict: 包含所有上下文变量的字典
|
296
|
+
"""
|
297
|
+
if context is None:
|
298
|
+
return {}
|
299
|
+
|
300
|
+
try:
|
301
|
+
# 使用新的get_all_context_variables方法
|
302
|
+
return context.get_all_context_variables()
|
303
|
+
except Exception as e:
|
304
|
+
logger.warning(f"收集上下文变量失败: {str(e)}")
|
305
|
+
return {}
|
306
|
+
|
236
307
|
def _send_initial_variables(self):
|
237
308
|
"""连接时发送初始变量到远程服务器"""
|
238
309
|
try:
|
@@ -247,17 +318,28 @@ class RemoteKeywordClient:
|
|
247
318
|
variables_to_send.update(self._collect_yaml_variables())
|
248
319
|
|
249
320
|
if variables_to_send:
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
321
|
+
# 使用统一的序列化工具进行变量过滤和转换
|
322
|
+
from pytest_dsl.core.serialization_utils import (
|
323
|
+
XMLRPCSerializer
|
324
|
+
)
|
325
|
+
serializable_variables = XMLRPCSerializer.filter_variables(
|
326
|
+
variables_to_send)
|
327
|
+
|
328
|
+
if serializable_variables:
|
329
|
+
try:
|
330
|
+
# 调用远程服务器的变量接收接口
|
331
|
+
result = self.server.sync_variables_from_client(
|
332
|
+
serializable_variables, self.api_key)
|
333
|
+
if result.get('status') == 'success':
|
334
|
+
print(f"成功传递 {len(serializable_variables)} "
|
335
|
+
f"个变量到远程服务器")
|
336
|
+
else:
|
337
|
+
print(f"传递变量到远程服务器失败: "
|
338
|
+
f"{result.get('error', '未知错误')}")
|
339
|
+
except Exception as e:
|
340
|
+
print(f"调用远程变量接口失败: {str(e)}")
|
341
|
+
else:
|
342
|
+
print("没有可序列化的变量需要传递")
|
261
343
|
else:
|
262
344
|
print("没有需要传递的变量")
|
263
345
|
|
@@ -286,9 +368,17 @@ class RemoteKeywordClient:
|
|
286
368
|
with open(storage_file, 'r', encoding='utf-8') as f:
|
287
369
|
stored_vars = json.load(f)
|
288
370
|
# 只同步g_开头的全局变量
|
289
|
-
|
290
|
-
|
291
|
-
|
371
|
+
global_vars = {
|
372
|
+
name: value for name, value in stored_vars.items()
|
373
|
+
if name.startswith('g_')
|
374
|
+
}
|
375
|
+
if global_vars:
|
376
|
+
from pytest_dsl.core.serialization_utils import (
|
377
|
+
XMLRPCSerializer
|
378
|
+
)
|
379
|
+
filtered_global_vars = XMLRPCSerializer.filter_variables(
|
380
|
+
global_vars)
|
381
|
+
variables.update(filtered_global_vars)
|
292
382
|
except Exception as e:
|
293
383
|
logger.warning(f"收集全局变量失败: {str(e)}")
|
294
384
|
|
@@ -304,47 +394,41 @@ class RemoteKeywordClient:
|
|
304
394
|
yaml_data = yaml_vars._variables
|
305
395
|
if yaml_data:
|
306
396
|
print(f"客户端YAML变量总数: {len(yaml_data)}")
|
307
|
-
|
397
|
+
|
308
398
|
# 检查同步配置中是否指定了特定的键
|
309
399
|
sync_keys = self.sync_config.get('yaml_sync_keys', None)
|
310
|
-
exclude_patterns = self.sync_config.get(
|
311
|
-
'
|
312
|
-
|
313
|
-
|
400
|
+
exclude_patterns = self.sync_config.get(
|
401
|
+
'yaml_exclude_patterns', [
|
402
|
+
'password', 'secret', 'token', 'credential', 'auth',
|
403
|
+
'private', 'remote_servers' # 排除远程服务器配置避免循环
|
404
|
+
]
|
405
|
+
)
|
314
406
|
|
315
407
|
if sync_keys:
|
316
408
|
# 如果指定了特定键,只传递这些键,直接使用原始变量名
|
317
|
-
|
318
|
-
|
319
|
-
|
409
|
+
specific_vars = {
|
410
|
+
key: yaml_data[key] for key in sync_keys
|
411
|
+
if key in yaml_data
|
412
|
+
}
|
413
|
+
if specific_vars:
|
414
|
+
from pytest_dsl.core.serialization_utils import (
|
415
|
+
XMLRPCSerializer
|
416
|
+
)
|
417
|
+
filtered_specific_vars = XMLRPCSerializer.filter_variables(
|
418
|
+
specific_vars)
|
419
|
+
variables.update(filtered_specific_vars)
|
420
|
+
for key in filtered_specific_vars:
|
320
421
|
print(f"传递指定YAML变量: {key}")
|
321
422
|
else:
|
322
423
|
# 传递所有YAML变量,但排除敏感信息
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
break
|
332
|
-
|
333
|
-
# 如果值是字符串,也检查是否包含敏感信息
|
334
|
-
if not should_exclude and isinstance(value, str):
|
335
|
-
value_lower = value.lower()
|
336
|
-
for pattern in exclude_patterns:
|
337
|
-
if (pattern.lower() in value_lower and
|
338
|
-
len(value) < 100): # 只检查短字符串
|
339
|
-
should_exclude = True
|
340
|
-
break
|
341
|
-
|
342
|
-
if not should_exclude:
|
343
|
-
# 直接使用原始变量名,不添加yaml_前缀,实现无缝传递
|
344
|
-
variables[key] = value
|
345
|
-
print(f"传递YAML变量: {key}")
|
346
|
-
else:
|
347
|
-
print(f"跳过敏感YAML变量: {key}")
|
424
|
+
from pytest_dsl.core.serialization_utils import (
|
425
|
+
XMLRPCSerializer
|
426
|
+
)
|
427
|
+
filtered_yaml_vars = XMLRPCSerializer.filter_variables(
|
428
|
+
yaml_data, exclude_patterns)
|
429
|
+
variables.update(filtered_yaml_vars)
|
430
|
+
for key in filtered_yaml_vars:
|
431
|
+
print(f"传递YAML变量: {key}")
|
348
432
|
|
349
433
|
except Exception as e:
|
350
434
|
logger.warning(f"收集YAML变量失败: {str(e)}")
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import xmlrpc.server
|
2
|
-
from functools import partial
|
3
2
|
import inspect
|
4
3
|
import json
|
5
4
|
import sys
|
@@ -8,12 +7,10 @@ import signal
|
|
8
7
|
import atexit
|
9
8
|
import threading
|
10
9
|
import time
|
11
|
-
from typing import Dict, Any, Callable, List
|
12
10
|
|
13
11
|
from pytest_dsl.core.keyword_manager import keyword_manager
|
14
12
|
from pytest_dsl.remote.hook_manager import hook_manager, HookType
|
15
|
-
|
16
|
-
from pytest_dsl.remote import variable_bridge
|
13
|
+
|
17
14
|
|
18
15
|
class RemoteKeywordServer:
|
19
16
|
"""远程关键字服务器,提供关键字的远程调用能力"""
|
@@ -35,7 +32,9 @@ class RemoteKeywordServer:
|
|
35
32
|
|
36
33
|
def _register_builtin_keywords(self):
|
37
34
|
"""注册所有内置关键字,复用本地模式的加载逻辑"""
|
38
|
-
from pytest_dsl.core.plugin_discovery import
|
35
|
+
from pytest_dsl.core.plugin_discovery import (
|
36
|
+
load_all_plugins, scan_local_keywords
|
37
|
+
)
|
39
38
|
|
40
39
|
# 0. 首先加载内置关键字模块(确保内置关键字被注册)
|
41
40
|
print("正在加载内置关键字...")
|
@@ -63,7 +62,8 @@ class RemoteKeywordServer:
|
|
63
62
|
print(f"接收到信号 {signum},正在关闭服务器...")
|
64
63
|
|
65
64
|
# 在新线程中执行关闭逻辑,避免阻塞信号处理器
|
66
|
-
shutdown_thread = threading.Thread(
|
65
|
+
shutdown_thread = threading.Thread(
|
66
|
+
target=self._shutdown_in_thread, daemon=True)
|
67
67
|
shutdown_thread.start()
|
68
68
|
|
69
69
|
# 保存信号处理器引用
|
@@ -119,7 +119,8 @@ class RemoteKeywordServer:
|
|
119
119
|
def start(self):
|
120
120
|
"""启动远程关键字服务器"""
|
121
121
|
try:
|
122
|
-
self.server = xmlrpc.server.SimpleXMLRPCServer(
|
122
|
+
self.server = xmlrpc.server.SimpleXMLRPCServer(
|
123
|
+
(self.host, self.port), allow_none=True)
|
123
124
|
except OSError as e:
|
124
125
|
if "Address already in use" in str(e):
|
125
126
|
print(f"端口 {self.port} 已被占用,请使用其他端口或关闭占用该端口的进程")
|
@@ -207,7 +208,8 @@ class RemoteKeywordServer:
|
|
207
208
|
try:
|
208
209
|
# 确保参数是字典格式
|
209
210
|
if not isinstance(args_dict, dict):
|
210
|
-
args_dict = json.loads(args_dict) if isinstance(
|
211
|
+
args_dict = json.loads(args_dict) if isinstance(
|
212
|
+
args_dict, str) else {}
|
211
213
|
|
212
214
|
# 获取关键字信息
|
213
215
|
keyword_info = keyword_manager.get_keyword_info(name)
|
@@ -226,6 +228,15 @@ class RemoteKeywordServer:
|
|
226
228
|
# 创建测试上下文(所有关键字都需要)
|
227
229
|
from pytest_dsl.core.context import TestContext
|
228
230
|
test_context = TestContext()
|
231
|
+
|
232
|
+
# 设置变量提供者,确保可以访问YAML变量和全局变量
|
233
|
+
try:
|
234
|
+
from pytest_dsl.core.variable_providers import setup_context_with_default_providers
|
235
|
+
setup_context_with_default_providers(test_context)
|
236
|
+
except ImportError:
|
237
|
+
# 如果导入失败,记录警告但继续执行
|
238
|
+
print("警告:无法设置变量提供者")
|
239
|
+
|
229
240
|
exec_kwargs['context'] = test_context
|
230
241
|
|
231
242
|
# 映射参数(通用逻辑)
|
@@ -292,10 +303,10 @@ class RemoteKeywordServer:
|
|
292
303
|
|
293
304
|
def get_keyword_parameter_details(self, name):
|
294
305
|
"""获取关键字的参数详细信息,包括默认值
|
295
|
-
|
306
|
+
|
296
307
|
Args:
|
297
308
|
name: 关键字名称
|
298
|
-
|
309
|
+
|
299
310
|
Returns:
|
300
311
|
list: 参数详细信息列表,每个元素包含name, mapping, description, default
|
301
312
|
"""
|
@@ -311,7 +322,7 @@ class RemoteKeywordServer:
|
|
311
322
|
'description': param.description,
|
312
323
|
'default': param.default
|
313
324
|
})
|
314
|
-
|
325
|
+
|
315
326
|
return param_details
|
316
327
|
|
317
328
|
def get_keyword_documentation(self, name):
|
@@ -405,7 +416,7 @@ class RemoteKeywordServer:
|
|
405
416
|
|
406
417
|
# 将所有同步的变量直接注入到yaml_vars中,实现无缝访问
|
407
418
|
from pytest_dsl.core.yaml_vars import yaml_vars
|
408
|
-
|
419
|
+
|
409
420
|
for name, value in variables.items():
|
410
421
|
# 直接设置到yaml_vars中,确保所有关键字都能无缝访问
|
411
422
|
yaml_vars._variables[name] = value
|
@@ -419,7 +430,7 @@ class RemoteKeywordServer:
|
|
419
430
|
print(f"✓ 全局变量 {name} 已注入到global_context")
|
420
431
|
|
421
432
|
print(f"✅ 总共同步了 {len(variables)} 个变量,全部实现无缝访问")
|
422
|
-
|
433
|
+
|
423
434
|
return {
|
424
435
|
'status': 'success',
|
425
436
|
'message': f'成功同步 {len(variables)} 个变量,全部实现无缝访问'
|
@@ -550,6 +561,7 @@ class RemoteKeywordServer:
|
|
550
561
|
'error': f'列出变量失败: {str(e)}'
|
551
562
|
}
|
552
563
|
|
564
|
+
|
553
565
|
def main():
|
554
566
|
"""启动远程关键字服务器的主函数"""
|
555
567
|
import argparse
|
@@ -572,7 +584,8 @@ def main():
|
|
572
584
|
_auto_load_extensions()
|
573
585
|
|
574
586
|
# 创建并启动服务器(服务器初始化时会自动加载标准关键字)
|
575
|
-
server = RemoteKeywordServer(
|
587
|
+
server = RemoteKeywordServer(
|
588
|
+
host=args.host, port=args.port, api_key=args.api_key)
|
576
589
|
server.start()
|
577
590
|
|
578
591
|
|
@@ -591,7 +604,8 @@ def _load_extensions(extensions_arg):
|
|
591
604
|
if os.path.isfile(ext_path) and ext_path.endswith('.py'):
|
592
605
|
# 加载单个Python文件
|
593
606
|
module_name = os.path.splitext(os.path.basename(ext_path))[0]
|
594
|
-
spec = importlib.util.spec_from_file_location(
|
607
|
+
spec = importlib.util.spec_from_file_location(
|
608
|
+
module_name, ext_path)
|
595
609
|
module = importlib.util.module_from_spec(spec)
|
596
610
|
spec.loader.exec_module(module)
|
597
611
|
print(f"已加载扩展模块: {ext_path}")
|
@@ -601,7 +615,8 @@ def _load_extensions(extensions_arg):
|
|
601
615
|
if filename.endswith('.py') and not filename.startswith('_'):
|
602
616
|
file_path = os.path.join(ext_path, filename)
|
603
617
|
module_name = os.path.splitext(filename)[0]
|
604
|
-
spec = importlib.util.spec_from_file_location(
|
618
|
+
spec = importlib.util.spec_from_file_location(
|
619
|
+
module_name, file_path)
|
605
620
|
module = importlib.util.module_from_spec(spec)
|
606
621
|
spec.loader.exec_module(module)
|
607
622
|
print(f"已加载扩展模块: {file_path}")
|
@@ -631,5 +646,6 @@ def _auto_load_extensions():
|
|
631
646
|
print(f"发现扩展文件: {remote_ext_file}")
|
632
647
|
_load_extensions(remote_ext_file)
|
633
648
|
|
649
|
+
|
634
650
|
if __name__ == '__main__':
|
635
651
|
main()
|
@@ -1,4 +1,4 @@
|
|
1
|
-
pytest_dsl/__init__.py,sha256=
|
1
|
+
pytest_dsl/__init__.py,sha256=Mb4fBll66yNvWOpESky6GF-iHFrdroeS5yRwUOCAs0Q,6813
|
2
2
|
pytest_dsl/cli.py,sha256=kuCEp0FOmlbes8CreAugSwYyU8yoU_oG3HzSGSFh2Cs,13246
|
3
3
|
pytest_dsl/conftest_adapter.py,sha256=cevEb0oEZKTZfUrGe1-CmkFByxKhUtjuurBJP7kpLc0,149
|
4
4
|
pytest_dsl/main_adapter.py,sha256=pUIPN_EzY3JCDlYK7yF_OeLDVqni8vtG15G7gVzPJXg,181
|
@@ -7,9 +7,9 @@ pytest_dsl/core/__init__.py,sha256=ersUoxIWSrisxs9GX_STlH4XAbjNxAWUQB0NboaC5zI,3
|
|
7
7
|
pytest_dsl/core/auth_provider.py,sha256=IZfXXrr4Uuc8QHwRPvhHSzNa2fwrqhjYts1xh78D39Q,14861
|
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
|
-
pytest_dsl/core/context.py,sha256=
|
10
|
+
pytest_dsl/core/context.py,sha256=F6c8U2jSNNqiI_QUn6rn_6pcIi-H_M9bthH-gu92P-0,3985
|
11
11
|
pytest_dsl/core/custom_keyword_manager.py,sha256=SSlsCv3GFMqmtORrqp2I70DvI5vjcBHUgofCgQATbtk,16272
|
12
|
-
pytest_dsl/core/dsl_executor.py,sha256=
|
12
|
+
pytest_dsl/core/dsl_executor.py,sha256=r1lGlakj5PXWguUnQdEoDCaMr6_jYQTizFHEwZkiY5k,65223
|
13
13
|
pytest_dsl/core/dsl_executor_utils.py,sha256=ZJLSYSsiKHqg53d3Bl--ZF8m9abd5kpqdevWl31KEZg,2276
|
14
14
|
pytest_dsl/core/execution_tracker.py,sha256=Pwcxraxt_xkOouq32KBqola-OVfnbaomCoMTyUIqoN4,9476
|
15
15
|
pytest_dsl/core/global_context.py,sha256=1wU0I1fd5I9K3rX90hu6BaodynnYRQNFBFhcecO5eQE,4629
|
@@ -27,6 +27,7 @@ pytest_dsl/core/parser.py,sha256=SvTQ4jgMSe3MITSu9PftraElPAzVaBbNPHMEk1H_lFY,165
|
|
27
27
|
pytest_dsl/core/parsetab.py,sha256=o4XbFKwpsi3fYmfI_F6u5NSM61Qp6gTx-Sfh1jDINxI,31767
|
28
28
|
pytest_dsl/core/plugin_discovery.py,sha256=3pt3EXJ7EPF0rkUlyDZMVHkIiTy2vicdIIQJkrHXZjY,8305
|
29
29
|
pytest_dsl/core/remote_server_registry.py,sha256=MqAf2w0W_5D-zSClD87f9JDNQv-irZ4BrS03dcOFGU0,11046
|
30
|
+
pytest_dsl/core/serialization_utils.py,sha256=b51uywA13vLU_G8rKFVl_BovN7A6mT-XYvn99OpqF54,8303
|
30
31
|
pytest_dsl/core/utils.py,sha256=yAe-PtPTB7gSy8xa_V9UBk4L5SELvTEKiAhkiG4_2rM,5374
|
31
32
|
pytest_dsl/core/validator.py,sha256=2mjw7yiDEMu80FjJ_y2KCS-vA1Tb4kotrKkmLwpRe8Y,17420
|
32
33
|
pytest_dsl/core/variable_providers.py,sha256=ee81Pzy3GlU7q4taoSSd5E7YW87iPdusH0TfxV-0aUw,6198
|
@@ -69,15 +70,15 @@ pytest_dsl/keywords/assertion_keywords.py,sha256=obW06H_3AizsvEM_9VE2JVuwvgrNVqP
|
|
69
70
|
pytest_dsl/keywords/global_keywords.py,sha256=4yw5yeXoGf_4W26F39EA2Pp-mH9GiKGy2jKgFO9a_wM,2509
|
70
71
|
pytest_dsl/keywords/http_keywords.py,sha256=NMOLqD9m7iNLIYKnkQiinZilgLNopd2QzxRgmnliaNc,28976
|
71
72
|
pytest_dsl/keywords/system_keywords.py,sha256=hjsACYER87rseSj4thBFnjDqe6At5hBT4Gjifj4ulDE,24470
|
72
|
-
pytest_dsl/remote/__init__.py,sha256=
|
73
|
+
pytest_dsl/remote/__init__.py,sha256=qNoYEZKl0m5GtDsl5YhLBOhw9Qn0x9lMrmX79ZZxOq8,1844
|
73
74
|
pytest_dsl/remote/hook_manager.py,sha256=0hwRKP8yhcnfAnrrnZGVT-S0TBgo6c0A4qO5XRpvV1U,4899
|
74
|
-
pytest_dsl/remote/keyword_client.py,sha256=
|
75
|
-
pytest_dsl/remote/keyword_server.py,sha256=
|
75
|
+
pytest_dsl/remote/keyword_client.py,sha256=HjVfZxvn549bo9-QYobIubVa4Knch8y5EVvNuVVJEM8,20572
|
76
|
+
pytest_dsl/remote/keyword_server.py,sha256=XSU8ojOhNV5WtvYNwn_HYiAulj7_L35IvtG7FECqGVo,22668
|
76
77
|
pytest_dsl/remote/variable_bridge.py,sha256=dv-d3Gq9ttvvrXM1fdlLtoSOPB6vRp0_GBOwX4wvcy8,7121
|
77
78
|
pytest_dsl/templates/keywords_report.html,sha256=7x84iq6hi08nf1iQ95jZ3izcAUPx6JFm0_8xS85CYws,31241
|
78
|
-
pytest_dsl-0.15.
|
79
|
-
pytest_dsl-0.15.
|
80
|
-
pytest_dsl-0.15.
|
81
|
-
pytest_dsl-0.15.
|
82
|
-
pytest_dsl-0.15.
|
83
|
-
pytest_dsl-0.15.
|
79
|
+
pytest_dsl-0.15.5.dist-info/licenses/LICENSE,sha256=Rguy8cb9sYhK6cmrBdXvwh94rKVDh2tVZEWptsHIsVM,1071
|
80
|
+
pytest_dsl-0.15.5.dist-info/METADATA,sha256=I9tSZQifKuUGk0wE_avhfYNMCYaGdaNKikY4l6o_srA,29655
|
81
|
+
pytest_dsl-0.15.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
82
|
+
pytest_dsl-0.15.5.dist-info/entry_points.txt,sha256=PLOBbH02OGY1XR1JDKIZB1Em87loUvbgMRWaag-5FhY,204
|
83
|
+
pytest_dsl-0.15.5.dist-info/top_level.txt,sha256=4CrSx4uNqxj7NvK6k1y2JZrSrJSzi-UvPZdqpUhumWM,11
|
84
|
+
pytest_dsl-0.15.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|