pytest-dsl 0.11.0__py3-none-any.whl → 0.11.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_dsl/core/http_client.py +2 -2
- pytest_dsl/keywords/http_keywords.py +90 -34
- pytest_dsl/remote/keyword_client.py +30 -17
- pytest_dsl/remote/keyword_server.py +18 -1
- pytest_dsl/remote/variable_bridge.py +41 -8
- {pytest_dsl-0.11.0.dist-info → pytest_dsl-0.11.1.dist-info}/METADATA +1 -1
- {pytest_dsl-0.11.0.dist-info → pytest_dsl-0.11.1.dist-info}/RECORD +11 -11
- {pytest_dsl-0.11.0.dist-info → pytest_dsl-0.11.1.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.11.0.dist-info → pytest_dsl-0.11.1.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.11.0.dist-info → pytest_dsl-0.11.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.11.0.dist-info → pytest_dsl-0.11.1.dist-info}/top_level.txt +0 -0
pytest_dsl/core/http_client.py
CHANGED
@@ -132,7 +132,7 @@ class HTTPClient:
|
|
132
132
|
request_kwargs = self.auth_provider.pre_request_hook(method, url, request_kwargs)
|
133
133
|
|
134
134
|
# 记录请求详情
|
135
|
-
logger.debug(
|
135
|
+
logger.debug("=== HTTP请求详情 ===")
|
136
136
|
logger.debug(f"方法: {method}")
|
137
137
|
logger.debug(f"URL: {url}")
|
138
138
|
if 'headers' in request_kwargs:
|
@@ -171,7 +171,7 @@ class HTTPClient:
|
|
171
171
|
response = requests.request(method, url, **request_kwargs)
|
172
172
|
|
173
173
|
# 记录响应详情
|
174
|
-
logger.debug(
|
174
|
+
logger.debug("\n=== HTTP响应详情 ===")
|
175
175
|
logger.debug(f"状态码: {response.status_code}")
|
176
176
|
logger.debug(f"响应头: {json.dumps(dict(response.headers), indent=2, ensure_ascii=False)}")
|
177
177
|
try:
|
@@ -20,7 +20,10 @@ from pytest_dsl.core.context import TestContext
|
|
20
20
|
# 配置日志
|
21
21
|
logger = logging.getLogger(__name__)
|
22
22
|
|
23
|
-
|
23
|
+
|
24
|
+
def _process_file_reference(reference: Union[str, Dict[str, Any]],
|
25
|
+
allow_vars: bool = True,
|
26
|
+
test_context: TestContext = None) -> Any:
|
24
27
|
"""处理文件引用,加载外部文件内容
|
25
28
|
|
26
29
|
支持两种语法:
|
@@ -43,7 +46,8 @@ def _process_file_reference(reference: Union[str, Dict[str, Any]], allow_vars: b
|
|
43
46
|
if match:
|
44
47
|
file_path = match.group(1).strip()
|
45
48
|
is_template = '_template' in reference[:15] # 检查是否为模板
|
46
|
-
return _load_file_content(file_path, is_template, 'auto', 'utf-8',
|
49
|
+
return _load_file_content(file_path, is_template, 'auto', 'utf-8',
|
50
|
+
test_context)
|
47
51
|
|
48
52
|
# 处理详细语法
|
49
53
|
elif isinstance(reference, dict) and 'file_ref' in reference:
|
@@ -51,7 +55,8 @@ def _process_file_reference(reference: Union[str, Dict[str, Any]], allow_vars: b
|
|
51
55
|
|
52
56
|
if isinstance(file_ref, str):
|
53
57
|
# 如果file_ref是字符串,使用默认配置
|
54
|
-
return _load_file_content(file_ref, allow_vars, 'auto', 'utf-8',
|
58
|
+
return _load_file_content(file_ref, allow_vars, 'auto', 'utf-8',
|
59
|
+
test_context)
|
55
60
|
elif isinstance(file_ref, dict):
|
56
61
|
# 如果file_ref是字典,使用自定义配置
|
57
62
|
file_path = file_ref.get('path')
|
@@ -62,14 +67,16 @@ def _process_file_reference(reference: Union[str, Dict[str, Any]], allow_vars: b
|
|
62
67
|
file_type = file_ref.get('type', 'auto')
|
63
68
|
encoding = file_ref.get('encoding', 'utf-8')
|
64
69
|
|
65
|
-
return _load_file_content(file_path, template, file_type, encoding,
|
70
|
+
return _load_file_content(file_path, template, file_type, encoding,
|
71
|
+
test_context)
|
66
72
|
|
67
73
|
# 如果不是文件引用,返回原始值
|
68
74
|
return reference
|
69
75
|
|
70
76
|
|
71
77
|
def _load_file_content(file_path: str, is_template: bool = False,
|
72
|
-
file_type: str = 'auto', encoding: str = 'utf-8',
|
78
|
+
file_type: str = 'auto', encoding: str = 'utf-8',
|
79
|
+
test_context: TestContext = None) -> Any:
|
73
80
|
"""加载文件内容
|
74
81
|
|
75
82
|
Args:
|
@@ -122,7 +129,8 @@ def _load_file_content(file_path: str, is_template: bool = False,
|
|
122
129
|
return content
|
123
130
|
|
124
131
|
|
125
|
-
def _process_request_config(config: Dict[str, Any],
|
132
|
+
def _process_request_config(config: Dict[str, Any],
|
133
|
+
test_context: TestContext = None) -> Dict[str, Any]:
|
126
134
|
"""处理请求配置,检查并处理文件引用
|
127
135
|
|
128
136
|
Args:
|
@@ -140,20 +148,24 @@ def _process_request_config(config: Dict[str, Any], test_context: TestContext =
|
|
140
148
|
|
141
149
|
# 处理json字段
|
142
150
|
if 'json' in request:
|
143
|
-
request['json'] = _process_file_reference(
|
151
|
+
request['json'] = _process_file_reference(
|
152
|
+
request['json'], test_context=test_context)
|
144
153
|
|
145
154
|
# 处理data字段
|
146
155
|
if 'data' in request:
|
147
|
-
request['data'] = _process_file_reference(
|
156
|
+
request['data'] = _process_file_reference(
|
157
|
+
request['data'], test_context=test_context)
|
148
158
|
|
149
159
|
# 处理headers字段
|
150
160
|
if 'headers' in request:
|
151
|
-
request['headers'] = _process_file_reference(
|
161
|
+
request['headers'] = _process_file_reference(
|
162
|
+
request['headers'], test_context=test_context)
|
152
163
|
|
153
164
|
return config
|
154
165
|
|
155
166
|
|
156
|
-
def _normalize_retry_config(config, assert_retry_count=None,
|
167
|
+
def _normalize_retry_config(config, assert_retry_count=None,
|
168
|
+
assert_retry_interval=None):
|
157
169
|
"""标准化断言重试配置
|
158
170
|
|
159
171
|
将不同来源的重试配置(命令行参数、retry配置、retry_assertions配置)
|
@@ -223,14 +235,23 @@ def _normalize_retry_config(config, assert_retry_count=None, assert_retry_interv
|
|
223
235
|
|
224
236
|
|
225
237
|
@keyword_manager.register('HTTP请求', [
|
226
|
-
{'name': '客户端', 'mapping': 'client',
|
227
|
-
|
228
|
-
|
229
|
-
{'name': '
|
230
|
-
|
231
|
-
{'name': '
|
232
|
-
|
233
|
-
{'name': '
|
238
|
+
{'name': '客户端', 'mapping': 'client',
|
239
|
+
'description': '客户端名称,对应YAML变量文件中的客户端配置',
|
240
|
+
'default': 'default'},
|
241
|
+
{'name': '配置', 'mapping': 'config',
|
242
|
+
'description': '包含请求、捕获和断言的YAML配置'},
|
243
|
+
{'name': '会话', 'mapping': 'session',
|
244
|
+
'description': '会话名称,用于在多个请求间保持会话状态'},
|
245
|
+
{'name': '保存响应', 'mapping': 'save_response',
|
246
|
+
'description': '将完整响应保存到指定变量名中'},
|
247
|
+
{'name': '禁用授权', 'mapping': 'disable_auth',
|
248
|
+
'description': '禁用客户端配置中的授权机制', 'default': False},
|
249
|
+
{'name': '模板', 'mapping': 'template',
|
250
|
+
'description': '使用YAML变量文件中定义的请求模板'},
|
251
|
+
{'name': '断言重试次数', 'mapping': 'assert_retry_count',
|
252
|
+
'description': '断言失败时的重试次数', 'default': 0},
|
253
|
+
{'name': '断言重试间隔', 'mapping': 'assert_retry_interval',
|
254
|
+
'description': '断言重试间隔时间(秒)', 'default': 1}
|
234
255
|
])
|
235
256
|
def http_request(context, **kwargs):
|
236
257
|
"""执行HTTP请求
|
@@ -260,7 +281,26 @@ def http_request(context, **kwargs):
|
|
260
281
|
assert_retry_count = kwargs.get('assert_retry_count')
|
261
282
|
assert_retry_interval = kwargs.get('assert_retry_interval')
|
262
283
|
|
263
|
-
|
284
|
+
# 添加调试信息,检查客户端配置是否可用
|
285
|
+
print(f"🌐 HTTP请求 - 客户端: {client_name}")
|
286
|
+
|
287
|
+
# 检查YAML变量中的http_clients配置(现在包含同步的变量)
|
288
|
+
http_clients_config = yaml_vars.get_variable("http_clients")
|
289
|
+
if http_clients_config:
|
290
|
+
print(f"✓ 找到http_clients配置,包含 {len(http_clients_config)} 个客户端")
|
291
|
+
if client_name in http_clients_config:
|
292
|
+
print(f"✓ 找到客户端 '{client_name}' 的配置")
|
293
|
+
client_config = http_clients_config[client_name]
|
294
|
+
print(f" - base_url: {client_config.get('base_url', 'N/A')}")
|
295
|
+
print(f" - timeout: {client_config.get('timeout', 'N/A')}")
|
296
|
+
else:
|
297
|
+
print(f"⚠️ 未找到客户端 '{client_name}' 的配置")
|
298
|
+
print(f" 可用客户端: {list(http_clients_config.keys())}")
|
299
|
+
else:
|
300
|
+
print("⚠️ 未找到http_clients配置")
|
301
|
+
|
302
|
+
with allure.step(f"发送HTTP请求 (客户端: {client_name}"
|
303
|
+
f"{', 会话: ' + session_name if session_name else ''})"):
|
264
304
|
# 处理模板
|
265
305
|
if template_name:
|
266
306
|
# 从YAML变量中获取模板
|
@@ -299,7 +339,8 @@ def http_request(context, **kwargs):
|
|
299
339
|
raise ValueError(f"无效的YAML配置: {str(e)}")
|
300
340
|
|
301
341
|
# 统一处理重试配置
|
302
|
-
retry_config = _normalize_retry_config(config, assert_retry_count,
|
342
|
+
retry_config = _normalize_retry_config(config, assert_retry_count,
|
343
|
+
assert_retry_interval)
|
303
344
|
|
304
345
|
# 为了兼容性,将标准化后的重试配置写回到配置中
|
305
346
|
if retry_config['enabled']:
|
@@ -344,7 +385,8 @@ def http_request(context, **kwargs):
|
|
344
385
|
if session_name:
|
345
386
|
try:
|
346
387
|
from pytest_dsl.core.http_client import http_client_manager
|
347
|
-
session_client = http_client_manager.get_session(
|
388
|
+
session_client = http_client_manager.get_session(
|
389
|
+
session_name, client_name)
|
348
390
|
if session_client and session_client._session:
|
349
391
|
session_state = {
|
350
392
|
"cookies": dict(session_client._session.cookies),
|
@@ -375,7 +417,8 @@ def http_request(context, **kwargs):
|
|
375
417
|
return {
|
376
418
|
"result": captured_values, # 主要返回值保持兼容
|
377
419
|
"captures": captured_values, # 明确的捕获变量
|
378
|
-
"session_state": {session_name: session_state}
|
420
|
+
"session_state": ({session_name: session_state}
|
421
|
+
if session_state else {}),
|
379
422
|
"response": response_data, # 完整响应(如果需要)
|
380
423
|
"metadata": {
|
381
424
|
"response_time": getattr(response, 'elapsed', None),
|
@@ -396,7 +439,8 @@ def _deep_merge(dict1, dict2):
|
|
396
439
|
合并后的字典
|
397
440
|
"""
|
398
441
|
for key in dict2:
|
399
|
-
if key in dict1 and isinstance(dict1[key], dict) and
|
442
|
+
if (key in dict1 and isinstance(dict1[key], dict) and
|
443
|
+
isinstance(dict2[key], dict)):
|
400
444
|
_deep_merge(dict1[key], dict2[key])
|
401
445
|
else:
|
402
446
|
dict1[key] = dict2[key]
|
@@ -424,7 +468,8 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
|
|
424
468
|
)
|
425
469
|
|
426
470
|
# 添加一个特殊的标记到配置中,表示我们只想收集失败的断言而不抛出异常
|
427
|
-
original_config = http_req.config.copy()
|
471
|
+
original_config = (http_req.config.copy()
|
472
|
+
if isinstance(http_req.config, dict) else {})
|
428
473
|
|
429
474
|
# 创建一个临时副本
|
430
475
|
temp_config = original_config.copy()
|
@@ -442,7 +487,8 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
|
|
442
487
|
except Exception as collect_err:
|
443
488
|
# 出现意外错误时记录
|
444
489
|
allure.attach(
|
445
|
-
f"收集失败断言时出错: {type(collect_err).__name__}:
|
490
|
+
f"收集失败断言时出错: {type(collect_err).__name__}: "
|
491
|
+
f"{str(collect_err)}",
|
446
492
|
name="断言收集错误",
|
447
493
|
attachment_type=allure.attachment_type.TEXT
|
448
494
|
)
|
@@ -513,16 +559,18 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
|
|
513
559
|
|
514
560
|
# 找出所有断言中最大的重试次数
|
515
561
|
for retryable_assertion in retryable_assertions:
|
516
|
-
max_retry_count = max(max_retry_count,
|
562
|
+
max_retry_count = max(max_retry_count,
|
563
|
+
retryable_assertion.get('retry_count', 3))
|
517
564
|
|
518
565
|
# 进行断言重试
|
519
|
-
for attempt in range(1, max_retry_count + 1):
|
566
|
+
for attempt in range(1, max_retry_count + 1):
|
520
567
|
# 等待重试间隔
|
521
568
|
with allure.step(f"断言重试 (尝试 {attempt}/{max_retry_count})"):
|
522
569
|
# 确定本次重试的间隔时间(使用每个断言中最长的间隔时间)
|
523
570
|
retry_interval = retry_config['interval']
|
524
571
|
for assertion in retryable_assertions:
|
525
|
-
retry_interval = max(retry_interval,
|
572
|
+
retry_interval = max(retry_interval,
|
573
|
+
assertion.get('retry_interval', 1.0))
|
526
574
|
|
527
575
|
allure.attach(
|
528
576
|
f"重试 {len(retryable_assertions)} 个断言\n"
|
@@ -552,14 +600,21 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
|
|
552
600
|
# 只重试那些仍在重试范围内的断言
|
553
601
|
try:
|
554
602
|
# 从原始断言配置中提取出需要重试的断言
|
555
|
-
retry_assertion_indexes = [
|
556
|
-
|
603
|
+
retry_assertion_indexes = [
|
604
|
+
a['index'] for a in still_retryable_assertions]
|
605
|
+
retry_assertions = [
|
606
|
+
http_req.config.get('asserts', [])[idx]
|
607
|
+
for idx in retry_assertion_indexes]
|
557
608
|
|
558
609
|
# 创建索引映射:新索引 -> 原始索引
|
559
|
-
index_mapping = {
|
610
|
+
index_mapping = {
|
611
|
+
new_idx: orig_idx for new_idx, orig_idx in
|
612
|
+
enumerate(retry_assertion_indexes)}
|
560
613
|
|
561
614
|
# 只处理需要重试的断言,传递索引映射
|
562
|
-
results, new_failed_assertions = http_req.process_asserts(
|
615
|
+
results, new_failed_assertions = http_req.process_asserts(
|
616
|
+
specific_asserts=retry_assertions,
|
617
|
+
index_mapping=index_mapping)
|
563
618
|
|
564
619
|
# 如果所有断言都通过了,检查全部断言
|
565
620
|
if not new_failed_assertions:
|
@@ -607,7 +662,8 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
|
|
607
662
|
except AssertionError as final_err:
|
608
663
|
# 重新格式化错误消息,添加重试信息
|
609
664
|
enhanced_error = (
|
610
|
-
f"断言验证失败 (已重试 {max_retry_count} 次):\n\n
|
665
|
+
f"断言验证失败 (已重试 {max_retry_count} 次):\n\n"
|
666
|
+
f"{str(final_err)}"
|
611
667
|
)
|
612
668
|
allure.attach(
|
613
669
|
enhanced_error,
|
@@ -617,4 +673,4 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
|
|
617
673
|
raise AssertionError(enhanced_error) from final_err
|
618
674
|
|
619
675
|
|
620
|
-
# 注意:旧的重试函数已被移除,现在使用统一的重试机制
|
676
|
+
# 注意:旧的重试函数已被移除,现在使用统一的重试机制
|
@@ -7,16 +7,19 @@ from pytest_dsl.core.keyword_manager import keyword_manager, Parameter
|
|
7
7
|
# 配置日志
|
8
8
|
logger = logging.getLogger(__name__)
|
9
9
|
|
10
|
+
|
10
11
|
class RemoteKeywordClient:
|
11
12
|
"""远程关键字客户端,用于连接远程关键字服务器并执行关键字"""
|
12
13
|
|
13
|
-
def __init__(self, url='http://localhost:8270/', api_key=None, alias=None,
|
14
|
+
def __init__(self, url='http://localhost:8270/', api_key=None, alias=None,
|
15
|
+
sync_config=None):
|
14
16
|
self.url = url
|
15
17
|
self.server = xmlrpc.client.ServerProxy(url, allow_none=True)
|
16
18
|
self.keyword_cache = {}
|
17
19
|
self.param_mappings = {} # 存储每个关键字的参数映射
|
18
20
|
self.api_key = api_key
|
19
|
-
self.alias = alias or url.replace('http://', '').replace(
|
21
|
+
self.alias = alias or url.replace('http://', '').replace(
|
22
|
+
'https://', '').split(':')[0]
|
20
23
|
|
21
24
|
# 变量传递配置(简化版)
|
22
25
|
self.sync_config = sync_config or {
|
@@ -24,7 +27,6 @@ class RemoteKeywordClient:
|
|
24
27
|
'sync_yaml_vars': True, # 连接时传递YAML配置变量
|
25
28
|
'yaml_sync_keys': None, # 指定要同步的YAML键列表,None表示同步所有(除了排除的)
|
26
29
|
'yaml_exclude_patterns': [ # 排除包含这些模式的YAML变量
|
27
|
-
'password', 'secret', 'key', 'token', 'credential', 'auth',
|
28
30
|
'private', 'remote_servers' # 排除远程服务器配置避免循环
|
29
31
|
]
|
30
32
|
}
|
@@ -42,7 +44,8 @@ class RemoteKeywordClient:
|
|
42
44
|
self._send_initial_variables()
|
43
45
|
|
44
46
|
logger.info(f"已连接到远程关键字服务器: {self.url}, 别名: {self.alias}")
|
45
|
-
print(f"RemoteKeywordClient: 成功连接到远程服务器 {self.url},
|
47
|
+
print(f"RemoteKeywordClient: 成功连接到远程服务器 {self.url}, "
|
48
|
+
f"别名: {self.alias}")
|
46
49
|
return True
|
47
50
|
except Exception as e:
|
48
51
|
error_msg = f"连接远程关键字服务器失败: {str(e)}"
|
@@ -81,7 +84,8 @@ class RemoteKeywordClient:
|
|
81
84
|
for param_detail in param_details:
|
82
85
|
param_name = param_detail['name']
|
83
86
|
param_mapping_name = param_detail.get('mapping', param_name)
|
84
|
-
param_desc = param_detail.get('description',
|
87
|
+
param_desc = param_detail.get('description',
|
88
|
+
f'远程关键字参数: {param_name}')
|
85
89
|
param_default = param_detail.get('default')
|
86
90
|
|
87
91
|
# 确保参数名称正确映射
|
@@ -114,7 +118,8 @@ class RemoteKeywordClient:
|
|
114
118
|
'func': remote_func,
|
115
119
|
'mapping': {p['name']: p['mapping'] for p in parameters},
|
116
120
|
'parameters': [Parameter(**p) for p in parameters],
|
117
|
-
'defaults': {p['mapping']: p['default'] for p in parameters
|
121
|
+
'defaults': {p['mapping']: p['default'] for p in parameters
|
122
|
+
if p['default'] is not None}, # 添加默认值支持
|
118
123
|
'remote': True, # 标记为远程关键字
|
119
124
|
'alias': self.alias,
|
120
125
|
'original_name': name
|
@@ -161,7 +166,7 @@ class RemoteKeywordClient:
|
|
161
166
|
else:
|
162
167
|
# 如果没有任何映射,使用原始参数名
|
163
168
|
param_mapping = None
|
164
|
-
print(
|
169
|
+
print("没有找到参数映射,使用原始参数名")
|
165
170
|
|
166
171
|
# 映射参数名称
|
167
172
|
mapped_kwargs = {}
|
@@ -209,10 +214,11 @@ class RemoteKeywordClient:
|
|
209
214
|
|
210
215
|
# 处理响应数据
|
211
216
|
if 'response' in return_data and return_data['response']:
|
212
|
-
print(
|
217
|
+
print("远程关键字响应数据: 已接收")
|
213
218
|
|
214
219
|
# 检查是否为新的统一返回格式(包含captures等字段)
|
215
|
-
if 'captures' in return_data or 'session_state' in return_data or
|
220
|
+
if ('captures' in return_data or 'session_state' in return_data or
|
221
|
+
'metadata' in return_data):
|
216
222
|
# 返回完整的新格式,让DSL执行器处理变量捕获
|
217
223
|
return return_data
|
218
224
|
elif 'result' in return_data:
|
@@ -243,11 +249,13 @@ class RemoteKeywordClient:
|
|
243
249
|
if variables_to_send:
|
244
250
|
try:
|
245
251
|
# 调用远程服务器的变量接收接口
|
246
|
-
result = self.server.sync_variables_from_client(
|
252
|
+
result = self.server.sync_variables_from_client(
|
253
|
+
variables_to_send, self.api_key)
|
247
254
|
if result.get('status') == 'success':
|
248
255
|
print(f"成功传递 {len(variables_to_send)} 个变量到远程服务器")
|
249
256
|
else:
|
250
|
-
print(f"传递变量到远程服务器失败:
|
257
|
+
print(f"传递变量到远程服务器失败: "
|
258
|
+
f"{result.get('error', '未知错误')}")
|
251
259
|
except Exception as e:
|
252
260
|
print(f"调用远程变量接口失败: {str(e)}")
|
253
261
|
else:
|
@@ -295,10 +303,12 @@ class RemoteKeywordClient:
|
|
295
303
|
# 获取所有YAML变量
|
296
304
|
yaml_data = yaml_vars._variables
|
297
305
|
if yaml_data:
|
306
|
+
print(f"客户端YAML变量总数: {len(yaml_data)}")
|
307
|
+
|
298
308
|
# 检查同步配置中是否指定了特定的键
|
299
309
|
sync_keys = self.sync_config.get('yaml_sync_keys', None)
|
300
310
|
exclude_patterns = self.sync_config.get('yaml_exclude_patterns', [
|
301
|
-
'password', 'secret', '
|
311
|
+
'password', 'secret', 'token', 'credential', 'auth',
|
302
312
|
'private', 'remote_servers' # 排除远程服务器配置避免循环
|
303
313
|
])
|
304
314
|
|
@@ -307,6 +317,7 @@ class RemoteKeywordClient:
|
|
307
317
|
for key in sync_keys:
|
308
318
|
if key in yaml_data:
|
309
319
|
variables[key] = yaml_data[key]
|
320
|
+
print(f"传递指定YAML变量: {key}")
|
310
321
|
else:
|
311
322
|
# 传递所有YAML变量,但排除敏感信息
|
312
323
|
for key, value in yaml_data.items():
|
@@ -323,7 +334,8 @@ class RemoteKeywordClient:
|
|
323
334
|
if not should_exclude and isinstance(value, str):
|
324
335
|
value_lower = value.lower()
|
325
336
|
for pattern in exclude_patterns:
|
326
|
-
if pattern.lower() in value_lower and
|
337
|
+
if (pattern.lower() in value_lower and
|
338
|
+
len(value) < 100): # 只检查短字符串
|
327
339
|
should_exclude = True
|
328
340
|
break
|
329
341
|
|
@@ -336,11 +348,11 @@ class RemoteKeywordClient:
|
|
336
348
|
|
337
349
|
except Exception as e:
|
338
350
|
logger.warning(f"收集YAML变量失败: {str(e)}")
|
351
|
+
print(f"收集YAML变量失败: {str(e)}")
|
339
352
|
|
340
353
|
return variables
|
341
354
|
|
342
355
|
|
343
|
-
|
344
356
|
# 远程关键字客户端管理器
|
345
357
|
class RemoteKeywordManager:
|
346
358
|
"""远程关键字客户端管理器,管理多个远程服务器连接"""
|
@@ -348,7 +360,8 @@ class RemoteKeywordManager:
|
|
348
360
|
def __init__(self):
|
349
361
|
self.clients = {} # 别名 -> 客户端实例
|
350
362
|
|
351
|
-
def register_remote_server(self, url, alias, api_key=None,
|
363
|
+
def register_remote_server(self, url, alias, api_key=None,
|
364
|
+
sync_config=None):
|
352
365
|
"""注册远程关键字服务器
|
353
366
|
|
354
367
|
Args:
|
@@ -361,7 +374,8 @@ class RemoteKeywordManager:
|
|
361
374
|
bool: 是否成功连接
|
362
375
|
"""
|
363
376
|
print(f"RemoteKeywordManager: 正在注册远程服务器 {url} 别名 {alias}")
|
364
|
-
client = RemoteKeywordClient(url=url, api_key=api_key, alias=alias,
|
377
|
+
client = RemoteKeywordClient(url=url, api_key=api_key, alias=alias,
|
378
|
+
sync_config=sync_config)
|
365
379
|
success = client.connect()
|
366
380
|
|
367
381
|
if success:
|
@@ -394,6 +408,5 @@ class RemoteKeywordManager:
|
|
394
408
|
return client._execute_remote_keyword(name=keyword_name, **kwargs)
|
395
409
|
|
396
410
|
|
397
|
-
|
398
411
|
# 创建全局远程关键字管理器实例
|
399
412
|
remote_keyword_manager = RemoteKeywordManager()
|
@@ -403,9 +403,26 @@ class RemoteKeywordServer:
|
|
403
403
|
self.shared_variables[name] = value
|
404
404
|
print(f"接收到客户端变量: {name}")
|
405
405
|
|
406
|
+
# 将所有同步的变量直接注入到yaml_vars中,实现无缝访问
|
407
|
+
from pytest_dsl.core.yaml_vars import yaml_vars
|
408
|
+
|
409
|
+
for name, value in variables.items():
|
410
|
+
# 直接设置到yaml_vars中,确保所有关键字都能无缝访问
|
411
|
+
yaml_vars._variables[name] = value
|
412
|
+
print(f"✓ 变量 {name} 已注入到yaml_vars,实现无缝访问")
|
413
|
+
|
414
|
+
# 同时处理全局变量到global_context
|
415
|
+
from pytest_dsl.core.global_context import global_context
|
416
|
+
for name, value in variables.items():
|
417
|
+
if name.startswith('g_'):
|
418
|
+
global_context.set_variable(name, value)
|
419
|
+
print(f"✓ 全局变量 {name} 已注入到global_context")
|
420
|
+
|
421
|
+
print(f"✅ 总共同步了 {len(variables)} 个变量,全部实现无缝访问")
|
422
|
+
|
406
423
|
return {
|
407
424
|
'status': 'success',
|
408
|
-
'message': f'成功同步 {len(variables)}
|
425
|
+
'message': f'成功同步 {len(variables)} 个变量,全部实现无缝访问'
|
409
426
|
}
|
410
427
|
except Exception as e:
|
411
428
|
return {
|
@@ -6,7 +6,8 @@
|
|
6
6
|
|
7
7
|
import logging
|
8
8
|
from typing import Any, Optional
|
9
|
-
from pytest_dsl.remote.hook_manager import register_startup_hook,
|
9
|
+
from pytest_dsl.remote.hook_manager import (register_startup_hook,
|
10
|
+
register_before_keyword_hook)
|
10
11
|
from pytest_dsl.core.yaml_vars import yaml_vars
|
11
12
|
from pytest_dsl.core.global_context import global_context
|
12
13
|
|
@@ -43,24 +44,28 @@ class VariableBridge:
|
|
43
44
|
|
44
45
|
self._bridge_installed = True
|
45
46
|
logger.info("变量桥接机制已安装")
|
47
|
+
print(f"🔗 变量桥接机制已安装,可桥接 {len(shared_variables)} 个同步变量")
|
46
48
|
|
47
49
|
def _bridged_yaml_get_variable(self, name: str) -> Optional[Any]:
|
48
50
|
"""桥接的YAML变量获取方法
|
49
51
|
|
50
52
|
优先级:
|
51
|
-
1. 原始YAML
|
53
|
+
1. 原始YAML变量(服务器本地的)
|
52
54
|
2. 客户端同步的变量
|
53
55
|
"""
|
54
56
|
# 首先尝试从原始YAML变量获取
|
55
57
|
original_value = self.original_yaml_get_variable(name)
|
56
58
|
if original_value is not None:
|
59
|
+
logger.debug(f"从原始YAML获取变量: {name}")
|
57
60
|
return original_value
|
58
61
|
|
59
62
|
# 如果原始YAML中没有,尝试从同步变量获取
|
60
63
|
if name in self.shared_variables:
|
61
64
|
logger.debug(f"从同步变量获取YAML变量: {name}")
|
65
|
+
print(f"🔗 变量桥接: 从同步变量获取 {name}")
|
62
66
|
return self.shared_variables[name]
|
63
67
|
|
68
|
+
logger.debug(f"变量 {name} 在原始YAML和同步变量中都不存在")
|
64
69
|
return None
|
65
70
|
|
66
71
|
def _bridged_global_get_variable(self, name: str) -> Any:
|
@@ -74,17 +79,20 @@ class VariableBridge:
|
|
74
79
|
# 首先尝试从原始全局上下文获取
|
75
80
|
original_value = self.original_global_get_variable(name)
|
76
81
|
if original_value is not None:
|
82
|
+
logger.debug(f"从原始全局上下文获取变量: {name}")
|
77
83
|
return original_value
|
78
|
-
except:
|
84
|
+
except Exception as e:
|
79
85
|
# 如果原始方法抛出异常,继续尝试同步变量
|
80
|
-
|
86
|
+
logger.debug(f"原始全局上下文获取变量 {name} 失败,尝试同步变量: {e}")
|
81
87
|
|
82
88
|
# 如果原始全局变量中没有,尝试从同步变量获取
|
83
89
|
if name in self.shared_variables:
|
84
90
|
logger.debug(f"从同步变量获取全局变量: {name}")
|
91
|
+
print(f"🔗 变量桥接: 从同步变量获取全局变量 {name}")
|
85
92
|
return self.shared_variables[name]
|
86
93
|
|
87
94
|
# 如果都没有找到,返回None(保持原有行为)
|
95
|
+
logger.debug(f"变量 {name} 在所有来源中都不存在")
|
88
96
|
return None
|
89
97
|
|
90
98
|
def uninstall_bridge(self):
|
@@ -113,6 +121,7 @@ def setup_variable_bridge(context):
|
|
113
121
|
if shared_variables is not None:
|
114
122
|
variable_bridge.install_bridge(shared_variables)
|
115
123
|
logger.info("变量桥接机制已在服务器启动时安装")
|
124
|
+
print(f"🔗 服务器启动时安装变量桥接机制,可桥接 {len(shared_variables)} 个变量")
|
116
125
|
else:
|
117
126
|
logger.warning("无法获取shared_variables,变量桥接机制安装失败")
|
118
127
|
|
@@ -124,11 +133,14 @@ def ensure_variable_bridge(context):
|
|
124
133
|
shared_variables = context.get('shared_variables')
|
125
134
|
keyword_name = context.get('keyword_name')
|
126
135
|
|
127
|
-
#
|
128
|
-
if
|
136
|
+
# 对所有关键字进行调试日志(如果有同步变量)
|
137
|
+
if shared_variables and len(shared_variables) > 0:
|
129
138
|
synced_count = len(shared_variables)
|
130
|
-
|
131
|
-
|
139
|
+
logger.debug(f"关键字 {keyword_name} 执行前,可用同步变量数量: {synced_count}")
|
140
|
+
|
141
|
+
# 对重要关键字显示详细信息
|
142
|
+
if keyword_name in ['HTTP请求', '数据库查询', 'API调用']:
|
143
|
+
print(f"🔗 关键字 {keyword_name} 可访问 {synced_count} 个同步变量")
|
132
144
|
|
133
145
|
|
134
146
|
def get_synced_variable(name: str) -> Optional[Any]:
|
@@ -162,3 +174,24 @@ def has_synced_variable(name: str) -> bool:
|
|
162
174
|
是否存在该同步变量
|
163
175
|
"""
|
164
176
|
return name in variable_bridge.shared_variables
|
177
|
+
|
178
|
+
|
179
|
+
def get_all_accessible_variables() -> dict:
|
180
|
+
"""获取所有可访问的变量(包括原始变量和同步变量)
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
所有可访问变量的字典
|
184
|
+
"""
|
185
|
+
all_vars = {}
|
186
|
+
|
187
|
+
# 添加原始YAML变量
|
188
|
+
try:
|
189
|
+
if hasattr(yaml_vars, '_variables'):
|
190
|
+
all_vars.update(yaml_vars._variables)
|
191
|
+
except Exception as e:
|
192
|
+
logger.warning(f"获取原始YAML变量失败: {e}")
|
193
|
+
|
194
|
+
# 添加同步变量(会覆盖同名的原始变量)
|
195
|
+
all_vars.update(variable_bridge.shared_variables)
|
196
|
+
|
197
|
+
return all_vars
|
@@ -12,7 +12,7 @@ pytest_dsl/core/custom_keyword_manager.py,sha256=UdlGUc_mT8hIwmU7LVf4wJLG-geChwY
|
|
12
12
|
pytest_dsl/core/dsl_executor.py,sha256=aEEfocFCFxNDN1puBFhQzL5fzw9eZx8BAyYJI5XSt4Y,30472
|
13
13
|
pytest_dsl/core/dsl_executor_utils.py,sha256=cFoR2p3qQ2pb-UhkoefleK-zbuFqf0aBLh2Rlp8Ebs4,2180
|
14
14
|
pytest_dsl/core/global_context.py,sha256=NcEcS2V61MT70tgAsGsFWQq0P3mKjtHQr1rgT3yTcyY,3535
|
15
|
-
pytest_dsl/core/http_client.py,sha256=
|
15
|
+
pytest_dsl/core/http_client.py,sha256=Ho1fPl23kYZzDswwAR-YCpgL5c6Oy1bWh_ieDOlgY4s,15808
|
16
16
|
pytest_dsl/core/http_request.py,sha256=nGMlx0mFc7rDLIdp9GJ3e09OQH3ryUA56yJdRZ99dOQ,57445
|
17
17
|
pytest_dsl/core/keyword_manager.py,sha256=hoNXHQcumnufPRUobnY0mnku4CHxSg2amwPFby4gQEs,7643
|
18
18
|
pytest_dsl/core/lexer.py,sha256=WaLzt9IhtHiA90Fg2WGgfVztveCUhtgxzANBaEiy-F8,4347
|
@@ -57,17 +57,17 @@ pytest_dsl/examples/quickstart/loops.auto,sha256=ZNZ6qP636v8QMY8QRyTUBB43gWCsqHb
|
|
57
57
|
pytest_dsl/keywords/__init__.py,sha256=5aiyPU_t1UiB2MEZ6M9ffOKnV1mFT_2YHxnZvyWaBNI,372
|
58
58
|
pytest_dsl/keywords/assertion_keywords.py,sha256=obW06H_3AizsvEM_9VE2JVuwvgrNVqP1kUTDd3U1SMk,23240
|
59
59
|
pytest_dsl/keywords/global_keywords.py,sha256=4yw5yeXoGf_4W26F39EA2Pp-mH9GiKGy2jKgFO9a_wM,2509
|
60
|
-
pytest_dsl/keywords/http_keywords.py,sha256=
|
60
|
+
pytest_dsl/keywords/http_keywords.py,sha256=z2Brqj1Mkufa_PlZZNwfKuYqkgJhnjsZw-7YNnbt2N4,27302
|
61
61
|
pytest_dsl/keywords/system_keywords.py,sha256=e65Mzyt56FYmw0vHegajLUSLeEYVI9Y-WSB1h6SKKLo,12089
|
62
62
|
pytest_dsl/remote/__init__.py,sha256=syRSxTlTUfdAPleJnVS4MykRyEN8_SKiqlsn6SlIK8k,120
|
63
63
|
pytest_dsl/remote/hook_manager.py,sha256=0hwRKP8yhcnfAnrrnZGVT-S0TBgo6c0A4qO5XRpvV1U,4899
|
64
|
-
pytest_dsl/remote/keyword_client.py,sha256=
|
65
|
-
pytest_dsl/remote/keyword_server.py,sha256=
|
66
|
-
pytest_dsl/remote/variable_bridge.py,sha256=
|
64
|
+
pytest_dsl/remote/keyword_client.py,sha256=BL80MOaLroUi0v-9sLtkJ55g1R0Iw9SE1k11Ifwqx-I,17292
|
65
|
+
pytest_dsl/remote/keyword_server.py,sha256=vGIE3Bhh461xX_u1U-Cf5nrWL2GQFYdtQdcMWfFIYgE,22320
|
66
|
+
pytest_dsl/remote/variable_bridge.py,sha256=dv-d3Gq9ttvvrXM1fdlLtoSOPB6vRp0_GBOwX4wvcy8,7121
|
67
67
|
pytest_dsl/templates/keywords_report.html,sha256=7x84iq6hi08nf1iQ95jZ3izcAUPx6JFm0_8xS85CYws,31241
|
68
|
-
pytest_dsl-0.11.
|
69
|
-
pytest_dsl-0.11.
|
70
|
-
pytest_dsl-0.11.
|
71
|
-
pytest_dsl-0.11.
|
72
|
-
pytest_dsl-0.11.
|
73
|
-
pytest_dsl-0.11.
|
68
|
+
pytest_dsl-0.11.1.dist-info/licenses/LICENSE,sha256=Rguy8cb9sYhK6cmrBdXvwh94rKVDh2tVZEWptsHIsVM,1071
|
69
|
+
pytest_dsl-0.11.1.dist-info/METADATA,sha256=5YIMx6SIIlzVFeeuPUhRqi0eISBdDIjpamHlptPjpIY,29642
|
70
|
+
pytest_dsl-0.11.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
71
|
+
pytest_dsl-0.11.1.dist-info/entry_points.txt,sha256=PLOBbH02OGY1XR1JDKIZB1Em87loUvbgMRWaag-5FhY,204
|
72
|
+
pytest_dsl-0.11.1.dist-info/top_level.txt,sha256=4CrSx4uNqxj7NvK6k1y2JZrSrJSzi-UvPZdqpUhumWM,11
|
73
|
+
pytest_dsl-0.11.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|