pytest-dsl 0.10.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/cli.py +533 -89
- pytest_dsl/core/custom_keyword_manager.py +1 -4
- pytest_dsl/core/http_client.py +2 -2
- pytest_dsl/core/keyword_manager.py +77 -3
- pytest_dsl/core/plugin_discovery.py +38 -1
- 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/templates/keywords_report.html +862 -0
- {pytest_dsl-0.10.0.dist-info → pytest_dsl-0.11.1.dist-info}/METADATA +2 -1
- {pytest_dsl-0.10.0.dist-info → pytest_dsl-0.11.1.dist-info}/RECORD +16 -15
- {pytest_dsl-0.10.0.dist-info → pytest_dsl-0.11.1.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.10.0.dist-info → pytest_dsl-0.11.1.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.10.0.dist-info → pytest_dsl-0.11.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.10.0.dist-info → pytest_dsl-0.11.1.dist-info}/top_level.txt +0 -0
pytest_dsl/cli.py
CHANGED
@@ -63,9 +63,9 @@ def parse_args():
|
|
63
63
|
help='罗列所有可用关键字和参数信息'
|
64
64
|
)
|
65
65
|
list_parser.add_argument(
|
66
|
-
'--format', choices=['text', 'json'],
|
66
|
+
'--format', choices=['text', 'json', 'html'],
|
67
67
|
default='json',
|
68
|
-
help='输出格式:json(默认) 或
|
68
|
+
help='输出格式:json(默认)、text 或 html'
|
69
69
|
)
|
70
70
|
list_parser.add_argument(
|
71
71
|
'--output', '-o', type=str, default=None,
|
@@ -77,10 +77,14 @@ def parse_args():
|
|
77
77
|
)
|
78
78
|
list_parser.add_argument(
|
79
79
|
'--category',
|
80
|
-
choices=['builtin', 'custom', 'remote', 'all'],
|
80
|
+
choices=['builtin', 'plugin', 'custom', 'project_custom', 'remote', 'all'],
|
81
81
|
default='all',
|
82
|
-
help='关键字类别:builtin(内置)、custom(自定义)、'
|
83
|
-
'remote(远程)、all(全部,默认)'
|
82
|
+
help='关键字类别:builtin(内置)、plugin(插件)、custom(自定义)、'
|
83
|
+
'project_custom(项目自定义)、remote(远程)、all(全部,默认)'
|
84
|
+
)
|
85
|
+
list_parser.add_argument(
|
86
|
+
'--include-remote', action='store_true',
|
87
|
+
help='是否包含远程关键字(默认不包含)'
|
84
88
|
)
|
85
89
|
|
86
90
|
return parser.parse_args(argv)
|
@@ -92,7 +96,7 @@ def parse_args():
|
|
92
96
|
if '--list-keywords' in argv:
|
93
97
|
parser.add_argument('--list-keywords', action='store_true')
|
94
98
|
parser.add_argument(
|
95
|
-
'--format', choices=['text', 'json'], default='json'
|
99
|
+
'--format', choices=['text', 'json', 'html'], default='json'
|
96
100
|
)
|
97
101
|
parser.add_argument(
|
98
102
|
'--output', '-o', type=str, default=None
|
@@ -100,9 +104,14 @@ def parse_args():
|
|
100
104
|
parser.add_argument('--filter', type=str, default=None)
|
101
105
|
parser.add_argument(
|
102
106
|
'--category',
|
103
|
-
choices=[
|
107
|
+
choices=[
|
108
|
+
'builtin', 'plugin', 'custom', 'project_custom', 'remote', 'all'
|
109
|
+
],
|
104
110
|
default='all'
|
105
111
|
)
|
112
|
+
parser.add_argument(
|
113
|
+
'--include-remote', action='store_true'
|
114
|
+
)
|
106
115
|
parser.add_argument('path', nargs='?') # 可选的路径参数
|
107
116
|
parser.add_argument(
|
108
117
|
'--yaml-vars', action='append', default=[]
|
@@ -125,8 +134,12 @@ def parse_args():
|
|
125
134
|
return args
|
126
135
|
|
127
136
|
|
128
|
-
def load_all_keywords():
|
129
|
-
"""加载所有可用的关键字
|
137
|
+
def load_all_keywords(include_remote=False):
|
138
|
+
"""加载所有可用的关键字
|
139
|
+
|
140
|
+
Args:
|
141
|
+
include_remote: 是否包含远程关键字,默认为False
|
142
|
+
"""
|
130
143
|
# 首先导入内置关键字模块,确保内置关键字被注册
|
131
144
|
try:
|
132
145
|
import pytest_dsl.keywords # noqa: F401
|
@@ -139,12 +152,61 @@ def load_all_keywords():
|
|
139
152
|
|
140
153
|
# 扫描本地关键字
|
141
154
|
scan_local_keywords()
|
155
|
+
|
156
|
+
# 扫描项目中的自定义关键字(.resource文件中定义的)
|
157
|
+
project_custom_keywords = scan_project_custom_keywords()
|
158
|
+
if project_custom_keywords:
|
159
|
+
print(f"发现 {len(project_custom_keywords)} 个项目自定义关键字")
|
160
|
+
|
161
|
+
# 加载.resource文件中的关键字到关键字管理器
|
162
|
+
from pytest_dsl.core.custom_keyword_manager import (
|
163
|
+
custom_keyword_manager
|
164
|
+
)
|
165
|
+
from pathlib import Path
|
166
|
+
|
167
|
+
project_root = Path(os.getcwd())
|
168
|
+
resource_files = list(project_root.glob('**/*.resource'))
|
169
|
+
|
170
|
+
for resource_file in resource_files:
|
171
|
+
try:
|
172
|
+
custom_keyword_manager.load_resource_file(str(resource_file))
|
173
|
+
print(f"已加载资源文件: {resource_file}")
|
174
|
+
except Exception as e:
|
175
|
+
print(f"加载资源文件失败 {resource_file}: {e}")
|
176
|
+
|
177
|
+
# 根据参数决定是否加载远程关键字
|
178
|
+
if include_remote:
|
179
|
+
print("正在扫描远程关键字...")
|
180
|
+
# 这里可以添加远程关键字的扫描逻辑
|
181
|
+
# 目前远程关键字是通过DSL文件中的@remote导入指令动态加载的
|
182
|
+
else:
|
183
|
+
print("跳过远程关键字扫描")
|
184
|
+
|
185
|
+
return project_custom_keywords
|
142
186
|
|
143
187
|
|
144
|
-
def categorize_keyword(keyword_name, keyword_info
|
188
|
+
def categorize_keyword(keyword_name, keyword_info,
|
189
|
+
project_custom_keywords=None):
|
145
190
|
"""判断关键字的类别"""
|
191
|
+
# 优先使用存储的来源信息
|
192
|
+
source_type = keyword_info.get('source_type')
|
193
|
+
if source_type:
|
194
|
+
if source_type == 'builtin':
|
195
|
+
return 'builtin'
|
196
|
+
elif source_type == 'plugin':
|
197
|
+
return 'plugin'
|
198
|
+
elif source_type in ['external', 'local']:
|
199
|
+
return 'custom'
|
200
|
+
elif source_type == 'project_custom':
|
201
|
+
return 'project_custom'
|
202
|
+
|
203
|
+
# 向后兼容:使用原有的判断逻辑
|
146
204
|
if keyword_info.get('remote', False):
|
147
205
|
return 'remote'
|
206
|
+
|
207
|
+
# 检查是否是项目自定义关键字(DSL文件中定义的)
|
208
|
+
if project_custom_keywords and keyword_name in project_custom_keywords:
|
209
|
+
return 'project_custom'
|
148
210
|
|
149
211
|
# 检查是否是内置关键字(通过检查函数所在模块)
|
150
212
|
func = keyword_info.get('func')
|
@@ -156,13 +218,74 @@ def categorize_keyword(keyword_name, keyword_info):
|
|
156
218
|
return 'custom'
|
157
219
|
|
158
220
|
|
159
|
-
def
|
221
|
+
def get_keyword_source_info(keyword_info):
|
222
|
+
"""获取关键字的详细来源信息"""
|
223
|
+
source_type = keyword_info.get('source_type', 'unknown')
|
224
|
+
source_name = keyword_info.get('source_name', '未知')
|
225
|
+
|
226
|
+
return {
|
227
|
+
'type': source_type,
|
228
|
+
'name': source_name,
|
229
|
+
'display_name': source_name,
|
230
|
+
'module': keyword_info.get('module_name', ''),
|
231
|
+
'plugin_module': keyword_info.get('plugin_module', '')
|
232
|
+
}
|
233
|
+
|
234
|
+
|
235
|
+
def group_keywords_by_source(keywords_dict, project_custom_keywords=None):
|
236
|
+
"""按来源分组关键字
|
237
|
+
|
238
|
+
Returns:
|
239
|
+
dict: 格式为 {source_group: {source_name: [keywords]}}
|
240
|
+
"""
|
241
|
+
groups = {
|
242
|
+
'builtin': {},
|
243
|
+
'plugin': {},
|
244
|
+
'custom': {},
|
245
|
+
'project_custom': {},
|
246
|
+
'remote': {}
|
247
|
+
}
|
248
|
+
|
249
|
+
for keyword_name, keyword_info in keywords_dict.items():
|
250
|
+
category = categorize_keyword(
|
251
|
+
keyword_name, keyword_info, project_custom_keywords
|
252
|
+
)
|
253
|
+
source_info = get_keyword_source_info(keyword_info)
|
254
|
+
|
255
|
+
# 特殊处理项目自定义关键字
|
256
|
+
if category == 'project_custom' and project_custom_keywords:
|
257
|
+
custom_info = project_custom_keywords[keyword_name]
|
258
|
+
source_name = custom_info['file']
|
259
|
+
else:
|
260
|
+
source_name = source_info['name']
|
261
|
+
|
262
|
+
if source_name not in groups[category]:
|
263
|
+
groups[category][source_name] = []
|
264
|
+
|
265
|
+
groups[category][source_name].append({
|
266
|
+
'name': keyword_name,
|
267
|
+
'info': keyword_info,
|
268
|
+
'source_info': source_info
|
269
|
+
})
|
270
|
+
|
271
|
+
return groups
|
272
|
+
|
273
|
+
|
274
|
+
def format_keyword_info_text(keyword_name, keyword_info, show_category=True,
|
275
|
+
project_custom_keywords=None):
|
160
276
|
"""格式化关键字信息为文本格式"""
|
161
277
|
lines = []
|
162
278
|
|
163
279
|
# 关键字名称和类别
|
164
|
-
category = categorize_keyword(
|
165
|
-
|
280
|
+
category = categorize_keyword(
|
281
|
+
keyword_name, keyword_info, project_custom_keywords
|
282
|
+
)
|
283
|
+
category_names = {
|
284
|
+
'builtin': '内置',
|
285
|
+
'custom': '自定义',
|
286
|
+
'project_custom': '项目自定义',
|
287
|
+
'remote': '远程'
|
288
|
+
}
|
166
289
|
|
167
290
|
if show_category:
|
168
291
|
category_display = category_names.get(category, '未知')
|
@@ -177,32 +300,64 @@ def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
|
|
177
300
|
lines.append(f" 远程服务器: {alias}")
|
178
301
|
lines.append(f" 原始名称: {original_name}")
|
179
302
|
|
180
|
-
#
|
181
|
-
|
182
|
-
|
183
|
-
lines.append("
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
param_info.
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
303
|
+
# 项目自定义关键字特殊标识
|
304
|
+
if category == 'project_custom' and project_custom_keywords:
|
305
|
+
custom_info = project_custom_keywords[keyword_name]
|
306
|
+
lines.append(f" 文件位置: {custom_info['file']}")
|
307
|
+
|
308
|
+
# 对于项目自定义关键字,使用从AST中提取的参数信息
|
309
|
+
custom_parameters = custom_info.get('parameters', [])
|
310
|
+
if custom_parameters:
|
311
|
+
lines.append(" 参数:")
|
312
|
+
for param_info in custom_parameters:
|
313
|
+
param_name = param_info['name']
|
314
|
+
param_mapping = param_info.get('mapping', '')
|
315
|
+
param_desc = param_info.get('description', '')
|
316
|
+
param_default = param_info.get('default', None)
|
317
|
+
|
318
|
+
# 构建参数描述
|
319
|
+
param_parts = []
|
320
|
+
if param_mapping and param_mapping != param_name:
|
321
|
+
param_parts.append(f"{param_name} ({param_mapping})")
|
322
|
+
else:
|
323
|
+
param_parts.append(param_name)
|
324
|
+
|
325
|
+
param_parts.append(f": {param_desc}")
|
326
|
+
|
327
|
+
# 添加默认值信息
|
328
|
+
if param_default is not None:
|
329
|
+
param_parts.append(f" (默认值: {param_default})")
|
330
|
+
|
331
|
+
lines.append(f" {''.join(param_parts)}")
|
332
|
+
else:
|
333
|
+
lines.append(" 参数: 无")
|
204
334
|
else:
|
205
|
-
|
335
|
+
# 参数信息(对于其他类型的关键字)
|
336
|
+
parameters = keyword_info.get('parameters', [])
|
337
|
+
if parameters:
|
338
|
+
lines.append(" 参数:")
|
339
|
+
for param in parameters:
|
340
|
+
param_name = getattr(param, 'name', str(param))
|
341
|
+
param_mapping = getattr(param, 'mapping', '')
|
342
|
+
param_desc = getattr(param, 'description', '')
|
343
|
+
param_default = getattr(param, 'default', None)
|
344
|
+
|
345
|
+
# 构建参数描述
|
346
|
+
param_info = []
|
347
|
+
if param_mapping and param_mapping != param_name:
|
348
|
+
param_info.append(f"{param_name} ({param_mapping})")
|
349
|
+
else:
|
350
|
+
param_info.append(param_name)
|
351
|
+
|
352
|
+
param_info.append(f": {param_desc}")
|
353
|
+
|
354
|
+
# 添加默认值信息
|
355
|
+
if param_default is not None:
|
356
|
+
param_info.append(f" (默认值: {param_default})")
|
357
|
+
|
358
|
+
lines.append(f" {''.join(param_info)}")
|
359
|
+
else:
|
360
|
+
lines.append(" 参数: 无")
|
206
361
|
|
207
362
|
# 函数文档
|
208
363
|
func = keyword_info.get('func')
|
@@ -212,13 +367,18 @@ def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
|
|
212
367
|
return '\n'.join(lines)
|
213
368
|
|
214
369
|
|
215
|
-
def format_keyword_info_json(keyword_name, keyword_info
|
370
|
+
def format_keyword_info_json(keyword_name, keyword_info,
|
371
|
+
project_custom_keywords=None):
|
216
372
|
"""格式化关键字信息为JSON格式"""
|
217
|
-
category = categorize_keyword(
|
373
|
+
category = categorize_keyword(
|
374
|
+
keyword_name, keyword_info, project_custom_keywords
|
375
|
+
)
|
376
|
+
source_info = get_keyword_source_info(keyword_info)
|
218
377
|
|
219
378
|
keyword_data = {
|
220
379
|
'name': keyword_name,
|
221
380
|
'category': category,
|
381
|
+
'source_info': source_info,
|
222
382
|
'parameters': []
|
223
383
|
}
|
224
384
|
|
@@ -229,21 +389,30 @@ def format_keyword_info_json(keyword_name, keyword_info):
|
|
229
389
|
'original_name': keyword_info.get('original_name', keyword_name)
|
230
390
|
}
|
231
391
|
|
232
|
-
#
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
'name': getattr(param, 'name', str(param)),
|
237
|
-
'mapping': getattr(param, 'mapping', ''),
|
238
|
-
'description': getattr(param, 'description', '')
|
239
|
-
}
|
392
|
+
# 项目自定义关键字特殊信息
|
393
|
+
if category == 'project_custom' and project_custom_keywords:
|
394
|
+
custom_info = project_custom_keywords[keyword_name]
|
395
|
+
keyword_data['file_location'] = custom_info['file']
|
240
396
|
|
241
|
-
#
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
397
|
+
# 对于项目自定义关键字,使用从AST中提取的参数信息
|
398
|
+
for param_info in custom_info.get('parameters', []):
|
399
|
+
keyword_data['parameters'].append(param_info)
|
400
|
+
else:
|
401
|
+
# 参数信息(对于其他类型的关键字)
|
402
|
+
parameters = keyword_info.get('parameters', [])
|
403
|
+
for param in parameters:
|
404
|
+
param_data = {
|
405
|
+
'name': getattr(param, 'name', str(param)),
|
406
|
+
'mapping': getattr(param, 'mapping', ''),
|
407
|
+
'description': getattr(param, 'description', '')
|
408
|
+
}
|
409
|
+
|
410
|
+
# 添加默认值信息
|
411
|
+
param_default = getattr(param, 'default', None)
|
412
|
+
if param_default is not None:
|
413
|
+
param_data['default'] = param_default
|
414
|
+
|
415
|
+
keyword_data['parameters'].append(param_data)
|
247
416
|
|
248
417
|
# 函数文档
|
249
418
|
func = keyword_info.get('func')
|
@@ -253,13 +422,104 @@ def format_keyword_info_json(keyword_name, keyword_info):
|
|
253
422
|
return keyword_data
|
254
423
|
|
255
424
|
|
425
|
+
def generate_html_report(keywords_data, output_file):
|
426
|
+
"""生成HTML格式的关键字报告"""
|
427
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
428
|
+
import os
|
429
|
+
|
430
|
+
# 准备数据
|
431
|
+
summary = keywords_data['summary']
|
432
|
+
keywords = keywords_data['keywords']
|
433
|
+
|
434
|
+
# 按类别分组
|
435
|
+
categories = {}
|
436
|
+
for keyword in keywords:
|
437
|
+
category = keyword['category']
|
438
|
+
if category not in categories:
|
439
|
+
categories[category] = []
|
440
|
+
categories[category].append(keyword)
|
441
|
+
|
442
|
+
# 按来源分组(用于更详细的分组视图)
|
443
|
+
source_groups = {}
|
444
|
+
for keyword in keywords:
|
445
|
+
source_info = keyword.get('source_info', {})
|
446
|
+
category = keyword['category']
|
447
|
+
source_name = source_info.get('name', '未知来源')
|
448
|
+
|
449
|
+
# 构建分组键
|
450
|
+
if category == 'plugin':
|
451
|
+
group_key = f"插件 - {source_name}"
|
452
|
+
elif category == 'builtin':
|
453
|
+
group_key = "内置关键字"
|
454
|
+
elif category == 'project_custom':
|
455
|
+
group_key = f"项目自定义 - {keyword.get('file_location', source_name)}"
|
456
|
+
elif category == 'remote':
|
457
|
+
group_key = f"远程 - {source_name}"
|
458
|
+
else:
|
459
|
+
group_key = f"自定义 - {source_name}"
|
460
|
+
|
461
|
+
if group_key not in source_groups:
|
462
|
+
source_groups[group_key] = []
|
463
|
+
source_groups[group_key].append(keyword)
|
464
|
+
|
465
|
+
# 按位置分组(用于全部关键字视图,保持向后兼容)
|
466
|
+
location_groups = {}
|
467
|
+
for keyword in keywords:
|
468
|
+
# 优先使用file_location,然后使用source_info中的name
|
469
|
+
location = keyword.get('file_location')
|
470
|
+
if not location:
|
471
|
+
source_info = keyword.get('source_info', {})
|
472
|
+
location = source_info.get('name', '内置/插件')
|
473
|
+
|
474
|
+
if location not in location_groups:
|
475
|
+
location_groups[location] = []
|
476
|
+
location_groups[location].append(keyword)
|
477
|
+
|
478
|
+
# 类别名称映射
|
479
|
+
category_names = {
|
480
|
+
'builtin': '内置',
|
481
|
+
'plugin': '插件',
|
482
|
+
'custom': '自定义',
|
483
|
+
'project_custom': '项目自定义',
|
484
|
+
'remote': '远程'
|
485
|
+
}
|
486
|
+
|
487
|
+
# 设置Jinja2环境
|
488
|
+
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
489
|
+
|
490
|
+
env = Environment(
|
491
|
+
loader=FileSystemLoader(template_dir),
|
492
|
+
autoescape=select_autoescape(['html', 'xml'])
|
493
|
+
)
|
494
|
+
|
495
|
+
# 加载模板
|
496
|
+
template = env.get_template('keywords_report.html')
|
497
|
+
|
498
|
+
# 渲染模板
|
499
|
+
html_content = template.render(
|
500
|
+
summary=summary,
|
501
|
+
keywords=keywords,
|
502
|
+
categories=categories,
|
503
|
+
source_groups=source_groups,
|
504
|
+
location_groups=location_groups,
|
505
|
+
category_names=category_names
|
506
|
+
)
|
507
|
+
|
508
|
+
# 写入文件
|
509
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
510
|
+
f.write(html_content)
|
511
|
+
|
512
|
+
print(f"HTML报告已生成: {output_file}")
|
513
|
+
|
514
|
+
|
256
515
|
def list_keywords(output_format='json', name_filter=None,
|
257
|
-
category_filter='all', output_file=None
|
516
|
+
category_filter='all', output_file=None,
|
517
|
+
include_remote=False):
|
258
518
|
"""罗列所有关键字信息"""
|
259
519
|
import json
|
260
520
|
|
261
521
|
print("正在加载关键字...")
|
262
|
-
load_all_keywords()
|
522
|
+
project_custom_keywords = load_all_keywords(include_remote=include_remote)
|
263
523
|
|
264
524
|
# 获取所有注册的关键字
|
265
525
|
all_keywords = keyword_manager._keywords
|
@@ -276,9 +536,15 @@ def list_keywords(output_format='json', name_filter=None,
|
|
276
536
|
if name_filter and name_filter.lower() not in name.lower():
|
277
537
|
continue
|
278
538
|
|
539
|
+
# 远程关键字过滤
|
540
|
+
if not include_remote and info.get('remote', False):
|
541
|
+
continue
|
542
|
+
|
279
543
|
# 类别过滤
|
280
544
|
if category_filter != 'all':
|
281
|
-
keyword_category = categorize_keyword(
|
545
|
+
keyword_category = categorize_keyword(
|
546
|
+
name, info, project_custom_keywords
|
547
|
+
)
|
282
548
|
if keyword_category != category_filter:
|
283
549
|
continue
|
284
550
|
|
@@ -294,51 +560,80 @@ def list_keywords(output_format='json', name_filter=None,
|
|
294
560
|
# 输出统计信息
|
295
561
|
total_count = len(filtered_keywords)
|
296
562
|
category_counts = {}
|
563
|
+
source_counts = {}
|
564
|
+
|
297
565
|
for name, info in filtered_keywords.items():
|
298
|
-
cat = categorize_keyword(name, info)
|
566
|
+
cat = categorize_keyword(name, info, project_custom_keywords)
|
299
567
|
category_counts[cat] = category_counts.get(cat, 0) + 1
|
568
|
+
|
569
|
+
# 统计各来源的关键字数量
|
570
|
+
source_info = get_keyword_source_info(info)
|
571
|
+
source_name = source_info['name']
|
572
|
+
if cat == 'project_custom' and project_custom_keywords:
|
573
|
+
custom_info = project_custom_keywords[name]
|
574
|
+
source_name = custom_info['file']
|
575
|
+
|
576
|
+
source_key = f"{cat}:{source_name}"
|
577
|
+
source_counts[source_key] = source_counts.get(source_key, 0) + 1
|
300
578
|
|
301
579
|
if output_format == 'text':
|
302
580
|
print(f"\n找到 {total_count} 个关键字:")
|
303
581
|
for cat, count in category_counts.items():
|
304
|
-
cat_names = {
|
582
|
+
cat_names = {
|
583
|
+
'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
|
584
|
+
'project_custom': '项目自定义', 'remote': '远程'
|
585
|
+
}
|
305
586
|
print(f" {cat_names.get(cat, cat)}: {count} 个")
|
306
587
|
print("-" * 60)
|
307
588
|
|
308
|
-
#
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
589
|
+
# 按类别和来源分组显示
|
590
|
+
grouped = group_keywords_by_source(
|
591
|
+
filtered_keywords, project_custom_keywords
|
592
|
+
)
|
593
|
+
|
594
|
+
for category in [
|
595
|
+
'builtin', 'plugin', 'custom', 'project_custom', 'remote'
|
596
|
+
]:
|
597
|
+
if category not in grouped or not grouped[category]:
|
598
|
+
continue
|
599
|
+
|
600
|
+
cat_names = {
|
601
|
+
'builtin': '内置关键字',
|
602
|
+
'plugin': '插件关键字',
|
603
|
+
'custom': '自定义关键字',
|
604
|
+
'project_custom': '项目自定义关键字',
|
605
|
+
'remote': '远程关键字'
|
313
606
|
}
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
for name in sorted(cat_keywords.keys()):
|
324
|
-
info = cat_keywords[name]
|
607
|
+
print(f"\n=== {cat_names[category]} ===")
|
608
|
+
|
609
|
+
for source_name, keyword_list in grouped[category].items():
|
610
|
+
if len(grouped[category]) > 1: # 如果有多个来源,显示来源名
|
611
|
+
print(f"\n--- {source_name} ---")
|
612
|
+
|
613
|
+
for keyword_data in keyword_list:
|
614
|
+
name = keyword_data['name']
|
615
|
+
info = keyword_data['info']
|
325
616
|
print()
|
326
617
|
print(format_keyword_info_text(
|
327
|
-
name, info, show_category=False
|
618
|
+
name, info, show_category=False,
|
619
|
+
project_custom_keywords=project_custom_keywords
|
328
620
|
))
|
329
621
|
|
330
622
|
elif output_format == 'json':
|
331
623
|
keywords_data = {
|
332
624
|
'summary': {
|
333
625
|
'total_count': total_count,
|
334
|
-
'category_counts': category_counts
|
626
|
+
'category_counts': category_counts,
|
627
|
+
'source_counts': source_counts
|
335
628
|
},
|
336
629
|
'keywords': []
|
337
630
|
}
|
338
631
|
|
339
632
|
for name in sorted(filtered_keywords.keys()):
|
340
633
|
info = filtered_keywords[name]
|
341
|
-
keyword_data = format_keyword_info_json(
|
634
|
+
keyword_data = format_keyword_info_json(
|
635
|
+
name, info, project_custom_keywords
|
636
|
+
)
|
342
637
|
keywords_data['keywords'].append(keyword_data)
|
343
638
|
|
344
639
|
json_output = json.dumps(keywords_data, ensure_ascii=False, indent=2)
|
@@ -354,13 +649,51 @@ def list_keywords(output_format='json', name_filter=None,
|
|
354
649
|
print(f"关键字信息已保存到文件: {output_file}")
|
355
650
|
print(f"共 {total_count} 个关键字")
|
356
651
|
for cat, count in category_counts.items():
|
357
|
-
cat_names = {
|
652
|
+
cat_names = {
|
653
|
+
'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
|
654
|
+
'project_custom': '项目自定义', 'remote': '远程'
|
655
|
+
}
|
358
656
|
print(f" {cat_names.get(cat, cat)}: {count} 个")
|
359
657
|
except Exception as e:
|
360
658
|
print(f"保存文件失败: {e}")
|
361
659
|
# 如果写入文件失败,则回退到打印
|
362
660
|
print(json_output)
|
363
661
|
|
662
|
+
elif output_format == 'html':
|
663
|
+
keywords_data = {
|
664
|
+
'summary': {
|
665
|
+
'total_count': total_count,
|
666
|
+
'category_counts': category_counts,
|
667
|
+
'source_counts': source_counts
|
668
|
+
},
|
669
|
+
'keywords': []
|
670
|
+
}
|
671
|
+
|
672
|
+
for name in sorted(filtered_keywords.keys()):
|
673
|
+
info = filtered_keywords[name]
|
674
|
+
keyword_data = format_keyword_info_json(
|
675
|
+
name, info, project_custom_keywords
|
676
|
+
)
|
677
|
+
keywords_data['keywords'].append(keyword_data)
|
678
|
+
|
679
|
+
# 确定输出文件名
|
680
|
+
if output_file is None:
|
681
|
+
output_file = 'keywords.html'
|
682
|
+
|
683
|
+
# 生成HTML报告
|
684
|
+
try:
|
685
|
+
generate_html_report(keywords_data, output_file)
|
686
|
+
print(f"共 {total_count} 个关键字")
|
687
|
+
for cat, count in category_counts.items():
|
688
|
+
cat_names = {
|
689
|
+
'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
|
690
|
+
'project_custom': '项目自定义', 'remote': '远程'
|
691
|
+
}
|
692
|
+
print(f" {cat_names.get(cat, cat)}: {count} 个")
|
693
|
+
except Exception as e:
|
694
|
+
print(f"生成HTML报告失败: {e}")
|
695
|
+
raise
|
696
|
+
|
364
697
|
|
365
698
|
def load_yaml_variables(args):
|
366
699
|
"""从命令行参数加载YAML变量"""
|
@@ -408,8 +741,8 @@ def run_dsl_tests(args):
|
|
408
741
|
print("错误: 必须指定要执行的DSL文件路径或目录")
|
409
742
|
sys.exit(1)
|
410
743
|
|
411
|
-
#
|
412
|
-
load_all_keywords()
|
744
|
+
# 加载内置关键字插件(运行时总是包含远程关键字)
|
745
|
+
load_all_keywords(include_remote=True)
|
413
746
|
|
414
747
|
# 加载YAML变量(包括远程服务器自动连接)
|
415
748
|
load_yaml_variables(args)
|
@@ -474,18 +807,21 @@ def main():
|
|
474
807
|
output_format=args.format,
|
475
808
|
name_filter=args.filter,
|
476
809
|
category_filter=args.category,
|
477
|
-
output_file=args.output
|
810
|
+
output_file=args.output,
|
811
|
+
include_remote=args.include_remote
|
478
812
|
)
|
479
813
|
elif args.command == 'run':
|
480
814
|
run_dsl_tests(args)
|
481
815
|
elif args.command == 'list-keywords-compat':
|
482
816
|
# 向后兼容:旧的--list-keywords格式
|
483
817
|
output_file = getattr(args, 'output', None)
|
818
|
+
include_remote = getattr(args, 'include_remote', False)
|
484
819
|
list_keywords(
|
485
820
|
output_format=args.format,
|
486
821
|
name_filter=args.filter,
|
487
822
|
category_filter=args.category,
|
488
|
-
output_file=output_file
|
823
|
+
output_file=output_file,
|
824
|
+
include_remote=include_remote
|
489
825
|
)
|
490
826
|
elif args.command == 'run-compat':
|
491
827
|
# 向后兼容:默认执行DSL测试
|
@@ -500,13 +836,13 @@ def main_list_keywords():
|
|
500
836
|
"""关键字列表命令的专用入口点"""
|
501
837
|
parser = argparse.ArgumentParser(description='查看pytest-dsl可用关键字列表')
|
502
838
|
parser.add_argument(
|
503
|
-
'--format', choices=['text', 'json'],
|
839
|
+
'--format', choices=['text', 'json', 'html'],
|
504
840
|
default='json',
|
505
|
-
help='输出格式:json(默认) 或
|
841
|
+
help='输出格式:json(默认)、text 或 html'
|
506
842
|
)
|
507
843
|
parser.add_argument(
|
508
844
|
'--output', '-o', type=str, default=None,
|
509
|
-
help='
|
845
|
+
help='输出文件路径(json格式默认为keywords.json,html格式默认为keywords.html)'
|
510
846
|
)
|
511
847
|
parser.add_argument(
|
512
848
|
'--filter', type=str, default=None,
|
@@ -514,9 +850,14 @@ def main_list_keywords():
|
|
514
850
|
)
|
515
851
|
parser.add_argument(
|
516
852
|
'--category',
|
517
|
-
choices=['builtin', 'custom', 'remote', 'all'],
|
853
|
+
choices=['builtin', 'plugin', 'custom', 'project_custom', 'remote', 'all'],
|
518
854
|
default='all',
|
519
|
-
help='关键字类别:builtin(内置)、
|
855
|
+
help='关键字类别:builtin(内置)、plugin(插件)、custom(自定义)、'
|
856
|
+
'project_custom(项目自定义)、remote(远程)、all(全部,默认)'
|
857
|
+
)
|
858
|
+
parser.add_argument(
|
859
|
+
'--include-remote', action='store_true',
|
860
|
+
help='是否包含远程关键字(默认不包含)'
|
520
861
|
)
|
521
862
|
|
522
863
|
args = parser.parse_args()
|
@@ -525,9 +866,112 @@ def main_list_keywords():
|
|
525
866
|
output_format=args.format,
|
526
867
|
name_filter=args.filter,
|
527
868
|
category_filter=args.category,
|
528
|
-
output_file=args.output
|
869
|
+
output_file=args.output,
|
870
|
+
include_remote=args.include_remote
|
529
871
|
)
|
530
872
|
|
531
873
|
|
874
|
+
def scan_project_custom_keywords(project_root=None):
|
875
|
+
"""扫描项目中.resource文件中的自定义关键字
|
876
|
+
|
877
|
+
Args:
|
878
|
+
project_root: 项目根目录,默认为当前工作目录
|
879
|
+
|
880
|
+
Returns:
|
881
|
+
dict: 自定义关键字信息,格式为
|
882
|
+
{keyword_name: {'file': file_path, 'node': ast_node}}
|
883
|
+
"""
|
884
|
+
if project_root is None:
|
885
|
+
project_root = os.getcwd()
|
886
|
+
|
887
|
+
project_root = Path(project_root)
|
888
|
+
custom_keywords = {}
|
889
|
+
|
890
|
+
# 查找所有.resource文件
|
891
|
+
resource_files = list(project_root.glob('**/*.resource'))
|
892
|
+
|
893
|
+
if not resource_files:
|
894
|
+
return custom_keywords
|
895
|
+
|
896
|
+
lexer = get_lexer()
|
897
|
+
parser = get_parser()
|
898
|
+
|
899
|
+
for file_path in resource_files:
|
900
|
+
try:
|
901
|
+
# 读取并解析文件
|
902
|
+
content = read_file(str(file_path))
|
903
|
+
ast = parser.parse(content, lexer=lexer)
|
904
|
+
|
905
|
+
# 查找自定义关键字定义
|
906
|
+
keywords_in_file = extract_custom_keywords_from_ast(
|
907
|
+
ast, str(file_path)
|
908
|
+
)
|
909
|
+
custom_keywords.update(keywords_in_file)
|
910
|
+
|
911
|
+
except Exception as e:
|
912
|
+
print(f"解析资源文件 {file_path} 时出错: {e}")
|
913
|
+
|
914
|
+
return custom_keywords
|
915
|
+
|
916
|
+
|
917
|
+
def extract_custom_keywords_from_ast(ast, file_path):
|
918
|
+
"""从AST中提取自定义关键字定义
|
919
|
+
|
920
|
+
Args:
|
921
|
+
ast: 抽象语法树
|
922
|
+
file_path: 文件路径
|
923
|
+
|
924
|
+
Returns:
|
925
|
+
dict: 自定义关键字信息
|
926
|
+
"""
|
927
|
+
custom_keywords = {}
|
928
|
+
|
929
|
+
if ast.type != 'Start' or len(ast.children) < 2:
|
930
|
+
return custom_keywords
|
931
|
+
|
932
|
+
# 遍历语句节点
|
933
|
+
statements_node = ast.children[1]
|
934
|
+
if statements_node.type != 'Statements':
|
935
|
+
return custom_keywords
|
936
|
+
|
937
|
+
for node in statements_node.children:
|
938
|
+
# 支持两种格式:CustomKeyword(旧格式)和Function(新格式)
|
939
|
+
if node.type in ['CustomKeyword', 'Function']:
|
940
|
+
keyword_name = node.value
|
941
|
+
|
942
|
+
# 提取参数信息
|
943
|
+
params_node = node.children[0] if node.children else None
|
944
|
+
parameters = []
|
945
|
+
|
946
|
+
if params_node:
|
947
|
+
for param in params_node:
|
948
|
+
param_name = param.value
|
949
|
+
param_default = None
|
950
|
+
|
951
|
+
# 检查是否有默认值
|
952
|
+
if param.children and param.children[0]:
|
953
|
+
param_default = param.children[0].value
|
954
|
+
|
955
|
+
param_info = {
|
956
|
+
'name': param_name,
|
957
|
+
'mapping': param_name,
|
958
|
+
'description': f'自定义关键字参数 {param_name}'
|
959
|
+
}
|
960
|
+
|
961
|
+
if param_default is not None:
|
962
|
+
param_info['default'] = param_default
|
963
|
+
|
964
|
+
parameters.append(param_info)
|
965
|
+
|
966
|
+
custom_keywords[keyword_name] = {
|
967
|
+
'file': file_path,
|
968
|
+
'node': node,
|
969
|
+
'type': 'project_custom',
|
970
|
+
'parameters': parameters
|
971
|
+
}
|
972
|
+
|
973
|
+
return custom_keywords
|
974
|
+
|
975
|
+
|
532
976
|
if __name__ == '__main__':
|
533
977
|
main()
|