pytest-dsl 0.15.3__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 +52 -3
- pytest_dsl/cli.py +8 -1
- pytest_dsl/core/context.py +82 -5
- pytest_dsl/core/custom_keyword_manager.py +14 -8
- pytest_dsl/core/dsl_executor.py +159 -35
- pytest_dsl/core/global_context.py +38 -8
- pytest_dsl/core/http_client.py +29 -5
- pytest_dsl/core/keyword_utils.py +7 -0
- pytest_dsl/core/remote_server_registry.py +333 -0
- pytest_dsl/core/serialization_utils.py +231 -0
- pytest_dsl/core/utils.py +34 -27
- pytest_dsl/core/variable_providers.py +202 -0
- pytest_dsl/core/variable_utils.py +45 -38
- pytest_dsl/core/yaml_loader.py +176 -36
- pytest_dsl/keywords/http_keywords.py +9 -5
- pytest_dsl/remote/__init__.py +63 -1
- pytest_dsl/remote/keyword_client.py +139 -55
- pytest_dsl/remote/keyword_server.py +32 -16
- {pytest_dsl-0.15.3.dist-info → pytest_dsl-0.15.5.dist-info}/METADATA +1 -1
- {pytest_dsl-0.15.3.dist-info → pytest_dsl-0.15.5.dist-info}/RECORD +24 -21
- {pytest_dsl-0.15.3.dist-info → pytest_dsl-0.15.5.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.15.3.dist-info → pytest_dsl-0.15.5.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.15.3.dist-info → pytest_dsl-0.15.5.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.15.3.dist-info → pytest_dsl-0.15.5.dist-info}/top_level.txt +0 -0
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
|
|
@@ -72,6 +74,10 @@ class DSLExecutor:
|
|
72
74
|
self.variables = {}
|
73
75
|
self.test_context = TestContext()
|
74
76
|
self.test_context.executor = self # 让 test_context 能够访问到 executor
|
77
|
+
|
78
|
+
# 设置变量提供者,实现YAML变量等外部变量源的注入
|
79
|
+
self._setup_variable_providers()
|
80
|
+
|
75
81
|
self.variable_replacer = VariableReplacer(
|
76
82
|
self.variables, self.test_context)
|
77
83
|
self.imported_files = set() # 跟踪已导入的文件,避免循环导入
|
@@ -104,7 +110,8 @@ class DSLExecutor:
|
|
104
110
|
target_node = node or self._current_node
|
105
111
|
|
106
112
|
# 尝试从当前节点获取行号
|
107
|
-
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):
|
108
115
|
return f"\n行号: {target_node.line_number}"
|
109
116
|
|
110
117
|
# 如果当前节点没有行号,从节点栈中查找最近的有行号的节点
|
@@ -113,12 +120,16 @@ class DSLExecutor:
|
|
113
120
|
return f"\n行号: {stack_node.line_number}"
|
114
121
|
|
115
122
|
# 如果当前节点没有行号,尝试从当前执行的节点获取
|
116
|
-
if
|
123
|
+
if (self._current_node and
|
124
|
+
hasattr(self._current_node, 'line_number') and
|
125
|
+
self._current_node.line_number):
|
117
126
|
return f"\n行号: {self._current_node.line_number}"
|
118
127
|
|
119
128
|
return ""
|
120
129
|
|
121
|
-
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):
|
122
133
|
"""统一处理异常并记录行号信息
|
123
134
|
|
124
135
|
Args:
|
@@ -275,7 +286,8 @@ class DSLExecutor:
|
|
275
286
|
elif expr_node.type == 'StringLiteral':
|
276
287
|
# 字符串字面量,如果包含变量占位符则进行替换,否则直接返回
|
277
288
|
if '${' in expr_node.value:
|
278
|
-
return self.variable_replacer.replace_in_string(
|
289
|
+
return self.variable_replacer.replace_in_string(
|
290
|
+
expr_node.value)
|
279
291
|
else:
|
280
292
|
return expr_node.value
|
281
293
|
elif expr_node.type == 'NumberLiteral':
|
@@ -290,7 +302,8 @@ class DSLExecutor:
|
|
290
302
|
raise KeyError(f"变量 '{var_name}' 不存在")
|
291
303
|
elif expr_node.type == 'PlaceholderRef':
|
292
304
|
# 变量占位符 ${var},进行变量替换
|
293
|
-
return self.variable_replacer.replace_in_string(
|
305
|
+
return self.variable_replacer.replace_in_string(
|
306
|
+
expr_node.value)
|
294
307
|
elif expr_node.type == 'KeywordCall':
|
295
308
|
return self.execute(expr_node)
|
296
309
|
elif expr_node.type == 'ListExpr':
|
@@ -321,7 +334,8 @@ class DSLExecutor:
|
|
321
334
|
else:
|
322
335
|
raise Exception(f"无法求值的表达式类型: {expr_node.type}")
|
323
336
|
|
324
|
-
return self._execute_with_error_handling(
|
337
|
+
return self._execute_with_error_handling(
|
338
|
+
_eval_expression_impl, expr_node)
|
325
339
|
|
326
340
|
def _eval_expression_value(self, value):
|
327
341
|
"""处理表达式值的具体逻辑"""
|
@@ -331,8 +345,10 @@ class DSLExecutor:
|
|
331
345
|
elif isinstance(value, str):
|
332
346
|
# 定义扩展的变量引用模式,支持数组索引和字典键访问
|
333
347
|
pattern = (
|
334
|
-
r'\$\{([a-zA-Z_\u4e00-\u9fa5]
|
335
|
-
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]*)'
|
336
352
|
r'|(?:\[[^\]]+\]))*)\}'
|
337
353
|
)
|
338
354
|
# 检查整个字符串是否完全匹配单一变量引用模式
|
@@ -347,8 +363,10 @@ class DSLExecutor:
|
|
347
363
|
else:
|
348
364
|
# 对于不包含 ${} 的普通字符串,检查是否为单纯的变量名
|
349
365
|
# 只有当字符串是有效的变量名格式且确实存在该变量时,才当作变量处理
|
350
|
-
|
351
|
-
|
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
|
369
|
+
value in self.variable_replacer.local_variables):
|
352
370
|
return self.variable_replacer.local_variables[value]
|
353
371
|
else:
|
354
372
|
# 否则当作字符串字面量处理
|
@@ -357,7 +375,8 @@ class DSLExecutor:
|
|
357
375
|
except Exception as e:
|
358
376
|
# 为变量解析异常添加更多上下文信息
|
359
377
|
context_info = f"解析表达式值 '{value}'"
|
360
|
-
self._handle_exception_with_line_info(
|
378
|
+
self._handle_exception_with_line_info(
|
379
|
+
e, context_info=context_info)
|
361
380
|
|
362
381
|
def _eval_comparison_expr(self, expr_node):
|
363
382
|
"""
|
@@ -366,6 +385,7 @@ class DSLExecutor:
|
|
366
385
|
:param expr_node: 比较表达式节点
|
367
386
|
:return: 比较结果(布尔值)
|
368
387
|
"""
|
388
|
+
operator = "未知" # 设置默认值,避免UnboundLocalError
|
369
389
|
try:
|
370
390
|
left_value = self.eval_expression(expr_node.children[0])
|
371
391
|
right_value = self.eval_expression(expr_node.children[1])
|
@@ -403,6 +423,7 @@ class DSLExecutor:
|
|
403
423
|
:param expr_node: 算术表达式节点
|
404
424
|
:return: 计算结果
|
405
425
|
"""
|
426
|
+
operator = "未知" # 设置默认值,避免UnboundLocalError
|
406
427
|
try:
|
407
428
|
left_value = self.eval_expression(expr_node.children[0])
|
408
429
|
right_value = self.eval_expression(expr_node.children[1])
|
@@ -746,9 +767,15 @@ class DSLExecutor:
|
|
746
767
|
name="赋值详情",
|
747
768
|
attachment_type=allure.attachment_type.TEXT
|
748
769
|
)
|
770
|
+
|
771
|
+
# 通知远程服务器变量已更新
|
772
|
+
self._notify_remote_servers_variable_changed(
|
773
|
+
var_name, expr_value)
|
774
|
+
|
749
775
|
except Exception as e:
|
750
776
|
# 在步骤内部记录异常详情
|
751
|
-
error_details = f"执行Assignment节点: {str(e)}{line_info}\n
|
777
|
+
error_details = (f"执行Assignment节点: {str(e)}{line_info}\n"
|
778
|
+
f"上下文: 执行Assignment节点")
|
752
779
|
allure.attach(
|
753
780
|
error_details,
|
754
781
|
name="DSL执行异常",
|
@@ -789,9 +816,15 @@ class DSLExecutor:
|
|
789
816
|
name="关键字赋值详情",
|
790
817
|
attachment_type=allure.attachment_type.TEXT
|
791
818
|
)
|
819
|
+
|
820
|
+
# 通知远程服务器变量已更新
|
821
|
+
self._notify_remote_servers_variable_changed(var_name, result)
|
822
|
+
|
792
823
|
except Exception as e:
|
793
824
|
# 在步骤内部记录异常详情
|
794
|
-
error_details = f"执行AssignmentKeywordCall节点: {str(e)}
|
825
|
+
error_details = (f"执行AssignmentKeywordCall节点: {str(e)}"
|
826
|
+
f"{line_info}\n"
|
827
|
+
f"上下文: 执行AssignmentKeywordCall节点")
|
795
828
|
allure.attach(
|
796
829
|
error_details,
|
797
830
|
name="DSL执行异常",
|
@@ -800,6 +833,51 @@ class DSLExecutor:
|
|
800
833
|
# 重新抛出异常,让外层的统一异常处理机制处理
|
801
834
|
raise
|
802
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
|
+
|
803
881
|
def _handle_for_loop(self, node):
|
804
882
|
"""处理for循环"""
|
805
883
|
step_name = f"执行循环: {node.value}"
|
@@ -828,6 +906,9 @@ class DSLExecutor:
|
|
828
906
|
self.variable_replacer.local_variables[var_name] = i
|
829
907
|
self.test_context.set(var_name, i)
|
830
908
|
|
909
|
+
# 通知远程服务器循环变量已更新
|
910
|
+
self._notify_remote_servers_variable_changed(var_name, i)
|
911
|
+
|
831
912
|
with allure.step(f"循环轮次: {var_name} = {i}"):
|
832
913
|
try:
|
833
914
|
self.execute(node.children[2])
|
@@ -857,7 +938,9 @@ class DSLExecutor:
|
|
857
938
|
raise e
|
858
939
|
except Exception as e:
|
859
940
|
# 在循环轮次内部记录异常详情
|
860
|
-
error_details = f"循环执行异常 ({var_name} = {i}):
|
941
|
+
error_details = (f"循环执行异常 ({var_name} = {i}): "
|
942
|
+
f"{str(e)}{line_info}\n"
|
943
|
+
f"上下文: 执行ForLoop节点")
|
861
944
|
allure.attach(
|
862
945
|
error_details,
|
863
946
|
name="DSL执行异常",
|
@@ -870,7 +953,8 @@ class DSLExecutor:
|
|
870
953
|
raise
|
871
954
|
except Exception as e:
|
872
955
|
# 在步骤内部记录异常详情
|
873
|
-
error_details = f"执行ForLoop节点: {str(e)}{line_info}\n
|
956
|
+
error_details = (f"执行ForLoop节点: {str(e)}{line_info}\n"
|
957
|
+
f"上下文: 执行ForLoop节点")
|
874
958
|
allure.attach(
|
875
959
|
error_details,
|
876
960
|
name="DSL执行异常",
|
@@ -891,7 +975,8 @@ class DSLExecutor:
|
|
891
975
|
# 在步骤内部记录异常
|
892
976
|
with allure.step(f"调用关键字: {keyword_name}"):
|
893
977
|
allure.attach(
|
894
|
-
f"执行KeywordCall节点: 未注册的关键字: {keyword_name}
|
978
|
+
f"执行KeywordCall节点: 未注册的关键字: {keyword_name}"
|
979
|
+
f"{line_info}\n上下文: 执行KeywordCall节点",
|
895
980
|
name="DSL执行异常",
|
896
981
|
attachment_type=allure.attachment_type.TEXT
|
897
982
|
)
|
@@ -903,7 +988,7 @@ class DSLExecutor:
|
|
903
988
|
try:
|
904
989
|
# 准备参数(这里可能抛出参数解析异常)
|
905
990
|
kwargs = self._prepare_keyword_params(node, keyword_info)
|
906
|
-
|
991
|
+
|
907
992
|
# 传递自定义步骤名称给KeywordManager,避免重复的allure步骤嵌套
|
908
993
|
kwargs['step_name'] = keyword_name # 内层步骤只显示关键字名称
|
909
994
|
# 避免KeywordManager重复记录,由DSL执行器统一记录
|
@@ -927,18 +1012,26 @@ class DSLExecutor:
|
|
927
1012
|
if "参数解析异常" in core_error:
|
928
1013
|
# 提取参数名和具体错误
|
929
1014
|
import re
|
930
|
-
match = re.search(
|
1015
|
+
match = re.search(
|
1016
|
+
r'参数解析异常 \(([^)]+)\): (.+)', core_error)
|
931
1017
|
if match:
|
932
1018
|
param_name, detailed_error = match.groups()
|
933
|
-
error_details = f"参数解析失败 ({param_name}):
|
1019
|
+
error_details = (f"参数解析失败 ({param_name}): "
|
1020
|
+
f"{detailed_error}{line_info}\n"
|
1021
|
+
f"上下文: 执行KeywordCall节点")
|
934
1022
|
else:
|
935
|
-
error_details = f"参数解析失败: {core_error}
|
1023
|
+
error_details = (f"参数解析失败: {core_error}"
|
1024
|
+
f"{line_info}\n"
|
1025
|
+
f"上下文: 执行KeywordCall节点")
|
936
1026
|
else:
|
937
|
-
error_details = f"参数解析失败: {core_error}
|
1027
|
+
error_details = (f"参数解析失败: {core_error}"
|
1028
|
+
f"{line_info}\n"
|
1029
|
+
f"上下文: 执行KeywordCall节点")
|
938
1030
|
else:
|
939
1031
|
# 其他异常
|
940
|
-
error_details = f"执行KeywordCall节点: {str(e)}{line_info}\n
|
941
|
-
|
1032
|
+
error_details = (f"执行KeywordCall节点: {str(e)}{line_info}\n"
|
1033
|
+
f"上下文: 执行KeywordCall节点")
|
1034
|
+
|
942
1035
|
allure.attach(
|
943
1036
|
error_details,
|
944
1037
|
name="DSL执行异常",
|
@@ -951,21 +1044,20 @@ class DSLExecutor:
|
|
951
1044
|
"""准备关键字调用参数"""
|
952
1045
|
mapping = keyword_info.get('mapping', {})
|
953
1046
|
kwargs = {'context': self.test_context} # 默认传入context参数
|
954
|
-
line_info = self._get_line_info(node)
|
955
1047
|
|
956
1048
|
# 检查是否有参数列表
|
957
1049
|
if node.children[0]:
|
958
1050
|
for param in node.children[0]:
|
959
1051
|
param_name = param.value
|
960
1052
|
english_param_name = mapping.get(param_name, param_name)
|
961
|
-
|
1053
|
+
|
962
1054
|
# 在子步骤中处理参数值解析,但不记录异常详情
|
963
1055
|
with allure.step(f"解析参数: {param_name}"):
|
964
1056
|
try:
|
965
1057
|
# 对参数值进行变量替换
|
966
1058
|
param_value = self.eval_expression(param.children[0])
|
967
1059
|
kwargs[english_param_name] = param_value
|
968
|
-
|
1060
|
+
|
969
1061
|
# 只记录参数解析成功的简要信息
|
970
1062
|
allure.attach(
|
971
1063
|
f"参数名: {param_name}\n"
|
@@ -975,7 +1067,8 @@ class DSLExecutor:
|
|
975
1067
|
)
|
976
1068
|
except Exception as e:
|
977
1069
|
# 将异常重新包装,添加参数名信息,但不在这里记录到allure
|
978
|
-
raise Exception(
|
1070
|
+
raise Exception(
|
1071
|
+
f"参数解析异常 ({param_name}): {str(e)}")
|
979
1072
|
|
980
1073
|
return kwargs
|
981
1074
|
|
@@ -1104,7 +1197,8 @@ class DSLExecutor:
|
|
1104
1197
|
return result
|
1105
1198
|
except Exception as e:
|
1106
1199
|
# 在步骤内部记录异常详情
|
1107
|
-
error_details = f"执行RemoteKeywordCall节点: {str(e)}
|
1200
|
+
error_details = (f"执行RemoteKeywordCall节点: {str(e)}"
|
1201
|
+
f"{line_info}\n上下文: 执行RemoteKeywordCall节点")
|
1108
1202
|
allure.attach(
|
1109
1203
|
error_details,
|
1110
1204
|
name="DSL执行异常",
|
@@ -1144,7 +1238,8 @@ class DSLExecutor:
|
|
1144
1238
|
else:
|
1145
1239
|
self.variable_replacer.local_variables[
|
1146
1240
|
capture_var] = capture_value
|
1147
|
-
self.test_context.set(
|
1241
|
+
self.test_context.set(
|
1242
|
+
capture_var, capture_value)
|
1148
1243
|
|
1149
1244
|
# 将主要结果赋值给指定变量
|
1150
1245
|
actual_result = main_result
|
@@ -1170,12 +1265,26 @@ class DSLExecutor:
|
|
1170
1265
|
name="远程关键字赋值",
|
1171
1266
|
attachment_type=allure.attachment_type.TEXT
|
1172
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)
|
1173
1280
|
else:
|
1174
1281
|
error_msg = "远程关键字没有返回结果"
|
1175
1282
|
raise Exception(error_msg)
|
1176
1283
|
except Exception as e:
|
1177
1284
|
# 在步骤内部记录异常详情
|
1178
|
-
error_details = f"执行AssignmentRemoteKeywordCall节点: {str(e)}
|
1285
|
+
error_details = (f"执行AssignmentRemoteKeywordCall节点: {str(e)}"
|
1286
|
+
f"{line_info}\n"
|
1287
|
+
f"上下文: 执行AssignmentRemoteKeywordCall节点")
|
1179
1288
|
allure.attach(
|
1180
1289
|
error_details,
|
1181
1290
|
name="DSL执行异常",
|
@@ -1249,7 +1358,8 @@ class DSLExecutor:
|
|
1249
1358
|
self.execution_tracker.finish_current_step(error=error_msg)
|
1250
1359
|
|
1251
1360
|
# 如果是控制流异常或已经是DSLExecutionError,直接重抛
|
1252
|
-
if isinstance(e, (BreakException, ContinueException,
|
1361
|
+
if isinstance(e, (BreakException, ContinueException,
|
1362
|
+
ReturnException, DSLExecutionError)):
|
1253
1363
|
raise
|
1254
1364
|
|
1255
1365
|
# 如果是断言异常,保持原样但可能添加行号信息
|
@@ -1265,7 +1375,7 @@ class DSLExecutor:
|
|
1265
1375
|
# 其他异常使用统一处理机制
|
1266
1376
|
# 对于这些节点类型,异常已经在步骤中记录过了,跳过重复记录
|
1267
1377
|
step_handled_nodes = {
|
1268
|
-
'KeywordCall', 'Assignment', 'AssignmentKeywordCall',
|
1378
|
+
'KeywordCall', 'Assignment', 'AssignmentKeywordCall',
|
1269
1379
|
'ForLoop', 'RemoteKeywordCall', 'AssignmentRemoteKeywordCall'
|
1270
1380
|
}
|
1271
1381
|
skip_logging = node.type in step_handled_nodes
|
@@ -1312,6 +1422,20 @@ class DSLExecutor:
|
|
1312
1422
|
f"hooks_enabled={self.enable_hooks}, "
|
1313
1423
|
f"tracking_enabled={self.enable_tracking})")
|
1314
1424
|
|
1425
|
+
def _setup_variable_providers(self):
|
1426
|
+
"""设置变量提供者,将外部变量源注入到TestContext中"""
|
1427
|
+
try:
|
1428
|
+
from .variable_providers import (
|
1429
|
+
setup_context_with_default_providers
|
1430
|
+
)
|
1431
|
+
setup_context_with_default_providers(self.test_context)
|
1432
|
+
|
1433
|
+
# 同步常用变量到context中,提高访问性能
|
1434
|
+
self.test_context.sync_variables_from_external_sources()
|
1435
|
+
except ImportError as e:
|
1436
|
+
# 如果导入失败,记录警告但不影响正常功能
|
1437
|
+
print(f"警告:无法设置变量提供者: {e}")
|
1438
|
+
|
1315
1439
|
def _init_hooks(self):
|
1316
1440
|
"""初始化hook机制"""
|
1317
1441
|
try:
|
@@ -4,7 +4,6 @@ import tempfile
|
|
4
4
|
import allure
|
5
5
|
from typing import Dict, Any, Optional
|
6
6
|
from filelock import FileLock
|
7
|
-
from .yaml_vars import yaml_vars
|
8
7
|
|
9
8
|
|
10
9
|
class GlobalContext:
|
@@ -19,6 +18,20 @@ class GlobalContext:
|
|
19
18
|
self._storage_dir, "global_vars.json")
|
20
19
|
self._lock_file = os.path.join(self._storage_dir, "global_vars.lock")
|
21
20
|
|
21
|
+
# 初始化变量提供者(延迟加载,避免循环导入)
|
22
|
+
self._yaml_provider = None
|
23
|
+
|
24
|
+
def _get_yaml_provider(self):
|
25
|
+
"""延迟获取YAML变量提供者,避免循环导入"""
|
26
|
+
if self._yaml_provider is None:
|
27
|
+
try:
|
28
|
+
from .variable_providers import YAMLVariableProvider
|
29
|
+
self._yaml_provider = YAMLVariableProvider()
|
30
|
+
except ImportError:
|
31
|
+
# 如果变量提供者不可用,创建一个空的提供者
|
32
|
+
self._yaml_provider = _EmptyProvider()
|
33
|
+
return self._yaml_provider
|
34
|
+
|
22
35
|
def set_variable(self, name: str, value: Any) -> None:
|
23
36
|
"""设置全局变量"""
|
24
37
|
with FileLock(self._lock_file):
|
@@ -34,8 +47,9 @@ class GlobalContext:
|
|
34
47
|
|
35
48
|
def get_variable(self, name: str) -> Any:
|
36
49
|
"""获取全局变量,优先从YAML变量中获取"""
|
37
|
-
# 首先尝试从YAML
|
38
|
-
|
50
|
+
# 首先尝试从YAML变量中获取(通过变量提供者)
|
51
|
+
yaml_provider = self._get_yaml_provider()
|
52
|
+
yaml_value = yaml_provider.get_variable(name)
|
39
53
|
if yaml_value is not None:
|
40
54
|
return yaml_value
|
41
55
|
|
@@ -46,8 +60,9 @@ class GlobalContext:
|
|
46
60
|
|
47
61
|
def has_variable(self, name: str) -> bool:
|
48
62
|
"""检查全局变量是否存在(包括YAML变量)"""
|
49
|
-
# 首先检查YAML
|
50
|
-
|
63
|
+
# 首先检查YAML变量(通过变量提供者)
|
64
|
+
yaml_provider = self._get_yaml_provider()
|
65
|
+
if yaml_provider.has_variable(name):
|
51
66
|
return True
|
52
67
|
|
53
68
|
# 然后检查全局变量存储
|
@@ -73,9 +88,11 @@ class GlobalContext:
|
|
73
88
|
"""清除所有全局变量(包括YAML变量)"""
|
74
89
|
with FileLock(self._lock_file):
|
75
90
|
self._save_variables({})
|
76
|
-
|
77
|
-
# 清除YAML
|
78
|
-
|
91
|
+
|
92
|
+
# 清除YAML变量(通过变量提供者)
|
93
|
+
yaml_provider = self._get_yaml_provider()
|
94
|
+
if hasattr(yaml_provider, 'clear'):
|
95
|
+
yaml_provider.clear()
|
79
96
|
|
80
97
|
allure.attach(
|
81
98
|
"清除所有全局变量",
|
@@ -99,5 +116,18 @@ class GlobalContext:
|
|
99
116
|
json.dump(variables, f, ensure_ascii=False, indent=2)
|
100
117
|
|
101
118
|
|
119
|
+
class _EmptyProvider:
|
120
|
+
"""空的变量提供者,用作后备方案"""
|
121
|
+
|
122
|
+
def get_variable(self, key: str) -> Optional[Any]:
|
123
|
+
return None
|
124
|
+
|
125
|
+
def has_variable(self, key: str) -> bool:
|
126
|
+
return False
|
127
|
+
|
128
|
+
def clear(self):
|
129
|
+
pass
|
130
|
+
|
131
|
+
|
102
132
|
# 创建全局上下文管理器实例
|
103
133
|
global_context = GlobalContext()
|
pytest_dsl/core/http_client.py
CHANGED
@@ -3,7 +3,6 @@ import logging
|
|
3
3
|
from typing import Dict, Any
|
4
4
|
import requests
|
5
5
|
from urllib.parse import urljoin
|
6
|
-
from pytest_dsl.core.yaml_vars import yaml_vars
|
7
6
|
from pytest_dsl.core.auth_provider import create_auth_provider
|
8
7
|
|
9
8
|
logger = logging.getLogger(__name__)
|
@@ -289,6 +288,31 @@ class HTTPClientManager:
|
|
289
288
|
"""初始化客户端管理器"""
|
290
289
|
self._clients: Dict[str, HTTPClient] = {}
|
291
290
|
self._sessions: Dict[str, HTTPClient] = {}
|
291
|
+
self._context = None # 添加context引用
|
292
|
+
|
293
|
+
def set_context(self, context):
|
294
|
+
"""设置测试上下文,用于获取HTTP客户端配置
|
295
|
+
|
296
|
+
Args:
|
297
|
+
context: TestContext实例
|
298
|
+
"""
|
299
|
+
self._context = context
|
300
|
+
|
301
|
+
def _get_http_clients_config(self) -> Dict[str, Any]:
|
302
|
+
"""从context获取HTTP客户端配置
|
303
|
+
|
304
|
+
Returns:
|
305
|
+
HTTP客户端配置字典
|
306
|
+
"""
|
307
|
+
if self._context:
|
308
|
+
return self._context.get("http_clients") or {}
|
309
|
+
|
310
|
+
# 如果没有context,尝试从yaml_vars获取(兼容性)
|
311
|
+
try:
|
312
|
+
from pytest_dsl.core.yaml_vars import yaml_vars
|
313
|
+
return yaml_vars.get_variable("http_clients") or {}
|
314
|
+
except ImportError:
|
315
|
+
return {}
|
292
316
|
|
293
317
|
def create_client(self, config: Dict[str, Any]) -> HTTPClient:
|
294
318
|
"""从配置创建客户端
|
@@ -326,8 +350,8 @@ class HTTPClientManager:
|
|
326
350
|
if name in self._clients:
|
327
351
|
return self._clients[name]
|
328
352
|
|
329
|
-
# 从
|
330
|
-
http_clients =
|
353
|
+
# 从context获取HTTP客户端配置(统一的变量获取方式)
|
354
|
+
http_clients = self._get_http_clients_config()
|
331
355
|
client_config = http_clients.get(name)
|
332
356
|
|
333
357
|
if not client_config:
|
@@ -377,7 +401,7 @@ class HTTPClientManager:
|
|
377
401
|
return session
|
378
402
|
|
379
403
|
def _get_client_config(self, name: str) -> Dict[str, Any]:
|
380
|
-
"""从
|
404
|
+
"""从context获取客户端配置
|
381
405
|
|
382
406
|
Args:
|
383
407
|
name: 客户端名称
|
@@ -385,7 +409,7 @@ class HTTPClientManager:
|
|
385
409
|
Returns:
|
386
410
|
客户端配置
|
387
411
|
"""
|
388
|
-
http_clients =
|
412
|
+
http_clients = self._get_http_clients_config()
|
389
413
|
client_config = http_clients.get(name)
|
390
414
|
|
391
415
|
if not client_config and name == "default":
|
pytest_dsl/core/keyword_utils.py
CHANGED
@@ -210,6 +210,13 @@ class KeywordFormatter:
|
|
210
210
|
'parameters': keyword_info.parameters
|
211
211
|
}
|
212
212
|
|
213
|
+
# 添加来源字段,优先显示项目自定义关键字的文件位置
|
214
|
+
if keyword_info.file_location:
|
215
|
+
keyword_data['source'] = keyword_info.file_location
|
216
|
+
else:
|
217
|
+
keyword_data['source'] = keyword_info.source_info.get(
|
218
|
+
'display_name', keyword_info.source_info.get('name', '未知'))
|
219
|
+
|
213
220
|
# 远程关键字特殊信息
|
214
221
|
if keyword_info.remote_info:
|
215
222
|
keyword_data['remote'] = keyword_info.remote_info
|