pytest-dsl 0.15.4__tar.gz → 0.15.5__tar.gz

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.
Files changed (107) hide show
  1. {pytest_dsl-0.15.4/pytest_dsl.egg-info → pytest_dsl-0.15.5}/PKG-INFO +1 -1
  2. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pyproject.toml +1 -1
  3. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/__init__.py +1 -1
  4. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/context.py +23 -0
  5. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/dsl_executor.py +132 -28
  6. pytest_dsl-0.15.5/pytest_dsl/core/serialization_utils.py +231 -0
  7. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/remote/__init__.py +1 -1
  8. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/remote/keyword_client.py +139 -55
  9. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/remote/keyword_server.py +32 -16
  10. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5/pytest_dsl.egg-info}/PKG-INFO +1 -1
  11. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl.egg-info/SOURCES.txt +1 -0
  12. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/LICENSE +0 -0
  13. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/MANIFEST.in +0 -0
  14. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/README.md +0 -0
  15. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/cli.py +0 -0
  16. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/conftest_adapter.py +0 -0
  17. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/__init__.py +0 -0
  18. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/auth_provider.py +0 -0
  19. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/auto_decorator.py +0 -0
  20. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/auto_directory.py +0 -0
  21. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/custom_keyword_manager.py +0 -0
  22. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/dsl_executor_utils.py +0 -0
  23. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/execution_tracker.py +0 -0
  24. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/global_context.py +0 -0
  25. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/hook_manager.py +0 -0
  26. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/hookable_executor.py +0 -0
  27. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/hookable_keyword_manager.py +0 -0
  28. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/hookspecs.py +0 -0
  29. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/http_client.py +0 -0
  30. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/http_request.py +0 -0
  31. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/keyword_loader.py +0 -0
  32. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/keyword_manager.py +0 -0
  33. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/keyword_utils.py +0 -0
  34. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/lexer.py +0 -0
  35. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/parser.py +0 -0
  36. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/parsetab.py +0 -0
  37. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/plugin_discovery.py +0 -0
  38. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/remote_server_registry.py +0 -0
  39. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/utils.py +0 -0
  40. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/validator.py +0 -0
  41. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/variable_providers.py +0 -0
  42. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/variable_utils.py +0 -0
  43. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/yaml_loader.py +0 -0
  44. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/core/yaml_vars.py +0 -0
  45. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/docs/custom_keywords.md +0 -0
  46. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/__init__.py +0 -0
  47. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/assert/assertion_example.auto +0 -0
  48. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/assert/boolean_test.auto +0 -0
  49. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/assert/expression_test.auto +0 -0
  50. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/custom/test_advanced_keywords.auto +0 -0
  51. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/custom/test_custom_keywords.auto +0 -0
  52. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/custom/test_default_values.auto +0 -0
  53. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/__init__.py +0 -0
  54. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/builtin_auth_test.auto +0 -0
  55. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/file_reference_test.auto +0 -0
  56. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/http_advanced.auto +0 -0
  57. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/http_example.auto +0 -0
  58. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/http_length_test.auto +0 -0
  59. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/http_retry_assertions.auto +0 -0
  60. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +0 -0
  61. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/http_with_yaml.auto +0 -0
  62. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/new_retry_test.auto +0 -0
  63. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/retry_assertions_only.auto +0 -0
  64. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/retry_config_only.auto +0 -0
  65. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/retry_debug.auto +0 -0
  66. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/retry_with_fix.auto +0 -0
  67. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/simple_retry.auto +0 -0
  68. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/http/vars.yaml +0 -0
  69. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/quickstart/api_basics.auto +0 -0
  70. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/quickstart/assertions.auto +0 -0
  71. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/quickstart/loops.auto +0 -0
  72. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/test_assert.py +0 -0
  73. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/test_custom_keyword.py +0 -0
  74. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/test_http.py +0 -0
  75. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/examples/test_quickstart.py +0 -0
  76. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/keywords/__init__.py +0 -0
  77. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/keywords/assertion_keywords.py +0 -0
  78. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/keywords/global_keywords.py +0 -0
  79. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/keywords/http_keywords.py +0 -0
  80. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/keywords/system_keywords.py +0 -0
  81. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/main_adapter.py +0 -0
  82. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/plugin.py +0 -0
  83. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/remote/hook_manager.py +0 -0
  84. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/remote/variable_bridge.py +0 -0
  85. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl/templates/keywords_report.html +0 -0
  86. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl.egg-info/dependency_links.txt +0 -0
  87. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl.egg-info/entry_points.txt +0 -0
  88. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl.egg-info/requires.txt +0 -0
  89. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/pytest_dsl.egg-info/top_level.txt +0 -0
  90. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/setup.cfg +0 -0
  91. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/setup.py +0 -0
  92. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/auth_config.yaml +0 -0
  93. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/mock_config.yaml +0 -0
  94. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/simple_config.yaml +0 -0
  95. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_auth_mock_server.py +0 -0
  96. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_auth_runner.py +0 -0
  97. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_end_to_end_seamless.py +0 -0
  98. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_enhanced_variable_access.py +0 -0
  99. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_http_assertions_extractors.py +0 -0
  100. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_mock_server.py +0 -0
  101. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_platform_hook_integration.py +0 -0
  102. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_platform_hook_pytest.py +0 -0
  103. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_retry_runner.py +0 -0
  104. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_seamless_variable_sync.py +0 -0
  105. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_simple_hook_demo.py +0 -0
  106. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_variable_sync.py +0 -0
  107. {pytest_dsl-0.15.4 → pytest_dsl-0.15.5}/tests/test_variable_sync_demo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-dsl
3
- Version: 0.15.4
3
+ Version: 0.15.5
4
4
  Summary: A DSL testing framework based on pytest
5
5
  Author: Chen Shuanglin
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytest-dsl"
7
- version = "0.15.4"
7
+ version = "0.15.5"
8
8
  description = "A DSL testing framework based on pytest"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -12,7 +12,7 @@ pytest-dsl - 基于pytest的DSL测试框架
12
12
  - 自定义关键字支持
13
13
  """
14
14
 
15
- __version__ = "0.15.4"
15
+ __version__ = "0.15.5"
16
16
 
17
17
  # 核心执行器
18
18
  from pytest_dsl.core.dsl_executor import DSLExecutor
@@ -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
 
@@ -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, node_type: str = 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__}: {str(original_exception)}")
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 target_node.line_number:
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 self._current_node and hasattr(self._current_node, 'line_number') and self._current_node.line_number:
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, context_info: str = "", skip_allure_logging: bool = False):
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(expr_node.value)
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(expr_node.value)
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(_eval_expression_impl, expr_node)
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][a-zA-Z0-9_\u4e00-\u9fa5]*'
339
- r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
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
- if (re.match(r'^[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*$', value) and
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(e, context_info=context_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上下文: 执行Assignment节点"
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)}{line_info}\n上下文: 执行AssignmentKeywordCall节点"
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}): {str(e)}{line_info}\n上下文: 执行ForLoop节点"
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上下文: 执行ForLoop节点"
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}{line_info}\n上下文: 执行KeywordCall节点",
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}): {detailed_error}{line_info}\n上下文: 执行KeywordCall节点"
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}{line_info}\n上下文: 执行KeywordCall节点"
1023
+ error_details = (f"参数解析失败: {core_error}"
1024
+ f"{line_info}\n"
1025
+ f"上下文: 执行KeywordCall节点")
943
1026
  else:
944
- error_details = f"参数解析失败: {core_error}{line_info}\n上下文: 执行KeywordCall节点"
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上下文: 执行KeywordCall节点"
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(f"参数解析异常 ({param_name}): {str(e)}")
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)}{line_info}\n上下文: 执行RemoteKeywordCall节点"
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)}{line_info}\n上下文: 执行AssignmentRemoteKeywordCall节点"
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, ReturnException, DSLExecutionError)):
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 setup_context_with_default_providers
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()
@@ -4,7 +4,7 @@ Remote module for pytest-dsl.
4
4
  This module provides remote keyword server functionality.
5
5
  """
6
6
 
7
- __version__ = "0.15.4"
7
+ __version__ = "0.15.5"
8
8
 
9
9
  # 导出远程关键字管理器和相关功能
10
10
  from .keyword_client import remote_keyword_manager, RemoteKeywordManager, RemoteKeywordClient