pytest-dsl 0.15.0__py3-none-any.whl → 0.15.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/__init__.py +12 -0
- pytest_dsl/cli.py +15 -427
- pytest_dsl/core/dsl_executor.py +20 -4
- pytest_dsl/core/keyword_utils.py +609 -0
- pytest_dsl/core/lexer.py +8 -0
- pytest_dsl/core/parser.py +37 -3
- {pytest_dsl-0.15.0.dist-info → pytest_dsl-0.15.1.dist-info}/METADATA +1 -1
- {pytest_dsl-0.15.0.dist-info → pytest_dsl-0.15.1.dist-info}/RECORD +12 -11
- {pytest_dsl-0.15.0.dist-info → pytest_dsl-0.15.1.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.15.0.dist-info → pytest_dsl-0.15.1.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.15.0.dist-info → pytest_dsl-0.15.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.15.0.dist-info → pytest_dsl-0.15.1.dist-info}/top_level.txt +0 -0
pytest_dsl/__init__.py
CHANGED
@@ -54,6 +54,13 @@ from pytest_dsl.core.keyword_loader import (
|
|
54
54
|
group_keywords_by_source, scan_project_custom_keywords
|
55
55
|
)
|
56
56
|
|
57
|
+
# 关键字工具
|
58
|
+
from pytest_dsl.core.keyword_utils import (
|
59
|
+
KeywordInfo, KeywordListOptions, KeywordFormatter, KeywordLister,
|
60
|
+
keyword_lister, list_keywords, get_keyword_info, search_keywords,
|
61
|
+
generate_html_report
|
62
|
+
)
|
63
|
+
|
57
64
|
# 便捷导入的别名
|
58
65
|
Executor = DSLExecutor
|
59
66
|
Validator = DSLValidator
|
@@ -77,6 +84,11 @@ __all__ = [
|
|
77
84
|
'load_all_keywords', 'categorize_keyword', 'get_keyword_source_info',
|
78
85
|
'group_keywords_by_source', 'scan_project_custom_keywords',
|
79
86
|
|
87
|
+
# 关键字工具
|
88
|
+
'KeywordInfo', 'KeywordListOptions', 'KeywordFormatter', 'KeywordLister',
|
89
|
+
'keyword_lister', 'list_keywords', 'get_keyword_info', 'search_keywords',
|
90
|
+
'generate_html_report',
|
91
|
+
|
80
92
|
# Hook系统
|
81
93
|
'hookimpl', 'hookspec', 'DSLHookSpecs',
|
82
94
|
'hook_manager', 'DSLHookManager', 'HookManager',
|
pytest_dsl/cli.py
CHANGED
@@ -16,11 +16,8 @@ from pytest_dsl.core.yaml_loader import load_yaml_variables_from_args
|
|
16
16
|
from pytest_dsl.core.auto_directory import (
|
17
17
|
SETUP_FILE_NAME, TEARDOWN_FILE_NAME, execute_hook_file
|
18
18
|
)
|
19
|
-
from pytest_dsl.core.keyword_loader import
|
20
|
-
|
21
|
-
group_keywords_by_source, scan_project_custom_keywords
|
22
|
-
)
|
23
|
-
from pytest_dsl.core.keyword_manager import keyword_manager
|
19
|
+
from pytest_dsl.core.keyword_loader import load_all_keywords
|
20
|
+
from pytest_dsl.core.keyword_utils import list_keywords as utils_list_keywords
|
24
21
|
|
25
22
|
|
26
23
|
def read_file(filename):
|
@@ -139,431 +136,25 @@ def parse_args():
|
|
139
136
|
return args
|
140
137
|
|
141
138
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
def format_keyword_info_text(keyword_name, keyword_info, show_category=True,
|
146
|
-
project_custom_keywords=None):
|
147
|
-
"""格式化关键字信息为文本格式"""
|
148
|
-
lines = []
|
149
|
-
|
150
|
-
# 关键字名称和类别
|
151
|
-
category = categorize_keyword(
|
152
|
-
keyword_name, keyword_info, project_custom_keywords
|
153
|
-
)
|
154
|
-
category_names = {
|
155
|
-
'builtin': '内置',
|
156
|
-
'custom': '自定义',
|
157
|
-
'project_custom': '项目自定义',
|
158
|
-
'remote': '远程'
|
159
|
-
}
|
160
|
-
|
161
|
-
if show_category:
|
162
|
-
category_display = category_names.get(category, '未知')
|
163
|
-
lines.append(f"关键字: {keyword_name} [{category_display}]")
|
164
|
-
else:
|
165
|
-
lines.append(f"关键字: {keyword_name}")
|
166
|
-
|
167
|
-
# 远程关键字特殊标识
|
168
|
-
if keyword_info.get('remote', False):
|
169
|
-
alias = keyword_info.get('alias', '未知')
|
170
|
-
original_name = keyword_info.get('original_name', keyword_name)
|
171
|
-
lines.append(f" 远程服务器: {alias}")
|
172
|
-
lines.append(f" 原始名称: {original_name}")
|
173
|
-
|
174
|
-
# 项目自定义关键字特殊标识
|
175
|
-
if category == 'project_custom' and project_custom_keywords:
|
176
|
-
custom_info = project_custom_keywords[keyword_name]
|
177
|
-
lines.append(f" 文件位置: {custom_info['file']}")
|
178
|
-
|
179
|
-
# 对于项目自定义关键字,使用从AST中提取的参数信息
|
180
|
-
custom_parameters = custom_info.get('parameters', [])
|
181
|
-
if custom_parameters:
|
182
|
-
lines.append(" 参数:")
|
183
|
-
for param_info in custom_parameters:
|
184
|
-
param_name = param_info['name']
|
185
|
-
param_mapping = param_info.get('mapping', '')
|
186
|
-
param_desc = param_info.get('description', '')
|
187
|
-
param_default = param_info.get('default', None)
|
188
|
-
|
189
|
-
# 构建参数描述
|
190
|
-
param_parts = []
|
191
|
-
if param_mapping and param_mapping != param_name:
|
192
|
-
param_parts.append(f"{param_name} ({param_mapping})")
|
193
|
-
else:
|
194
|
-
param_parts.append(param_name)
|
195
|
-
|
196
|
-
param_parts.append(f": {param_desc}")
|
197
|
-
|
198
|
-
# 添加默认值信息
|
199
|
-
if param_default is not None:
|
200
|
-
param_parts.append(f" (默认值: {param_default})")
|
201
|
-
|
202
|
-
lines.append(f" {''.join(param_parts)}")
|
203
|
-
else:
|
204
|
-
lines.append(" 参数: 无")
|
205
|
-
else:
|
206
|
-
# 参数信息(对于其他类型的关键字)
|
207
|
-
parameters = keyword_info.get('parameters', [])
|
208
|
-
if parameters:
|
209
|
-
lines.append(" 参数:")
|
210
|
-
for param in parameters:
|
211
|
-
param_name = getattr(param, 'name', str(param))
|
212
|
-
param_mapping = getattr(param, 'mapping', '')
|
213
|
-
param_desc = getattr(param, 'description', '')
|
214
|
-
param_default = getattr(param, 'default', None)
|
215
|
-
|
216
|
-
# 构建参数描述
|
217
|
-
param_info = []
|
218
|
-
if param_mapping and param_mapping != param_name:
|
219
|
-
param_info.append(f"{param_name} ({param_mapping})")
|
220
|
-
else:
|
221
|
-
param_info.append(param_name)
|
222
|
-
|
223
|
-
param_info.append(f": {param_desc}")
|
224
|
-
|
225
|
-
# 添加默认值信息
|
226
|
-
if param_default is not None:
|
227
|
-
param_info.append(f" (默认值: {param_default})")
|
228
|
-
|
229
|
-
lines.append(f" {''.join(param_info)}")
|
230
|
-
else:
|
231
|
-
lines.append(" 参数: 无")
|
232
|
-
|
233
|
-
# 函数文档
|
234
|
-
func = keyword_info.get('func')
|
235
|
-
if func and hasattr(func, '__doc__') and func.__doc__:
|
236
|
-
lines.append(f" 说明: {func.__doc__.strip()}")
|
237
|
-
|
238
|
-
return '\n'.join(lines)
|
239
|
-
|
240
|
-
|
241
|
-
def format_keyword_info_json(keyword_name, keyword_info,
|
242
|
-
project_custom_keywords=None):
|
243
|
-
"""格式化关键字信息为JSON格式"""
|
244
|
-
category = categorize_keyword(
|
245
|
-
keyword_name, keyword_info, project_custom_keywords
|
246
|
-
)
|
247
|
-
source_info = get_keyword_source_info(keyword_info)
|
248
|
-
|
249
|
-
keyword_data = {
|
250
|
-
'name': keyword_name,
|
251
|
-
'category': category,
|
252
|
-
'source_info': source_info,
|
253
|
-
'parameters': []
|
254
|
-
}
|
255
|
-
|
256
|
-
# 远程关键字特殊信息
|
257
|
-
if keyword_info.get('remote', False):
|
258
|
-
keyword_data['remote'] = {
|
259
|
-
'alias': keyword_info.get('alias', ''),
|
260
|
-
'original_name': keyword_info.get('original_name', keyword_name)
|
261
|
-
}
|
262
|
-
|
263
|
-
# 项目自定义关键字特殊信息
|
264
|
-
if category == 'project_custom' and project_custom_keywords:
|
265
|
-
custom_info = project_custom_keywords[keyword_name]
|
266
|
-
keyword_data['file_location'] = custom_info['file']
|
267
|
-
|
268
|
-
# 对于项目自定义关键字,使用从AST中提取的参数信息
|
269
|
-
for param_info in custom_info.get('parameters', []):
|
270
|
-
keyword_data['parameters'].append(param_info)
|
271
|
-
else:
|
272
|
-
# 参数信息(对于其他类型的关键字)
|
273
|
-
parameters = keyword_info.get('parameters', [])
|
274
|
-
for param in parameters:
|
275
|
-
param_data = {
|
276
|
-
'name': getattr(param, 'name', str(param)),
|
277
|
-
'mapping': getattr(param, 'mapping', ''),
|
278
|
-
'description': getattr(param, 'description', '')
|
279
|
-
}
|
280
|
-
|
281
|
-
# 添加默认值信息
|
282
|
-
param_default = getattr(param, 'default', None)
|
283
|
-
if param_default is not None:
|
284
|
-
param_data['default'] = param_default
|
285
|
-
|
286
|
-
keyword_data['parameters'].append(param_data)
|
287
|
-
|
288
|
-
# 函数文档
|
289
|
-
func = keyword_info.get('func')
|
290
|
-
if func and hasattr(func, '__doc__') and func.__doc__:
|
291
|
-
keyword_data['documentation'] = func.__doc__.strip()
|
292
|
-
|
293
|
-
return keyword_data
|
294
|
-
|
295
|
-
|
296
|
-
def generate_html_report(keywords_data, output_file):
|
297
|
-
"""生成HTML格式的关键字报告"""
|
298
|
-
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
299
|
-
import os
|
300
|
-
|
301
|
-
# 准备数据
|
302
|
-
summary = keywords_data['summary']
|
303
|
-
keywords = keywords_data['keywords']
|
304
|
-
|
305
|
-
# 按类别分组
|
306
|
-
categories = {}
|
307
|
-
for keyword in keywords:
|
308
|
-
category = keyword['category']
|
309
|
-
if category not in categories:
|
310
|
-
categories[category] = []
|
311
|
-
categories[category].append(keyword)
|
312
|
-
|
313
|
-
# 按来源分组(用于更详细的分组视图)
|
314
|
-
source_groups = {}
|
315
|
-
for keyword in keywords:
|
316
|
-
source_info = keyword.get('source_info', {})
|
317
|
-
category = keyword['category']
|
318
|
-
source_name = source_info.get('name', '未知来源')
|
319
|
-
|
320
|
-
# 构建分组键
|
321
|
-
if category == 'plugin':
|
322
|
-
group_key = f"插件 - {source_name}"
|
323
|
-
elif category == 'builtin':
|
324
|
-
group_key = "内置关键字"
|
325
|
-
elif category == 'project_custom':
|
326
|
-
group_key = f"项目自定义 - {keyword.get('file_location', source_name)}"
|
327
|
-
elif category == 'remote':
|
328
|
-
group_key = f"远程 - {source_name}"
|
329
|
-
else:
|
330
|
-
group_key = f"自定义 - {source_name}"
|
331
|
-
|
332
|
-
if group_key not in source_groups:
|
333
|
-
source_groups[group_key] = []
|
334
|
-
source_groups[group_key].append(keyword)
|
335
|
-
|
336
|
-
# 按位置分组(用于全部关键字视图,保持向后兼容)
|
337
|
-
location_groups = {}
|
338
|
-
for keyword in keywords:
|
339
|
-
# 优先使用file_location,然后使用source_info中的name
|
340
|
-
location = keyword.get('file_location')
|
341
|
-
if not location:
|
342
|
-
source_info = keyword.get('source_info', {})
|
343
|
-
location = source_info.get('name', '内置/插件')
|
344
|
-
|
345
|
-
if location not in location_groups:
|
346
|
-
location_groups[location] = []
|
347
|
-
location_groups[location].append(keyword)
|
348
|
-
|
349
|
-
# 类别名称映射
|
350
|
-
category_names = {
|
351
|
-
'builtin': '内置',
|
352
|
-
'plugin': '插件',
|
353
|
-
'custom': '自定义',
|
354
|
-
'project_custom': '项目自定义',
|
355
|
-
'remote': '远程'
|
356
|
-
}
|
357
|
-
|
358
|
-
# 设置Jinja2环境
|
359
|
-
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
|
360
|
-
|
361
|
-
env = Environment(
|
362
|
-
loader=FileSystemLoader(template_dir),
|
363
|
-
autoescape=select_autoescape(['html', 'xml'])
|
364
|
-
)
|
365
|
-
|
366
|
-
# 加载模板
|
367
|
-
template = env.get_template('keywords_report.html')
|
368
|
-
|
369
|
-
# 渲染模板
|
370
|
-
html_content = template.render(
|
371
|
-
summary=summary,
|
372
|
-
keywords=keywords,
|
373
|
-
categories=categories,
|
374
|
-
source_groups=source_groups,
|
375
|
-
location_groups=location_groups,
|
376
|
-
category_names=category_names
|
377
|
-
)
|
378
|
-
|
379
|
-
# 写入文件
|
380
|
-
with open(output_file, 'w', encoding='utf-8') as f:
|
381
|
-
f.write(html_content)
|
382
|
-
|
383
|
-
print(f"HTML报告已生成: {output_file}")
|
384
|
-
|
385
|
-
|
386
139
|
def list_keywords(output_format='json', name_filter=None,
|
387
140
|
category_filter='all', output_file=None,
|
388
141
|
include_remote=False):
|
389
|
-
"""
|
390
|
-
import json
|
391
|
-
|
142
|
+
"""罗列所有关键字信息(简化版,调用统一的工具函数)"""
|
392
143
|
print("正在加载关键字...")
|
393
|
-
project_custom_keywords = load_all_keywords(include_remote=include_remote)
|
394
|
-
|
395
|
-
# 获取所有注册的关键字
|
396
|
-
all_keywords = keyword_manager._keywords
|
397
|
-
|
398
|
-
if not all_keywords:
|
399
|
-
print("未发现任何关键字")
|
400
|
-
return
|
401
|
-
|
402
|
-
# 过滤关键字
|
403
|
-
filtered_keywords = {}
|
404
144
|
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
# 类别过滤
|
415
|
-
if category_filter != 'all':
|
416
|
-
keyword_category = categorize_keyword(
|
417
|
-
name, info, project_custom_keywords
|
418
|
-
)
|
419
|
-
if keyword_category != category_filter:
|
420
|
-
continue
|
421
|
-
|
422
|
-
filtered_keywords[name] = info
|
423
|
-
|
424
|
-
if not filtered_keywords:
|
425
|
-
if name_filter:
|
426
|
-
print(f"未找到包含 '{name_filter}' 的关键字")
|
427
|
-
else:
|
428
|
-
print(f"未找到 {category_filter} 类别的关键字")
|
429
|
-
return
|
430
|
-
|
431
|
-
# 输出统计信息
|
432
|
-
total_count = len(filtered_keywords)
|
433
|
-
category_counts = {}
|
434
|
-
source_counts = {}
|
435
|
-
|
436
|
-
for name, info in filtered_keywords.items():
|
437
|
-
cat = categorize_keyword(name, info, project_custom_keywords)
|
438
|
-
category_counts[cat] = category_counts.get(cat, 0) + 1
|
439
|
-
|
440
|
-
# 统计各来源的关键字数量
|
441
|
-
source_info = get_keyword_source_info(info)
|
442
|
-
source_name = source_info['name']
|
443
|
-
if cat == 'project_custom' and project_custom_keywords:
|
444
|
-
custom_info = project_custom_keywords[name]
|
445
|
-
source_name = custom_info['file']
|
446
|
-
|
447
|
-
source_key = f"{cat}:{source_name}"
|
448
|
-
source_counts[source_key] = source_counts.get(source_key, 0) + 1
|
449
|
-
|
450
|
-
if output_format == 'text':
|
451
|
-
print(f"\n找到 {total_count} 个关键字:")
|
452
|
-
for cat, count in category_counts.items():
|
453
|
-
cat_names = {
|
454
|
-
'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
|
455
|
-
'project_custom': '项目自定义', 'remote': '远程'
|
456
|
-
}
|
457
|
-
print(f" {cat_names.get(cat, cat)}: {count} 个")
|
458
|
-
print("-" * 60)
|
459
|
-
|
460
|
-
# 按类别和来源分组显示
|
461
|
-
grouped = group_keywords_by_source(
|
462
|
-
filtered_keywords, project_custom_keywords
|
145
|
+
# 使用统一的工具函数
|
146
|
+
try:
|
147
|
+
utils_list_keywords(
|
148
|
+
output_format=output_format,
|
149
|
+
name_filter=name_filter,
|
150
|
+
category_filter=category_filter,
|
151
|
+
include_remote=include_remote,
|
152
|
+
output_file=output_file,
|
153
|
+
print_summary=True
|
463
154
|
)
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
]:
|
468
|
-
if category not in grouped or not grouped[category]:
|
469
|
-
continue
|
470
|
-
|
471
|
-
cat_names = {
|
472
|
-
'builtin': '内置关键字',
|
473
|
-
'plugin': '插件关键字',
|
474
|
-
'custom': '自定义关键字',
|
475
|
-
'project_custom': '项目自定义关键字',
|
476
|
-
'remote': '远程关键字'
|
477
|
-
}
|
478
|
-
print(f"\n=== {cat_names[category]} ===")
|
479
|
-
|
480
|
-
for source_name, keyword_list in grouped[category].items():
|
481
|
-
if len(grouped[category]) > 1: # 如果有多个来源,显示来源名
|
482
|
-
print(f"\n--- {source_name} ---")
|
483
|
-
|
484
|
-
for keyword_data in keyword_list:
|
485
|
-
name = keyword_data['name']
|
486
|
-
info = keyword_data['info']
|
487
|
-
print()
|
488
|
-
print(format_keyword_info_text(
|
489
|
-
name, info, show_category=False,
|
490
|
-
project_custom_keywords=project_custom_keywords
|
491
|
-
))
|
492
|
-
|
493
|
-
elif output_format == 'json':
|
494
|
-
keywords_data = {
|
495
|
-
'summary': {
|
496
|
-
'total_count': total_count,
|
497
|
-
'category_counts': category_counts,
|
498
|
-
'source_counts': source_counts
|
499
|
-
},
|
500
|
-
'keywords': []
|
501
|
-
}
|
502
|
-
|
503
|
-
for name in sorted(filtered_keywords.keys()):
|
504
|
-
info = filtered_keywords[name]
|
505
|
-
keyword_data = format_keyword_info_json(
|
506
|
-
name, info, project_custom_keywords
|
507
|
-
)
|
508
|
-
keywords_data['keywords'].append(keyword_data)
|
509
|
-
|
510
|
-
json_output = json.dumps(keywords_data, ensure_ascii=False, indent=2)
|
511
|
-
|
512
|
-
# 确定输出文件名
|
513
|
-
if output_file is None:
|
514
|
-
output_file = 'keywords.json'
|
515
|
-
|
516
|
-
# 写入到文件
|
517
|
-
try:
|
518
|
-
with open(output_file, 'w', encoding='utf-8') as f:
|
519
|
-
f.write(json_output)
|
520
|
-
print(f"关键字信息已保存到文件: {output_file}")
|
521
|
-
print(f"共 {total_count} 个关键字")
|
522
|
-
for cat, count in category_counts.items():
|
523
|
-
cat_names = {
|
524
|
-
'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
|
525
|
-
'project_custom': '项目自定义', 'remote': '远程'
|
526
|
-
}
|
527
|
-
print(f" {cat_names.get(cat, cat)}: {count} 个")
|
528
|
-
except Exception as e:
|
529
|
-
print(f"保存文件失败: {e}")
|
530
|
-
# 如果写入文件失败,则回退到打印
|
531
|
-
print(json_output)
|
532
|
-
|
533
|
-
elif output_format == 'html':
|
534
|
-
keywords_data = {
|
535
|
-
'summary': {
|
536
|
-
'total_count': total_count,
|
537
|
-
'category_counts': category_counts,
|
538
|
-
'source_counts': source_counts
|
539
|
-
},
|
540
|
-
'keywords': []
|
541
|
-
}
|
542
|
-
|
543
|
-
for name in sorted(filtered_keywords.keys()):
|
544
|
-
info = filtered_keywords[name]
|
545
|
-
keyword_data = format_keyword_info_json(
|
546
|
-
name, info, project_custom_keywords
|
547
|
-
)
|
548
|
-
keywords_data['keywords'].append(keyword_data)
|
549
|
-
|
550
|
-
# 确定输出文件名
|
551
|
-
if output_file is None:
|
552
|
-
output_file = 'keywords.html'
|
553
|
-
|
554
|
-
# 生成HTML报告
|
555
|
-
try:
|
556
|
-
generate_html_report(keywords_data, output_file)
|
557
|
-
print(f"共 {total_count} 个关键字")
|
558
|
-
for cat, count in category_counts.items():
|
559
|
-
cat_names = {
|
560
|
-
'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
|
561
|
-
'project_custom': '项目自定义', 'remote': '远程'
|
562
|
-
}
|
563
|
-
print(f" {cat_names.get(cat, cat)}: {count} 个")
|
564
|
-
except Exception as e:
|
565
|
-
print(f"生成HTML报告失败: {e}")
|
566
|
-
raise
|
155
|
+
except Exception as e:
|
156
|
+
print(f"列出关键字失败: {e}")
|
157
|
+
raise
|
567
158
|
|
568
159
|
|
569
160
|
def load_yaml_variables(args):
|
@@ -776,8 +367,5 @@ def main_list_keywords():
|
|
776
367
|
)
|
777
368
|
|
778
369
|
|
779
|
-
|
780
|
-
|
781
|
-
|
782
370
|
if __name__ == '__main__':
|
783
371
|
main()
|
pytest_dsl/core/dsl_executor.py
CHANGED
@@ -321,8 +321,6 @@ class DSLExecutor:
|
|
321
321
|
def _handle_start(self, node):
|
322
322
|
"""处理开始节点"""
|
323
323
|
try:
|
324
|
-
# 清空上下文,确保每个测试用例都有一个新的上下文
|
325
|
-
self.test_context.clear()
|
326
324
|
metadata = {}
|
327
325
|
teardown_node = None
|
328
326
|
|
@@ -722,6 +720,7 @@ class DSLExecutor:
|
|
722
720
|
kwargs['step_name'] = keyword_name # 内层步骤只显示关键字名称
|
723
721
|
# 避免KeywordManager重复记录,由DSL执行器统一记录
|
724
722
|
kwargs['skip_logging'] = True
|
723
|
+
|
725
724
|
result = keyword_manager.execute(keyword_name, **kwargs)
|
726
725
|
|
727
726
|
# 执行成功后记录关键字信息,包含行号
|
@@ -733,9 +732,26 @@ class DSLExecutor:
|
|
733
732
|
|
734
733
|
return result
|
735
734
|
except Exception as e:
|
736
|
-
#
|
735
|
+
# 记录关键字执行失败,包含行号信息和更详细的错误信息
|
736
|
+
error_details = (
|
737
|
+
f"关键字: {keyword_name}\n"
|
738
|
+
f"错误: {str(e)}\n"
|
739
|
+
f"错误类型: {type(e).__name__}"
|
740
|
+
f"{line_info}"
|
741
|
+
)
|
742
|
+
|
743
|
+
# 如果异常中包含了内部行号信息,提取并显示
|
744
|
+
if hasattr(e, 'args') and e.args:
|
745
|
+
error_msg = str(e.args[0])
|
746
|
+
# 尝试从错误消息中提取行号信息
|
747
|
+
import re
|
748
|
+
line_match = re.search(r'行(\d+)', error_msg)
|
749
|
+
if line_match:
|
750
|
+
inner_line = int(line_match.group(1))
|
751
|
+
error_details += f"\n内部错误行号: {inner_line}"
|
752
|
+
|
737
753
|
allure.attach(
|
738
|
-
|
754
|
+
error_details,
|
739
755
|
name="关键字调用失败",
|
740
756
|
attachment_type=allure.attachment_type.TEXT
|
741
757
|
)
|
@@ -0,0 +1,609 @@
|
|
1
|
+
"""
|
2
|
+
pytest-dsl关键字工具
|
3
|
+
|
4
|
+
提供统一的关键字列表查看、格式化和导出功能,供CLI和其他程序使用。
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
import os
|
9
|
+
from typing import Dict, Any, Optional, Union, List
|
10
|
+
|
11
|
+
from pytest_dsl.core.keyword_loader import (
|
12
|
+
load_all_keywords, categorize_keyword, get_keyword_source_info,
|
13
|
+
group_keywords_by_source
|
14
|
+
)
|
15
|
+
from pytest_dsl.core.keyword_manager import keyword_manager
|
16
|
+
|
17
|
+
|
18
|
+
class KeywordInfo:
|
19
|
+
"""关键字信息类"""
|
20
|
+
|
21
|
+
def __init__(self, name: str, info: Dict[str, Any],
|
22
|
+
project_custom_keywords: Optional[Dict[str, Any]] = None):
|
23
|
+
self.name = name
|
24
|
+
self.info = info
|
25
|
+
self.project_custom_keywords = project_custom_keywords
|
26
|
+
self._category = None
|
27
|
+
self._source_info = None
|
28
|
+
|
29
|
+
@property
|
30
|
+
def category(self) -> str:
|
31
|
+
"""获取关键字类别"""
|
32
|
+
if self._category is None:
|
33
|
+
self._category = categorize_keyword(
|
34
|
+
self.name, self.info, self.project_custom_keywords
|
35
|
+
)
|
36
|
+
return self._category
|
37
|
+
|
38
|
+
@property
|
39
|
+
def source_info(self) -> Dict[str, Any]:
|
40
|
+
"""获取来源信息"""
|
41
|
+
if self._source_info is None:
|
42
|
+
self._source_info = get_keyword_source_info(self.info)
|
43
|
+
return self._source_info
|
44
|
+
|
45
|
+
@property
|
46
|
+
def parameters(self) -> List[Dict[str, Any]]:
|
47
|
+
"""获取参数信息"""
|
48
|
+
if (self.category == 'project_custom' and
|
49
|
+
self.project_custom_keywords and
|
50
|
+
self.name in self.project_custom_keywords):
|
51
|
+
return self.project_custom_keywords[self.name].get('parameters', [])
|
52
|
+
|
53
|
+
# 对于其他类型的关键字
|
54
|
+
parameters = self.info.get('parameters', [])
|
55
|
+
param_list = []
|
56
|
+
for param in parameters:
|
57
|
+
param_data = {
|
58
|
+
'name': getattr(param, 'name', str(param)),
|
59
|
+
'mapping': getattr(param, 'mapping', ''),
|
60
|
+
'description': getattr(param, 'description', '')
|
61
|
+
}
|
62
|
+
|
63
|
+
# 添加默认值信息
|
64
|
+
param_default = getattr(param, 'default', None)
|
65
|
+
if param_default is not None:
|
66
|
+
param_data['default'] = param_default
|
67
|
+
|
68
|
+
param_list.append(param_data)
|
69
|
+
|
70
|
+
return param_list
|
71
|
+
|
72
|
+
@property
|
73
|
+
def documentation(self) -> str:
|
74
|
+
"""获取文档信息"""
|
75
|
+
func = self.info.get('func')
|
76
|
+
if func and hasattr(func, '__doc__') and func.__doc__:
|
77
|
+
return func.__doc__.strip()
|
78
|
+
return ""
|
79
|
+
|
80
|
+
@property
|
81
|
+
def file_location(self) -> Optional[str]:
|
82
|
+
"""获取文件位置(仅适用于项目自定义关键字)"""
|
83
|
+
if (self.category == 'project_custom' and
|
84
|
+
self.project_custom_keywords and
|
85
|
+
self.name in self.project_custom_keywords):
|
86
|
+
return self.project_custom_keywords[self.name]['file']
|
87
|
+
return None
|
88
|
+
|
89
|
+
@property
|
90
|
+
def remote_info(self) -> Optional[Dict[str, str]]:
|
91
|
+
"""获取远程关键字信息"""
|
92
|
+
if self.info.get('remote', False):
|
93
|
+
return {
|
94
|
+
'alias': self.info.get('alias', ''),
|
95
|
+
'original_name': self.info.get('original_name', self.name)
|
96
|
+
}
|
97
|
+
return None
|
98
|
+
|
99
|
+
|
100
|
+
class KeywordListOptions:
|
101
|
+
"""关键字列表选项"""
|
102
|
+
|
103
|
+
def __init__(self,
|
104
|
+
output_format: str = 'json',
|
105
|
+
name_filter: Optional[str] = None,
|
106
|
+
category_filter: str = 'all',
|
107
|
+
include_remote: bool = False,
|
108
|
+
output_file: Optional[str] = None):
|
109
|
+
self.output_format = output_format
|
110
|
+
self.name_filter = name_filter
|
111
|
+
self.category_filter = category_filter
|
112
|
+
self.include_remote = include_remote
|
113
|
+
self.output_file = output_file
|
114
|
+
|
115
|
+
def should_include_keyword(self, keyword_info: KeywordInfo) -> bool:
|
116
|
+
"""判断是否应该包含此关键字"""
|
117
|
+
# 名称过滤
|
118
|
+
if (self.name_filter and
|
119
|
+
self.name_filter.lower() not in keyword_info.name.lower()):
|
120
|
+
return False
|
121
|
+
|
122
|
+
# 远程关键字过滤
|
123
|
+
if (not self.include_remote and
|
124
|
+
keyword_info.info.get('remote', False)):
|
125
|
+
return False
|
126
|
+
|
127
|
+
# 类别过滤
|
128
|
+
if (self.category_filter != 'all' and
|
129
|
+
keyword_info.category != self.category_filter):
|
130
|
+
return False
|
131
|
+
|
132
|
+
return True
|
133
|
+
|
134
|
+
|
135
|
+
class KeywordFormatter:
|
136
|
+
"""关键字格式化器"""
|
137
|
+
|
138
|
+
def __init__(self):
|
139
|
+
self.category_names = {
|
140
|
+
'builtin': '内置',
|
141
|
+
'plugin': '插件',
|
142
|
+
'custom': '自定义',
|
143
|
+
'project_custom': '项目自定义',
|
144
|
+
'remote': '远程'
|
145
|
+
}
|
146
|
+
|
147
|
+
def format_text(self, keyword_info: KeywordInfo,
|
148
|
+
show_category: bool = True) -> str:
|
149
|
+
"""格式化为文本格式"""
|
150
|
+
lines = []
|
151
|
+
|
152
|
+
# 关键字名称和类别
|
153
|
+
if show_category:
|
154
|
+
category_display = self.category_names.get(
|
155
|
+
keyword_info.category, '未知'
|
156
|
+
)
|
157
|
+
lines.append(f"关键字: {keyword_info.name} [{category_display}]")
|
158
|
+
else:
|
159
|
+
lines.append(f"关键字: {keyword_info.name}")
|
160
|
+
|
161
|
+
# 远程关键字特殊信息
|
162
|
+
if keyword_info.remote_info:
|
163
|
+
remote = keyword_info.remote_info
|
164
|
+
lines.append(f" 远程服务器: {remote['alias']}")
|
165
|
+
lines.append(f" 原始名称: {remote['original_name']}")
|
166
|
+
|
167
|
+
# 项目自定义关键字文件位置
|
168
|
+
if keyword_info.file_location:
|
169
|
+
lines.append(f" 文件位置: {keyword_info.file_location}")
|
170
|
+
|
171
|
+
# 参数信息
|
172
|
+
parameters = keyword_info.parameters
|
173
|
+
if parameters:
|
174
|
+
lines.append(" 参数:")
|
175
|
+
for param_info in parameters:
|
176
|
+
param_name = param_info['name']
|
177
|
+
param_mapping = param_info.get('mapping', '')
|
178
|
+
param_desc = param_info.get('description', '')
|
179
|
+
param_default = param_info.get('default', None)
|
180
|
+
|
181
|
+
# 构建参数描述
|
182
|
+
param_parts = []
|
183
|
+
if param_mapping and param_mapping != param_name:
|
184
|
+
param_parts.append(f"{param_name} ({param_mapping})")
|
185
|
+
else:
|
186
|
+
param_parts.append(param_name)
|
187
|
+
|
188
|
+
param_parts.append(f": {param_desc}")
|
189
|
+
|
190
|
+
# 添加默认值信息
|
191
|
+
if param_default is not None:
|
192
|
+
param_parts.append(f" (默认值: {param_default})")
|
193
|
+
|
194
|
+
lines.append(f" {''.join(param_parts)}")
|
195
|
+
else:
|
196
|
+
lines.append(" 参数: 无")
|
197
|
+
|
198
|
+
# 函数文档
|
199
|
+
if keyword_info.documentation:
|
200
|
+
lines.append(f" 说明: {keyword_info.documentation}")
|
201
|
+
|
202
|
+
return '\n'.join(lines)
|
203
|
+
|
204
|
+
def format_json(self, keyword_info: KeywordInfo) -> Dict[str, Any]:
|
205
|
+
"""格式化为JSON格式"""
|
206
|
+
keyword_data = {
|
207
|
+
'name': keyword_info.name,
|
208
|
+
'category': keyword_info.category,
|
209
|
+
'source_info': keyword_info.source_info,
|
210
|
+
'parameters': keyword_info.parameters
|
211
|
+
}
|
212
|
+
|
213
|
+
# 远程关键字特殊信息
|
214
|
+
if keyword_info.remote_info:
|
215
|
+
keyword_data['remote'] = keyword_info.remote_info
|
216
|
+
|
217
|
+
# 项目自定义关键字文件位置
|
218
|
+
if keyword_info.file_location:
|
219
|
+
keyword_data['file_location'] = keyword_info.file_location
|
220
|
+
|
221
|
+
# 函数文档
|
222
|
+
if keyword_info.documentation:
|
223
|
+
keyword_data['documentation'] = keyword_info.documentation
|
224
|
+
|
225
|
+
return keyword_data
|
226
|
+
|
227
|
+
|
228
|
+
class KeywordLister:
|
229
|
+
"""关键字列表器"""
|
230
|
+
|
231
|
+
def __init__(self):
|
232
|
+
self.formatter = KeywordFormatter()
|
233
|
+
self._project_custom_keywords = None
|
234
|
+
|
235
|
+
def get_keywords(self, options: KeywordListOptions) -> List[KeywordInfo]:
|
236
|
+
"""获取关键字列表
|
237
|
+
|
238
|
+
Args:
|
239
|
+
options: 列表选项
|
240
|
+
|
241
|
+
Returns:
|
242
|
+
符合条件的关键字信息列表
|
243
|
+
"""
|
244
|
+
# 加载关键字
|
245
|
+
if self._project_custom_keywords is None:
|
246
|
+
self._project_custom_keywords = load_all_keywords(
|
247
|
+
include_remote=options.include_remote
|
248
|
+
)
|
249
|
+
|
250
|
+
# 获取所有注册的关键字
|
251
|
+
all_keywords = keyword_manager._keywords
|
252
|
+
|
253
|
+
if not all_keywords:
|
254
|
+
return []
|
255
|
+
|
256
|
+
# 过滤关键字
|
257
|
+
filtered_keywords = []
|
258
|
+
for name, info in all_keywords.items():
|
259
|
+
keyword_info = KeywordInfo(
|
260
|
+
name, info, self._project_custom_keywords
|
261
|
+
)
|
262
|
+
|
263
|
+
if options.should_include_keyword(keyword_info):
|
264
|
+
filtered_keywords.append(keyword_info)
|
265
|
+
|
266
|
+
return filtered_keywords
|
267
|
+
|
268
|
+
def get_keywords_summary(self, keywords: List[KeywordInfo]) -> Dict[str, Any]:
|
269
|
+
"""获取关键字统计摘要
|
270
|
+
|
271
|
+
Args:
|
272
|
+
keywords: 关键字列表
|
273
|
+
|
274
|
+
Returns:
|
275
|
+
统计摘要信息
|
276
|
+
"""
|
277
|
+
total_count = len(keywords)
|
278
|
+
category_counts = {}
|
279
|
+
source_counts = {}
|
280
|
+
|
281
|
+
for keyword_info in keywords:
|
282
|
+
# 类别统计
|
283
|
+
cat = keyword_info.category
|
284
|
+
category_counts[cat] = category_counts.get(cat, 0) + 1
|
285
|
+
|
286
|
+
# 来源统计
|
287
|
+
source_name = keyword_info.source_info['name']
|
288
|
+
if keyword_info.file_location:
|
289
|
+
source_name = keyword_info.file_location
|
290
|
+
|
291
|
+
source_key = f"{cat}:{source_name}"
|
292
|
+
source_counts[source_key] = source_counts.get(source_key, 0) + 1
|
293
|
+
|
294
|
+
return {
|
295
|
+
'total_count': total_count,
|
296
|
+
'category_counts': category_counts,
|
297
|
+
'source_counts': source_counts
|
298
|
+
}
|
299
|
+
|
300
|
+
def list_keywords_text(self, options: KeywordListOptions) -> str:
|
301
|
+
"""以文本格式列出关键字"""
|
302
|
+
keywords = self.get_keywords(options)
|
303
|
+
summary = self.get_keywords_summary(keywords)
|
304
|
+
|
305
|
+
if not keywords:
|
306
|
+
if options.name_filter:
|
307
|
+
return f"未找到包含 '{options.name_filter}' 的关键字"
|
308
|
+
else:
|
309
|
+
return f"未找到 {options.category_filter} 类别的关键字"
|
310
|
+
|
311
|
+
lines = []
|
312
|
+
|
313
|
+
# 统计信息
|
314
|
+
lines.append(f"找到 {summary['total_count']} 个关键字:")
|
315
|
+
for cat, count in summary['category_counts'].items():
|
316
|
+
cat_display = self.formatter.category_names.get(cat, cat)
|
317
|
+
lines.append(f" {cat_display}: {count} 个")
|
318
|
+
lines.append("-" * 60)
|
319
|
+
|
320
|
+
# 按类别和来源分组显示
|
321
|
+
all_keywords_dict = {kw.name: kw.info for kw in keywords}
|
322
|
+
grouped = group_keywords_by_source(
|
323
|
+
all_keywords_dict, self._project_custom_keywords
|
324
|
+
)
|
325
|
+
|
326
|
+
for category in ['builtin', 'plugin', 'custom', 'project_custom', 'remote']:
|
327
|
+
if category not in grouped or not grouped[category]:
|
328
|
+
continue
|
329
|
+
|
330
|
+
cat_names = {
|
331
|
+
'builtin': '内置关键字',
|
332
|
+
'plugin': '插件关键字',
|
333
|
+
'custom': '自定义关键字',
|
334
|
+
'project_custom': '项目自定义关键字',
|
335
|
+
'remote': '远程关键字'
|
336
|
+
}
|
337
|
+
lines.append(f"\n=== {cat_names[category]} ===")
|
338
|
+
|
339
|
+
for source_name, keyword_list in grouped[category].items():
|
340
|
+
if len(grouped[category]) > 1: # 如果有多个来源,显示来源名
|
341
|
+
lines.append(f"\n--- {source_name} ---")
|
342
|
+
|
343
|
+
for keyword_data in keyword_list:
|
344
|
+
name = keyword_data['name']
|
345
|
+
keyword_info = next(
|
346
|
+
kw for kw in keywords if kw.name == name)
|
347
|
+
lines.append("")
|
348
|
+
lines.append(self.formatter.format_text(
|
349
|
+
keyword_info, show_category=False
|
350
|
+
))
|
351
|
+
|
352
|
+
return '\n'.join(lines)
|
353
|
+
|
354
|
+
def list_keywords_json(self, options: KeywordListOptions) -> Dict[str, Any]:
|
355
|
+
"""以JSON格式列出关键字"""
|
356
|
+
keywords = self.get_keywords(options)
|
357
|
+
summary = self.get_keywords_summary(keywords)
|
358
|
+
|
359
|
+
keywords_data = {
|
360
|
+
'summary': summary,
|
361
|
+
'keywords': []
|
362
|
+
}
|
363
|
+
|
364
|
+
# 按名称排序
|
365
|
+
keywords.sort(key=lambda x: x.name)
|
366
|
+
|
367
|
+
for keyword_info in keywords:
|
368
|
+
keyword_data = self.formatter.format_json(keyword_info)
|
369
|
+
keywords_data['keywords'].append(keyword_data)
|
370
|
+
|
371
|
+
return keywords_data
|
372
|
+
|
373
|
+
def list_keywords(self, options: KeywordListOptions) -> Union[str, Dict[str, Any]]:
|
374
|
+
"""列出关键字(根据格式返回不同类型)
|
375
|
+
|
376
|
+
Args:
|
377
|
+
options: 列表选项
|
378
|
+
|
379
|
+
Returns:
|
380
|
+
文本格式返回str,JSON格式返回dict,HTML格式返回dict
|
381
|
+
"""
|
382
|
+
if options.output_format == 'text':
|
383
|
+
return self.list_keywords_text(options)
|
384
|
+
elif options.output_format in ['json', 'html']:
|
385
|
+
return self.list_keywords_json(options)
|
386
|
+
else:
|
387
|
+
raise ValueError(f"不支持的输出格式: {options.output_format}")
|
388
|
+
|
389
|
+
|
390
|
+
def generate_html_report(keywords_data: Dict[str, Any], output_file: str):
|
391
|
+
"""生成HTML格式的关键字报告
|
392
|
+
|
393
|
+
Args:
|
394
|
+
keywords_data: 关键字数据(JSON格式)
|
395
|
+
output_file: 输出文件路径
|
396
|
+
"""
|
397
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
398
|
+
|
399
|
+
# 准备数据
|
400
|
+
summary = keywords_data['summary']
|
401
|
+
keywords = keywords_data['keywords']
|
402
|
+
|
403
|
+
# 按类别分组
|
404
|
+
categories = {}
|
405
|
+
for keyword in keywords:
|
406
|
+
category = keyword['category']
|
407
|
+
if category not in categories:
|
408
|
+
categories[category] = []
|
409
|
+
categories[category].append(keyword)
|
410
|
+
|
411
|
+
# 按来源分组
|
412
|
+
source_groups = {}
|
413
|
+
for keyword in keywords:
|
414
|
+
source_info = keyword.get('source_info', {})
|
415
|
+
category = keyword['category']
|
416
|
+
source_name = source_info.get('name', '未知来源')
|
417
|
+
|
418
|
+
# 构建分组键
|
419
|
+
if category == 'plugin':
|
420
|
+
group_key = f"插件 - {source_name}"
|
421
|
+
elif category == 'builtin':
|
422
|
+
group_key = "内置关键字"
|
423
|
+
elif category == 'project_custom':
|
424
|
+
group_key = f"项目自定义 - {keyword.get('file_location', source_name)}"
|
425
|
+
elif category == 'remote':
|
426
|
+
group_key = f"远程 - {source_name}"
|
427
|
+
else:
|
428
|
+
group_key = f"自定义 - {source_name}"
|
429
|
+
|
430
|
+
if group_key not in source_groups:
|
431
|
+
source_groups[group_key] = []
|
432
|
+
source_groups[group_key].append(keyword)
|
433
|
+
|
434
|
+
# 类别名称映射
|
435
|
+
category_names = {
|
436
|
+
'builtin': '内置',
|
437
|
+
'plugin': '插件',
|
438
|
+
'custom': '自定义',
|
439
|
+
'project_custom': '项目自定义',
|
440
|
+
'remote': '远程'
|
441
|
+
}
|
442
|
+
|
443
|
+
# 设置Jinja2环境
|
444
|
+
template_dir = os.path.join(os.path.dirname(__file__), '..', 'templates')
|
445
|
+
|
446
|
+
env = Environment(
|
447
|
+
loader=FileSystemLoader(template_dir),
|
448
|
+
autoescape=select_autoescape(['html', 'xml'])
|
449
|
+
)
|
450
|
+
|
451
|
+
# 加载模板
|
452
|
+
template = env.get_template('keywords_report.html')
|
453
|
+
|
454
|
+
# 渲染模板
|
455
|
+
html_content = template.render(
|
456
|
+
summary=summary,
|
457
|
+
keywords=keywords,
|
458
|
+
categories=categories,
|
459
|
+
source_groups=source_groups,
|
460
|
+
category_names=category_names
|
461
|
+
)
|
462
|
+
|
463
|
+
# 写入文件
|
464
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
465
|
+
f.write(html_content)
|
466
|
+
|
467
|
+
|
468
|
+
# 创建全局实例
|
469
|
+
keyword_lister = KeywordLister()
|
470
|
+
|
471
|
+
|
472
|
+
# 便捷函数
|
473
|
+
def list_keywords(output_format: str = 'json',
|
474
|
+
name_filter: Optional[str] = None,
|
475
|
+
category_filter: str = 'all',
|
476
|
+
include_remote: bool = False,
|
477
|
+
output_file: Optional[str] = None,
|
478
|
+
print_summary: bool = True) -> Union[str, Dict[str, Any], None]:
|
479
|
+
"""列出关键字的便捷函数
|
480
|
+
|
481
|
+
Args:
|
482
|
+
output_format: 输出格式 ('text', 'json', 'html')
|
483
|
+
name_filter: 名称过滤器(支持部分匹配)
|
484
|
+
category_filter: 类别过滤器
|
485
|
+
include_remote: 是否包含远程关键字
|
486
|
+
output_file: 输出文件路径(可选)
|
487
|
+
print_summary: 是否打印摘要信息
|
488
|
+
|
489
|
+
Returns:
|
490
|
+
根据输出格式返回相应的数据,如果输出到文件则返回None
|
491
|
+
"""
|
492
|
+
options = KeywordListOptions(
|
493
|
+
output_format=output_format,
|
494
|
+
name_filter=name_filter,
|
495
|
+
category_filter=category_filter,
|
496
|
+
include_remote=include_remote,
|
497
|
+
output_file=output_file
|
498
|
+
)
|
499
|
+
|
500
|
+
# 获取数据
|
501
|
+
result = keyword_lister.list_keywords(options)
|
502
|
+
|
503
|
+
if isinstance(result, str):
|
504
|
+
# 文本格式
|
505
|
+
if output_file:
|
506
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
507
|
+
f.write(result)
|
508
|
+
if print_summary:
|
509
|
+
print(f"关键字信息已保存到文件: {output_file}")
|
510
|
+
return None
|
511
|
+
else:
|
512
|
+
return result
|
513
|
+
|
514
|
+
elif isinstance(result, dict):
|
515
|
+
# JSON或HTML格式
|
516
|
+
if output_format == 'json':
|
517
|
+
json_output = json.dumps(result, ensure_ascii=False, indent=2)
|
518
|
+
|
519
|
+
if output_file:
|
520
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
521
|
+
f.write(json_output)
|
522
|
+
if print_summary:
|
523
|
+
_print_json_summary(result, output_file)
|
524
|
+
return None
|
525
|
+
else:
|
526
|
+
return result
|
527
|
+
|
528
|
+
elif output_format == 'html':
|
529
|
+
if not output_file:
|
530
|
+
output_file = 'keywords.html'
|
531
|
+
|
532
|
+
try:
|
533
|
+
generate_html_report(result, output_file)
|
534
|
+
if print_summary:
|
535
|
+
_print_json_summary(result, output_file, is_html=True)
|
536
|
+
return None
|
537
|
+
except Exception as e:
|
538
|
+
if print_summary:
|
539
|
+
print(f"生成HTML报告失败: {e}")
|
540
|
+
raise
|
541
|
+
|
542
|
+
return result
|
543
|
+
|
544
|
+
|
545
|
+
def _print_json_summary(keywords_data: Dict[str, Any],
|
546
|
+
output_file: str, is_html: bool = False):
|
547
|
+
"""打印JSON数据的摘要信息"""
|
548
|
+
summary = keywords_data['summary']
|
549
|
+
total_count = summary['total_count']
|
550
|
+
category_counts = summary['category_counts']
|
551
|
+
|
552
|
+
if is_html:
|
553
|
+
print(f"HTML报告已生成: {output_file}")
|
554
|
+
else:
|
555
|
+
print(f"关键字信息已保存到文件: {output_file}")
|
556
|
+
|
557
|
+
print(f"共 {total_count} 个关键字")
|
558
|
+
|
559
|
+
category_names = {
|
560
|
+
'builtin': '内置',
|
561
|
+
'plugin': '插件',
|
562
|
+
'custom': '自定义',
|
563
|
+
'project_custom': '项目自定义',
|
564
|
+
'remote': '远程'
|
565
|
+
}
|
566
|
+
|
567
|
+
for cat, count in category_counts.items():
|
568
|
+
cat_display = category_names.get(cat, cat)
|
569
|
+
print(f" {cat_display}: {count} 个")
|
570
|
+
|
571
|
+
|
572
|
+
def get_keyword_info(keyword_name: str,
|
573
|
+
include_remote: bool = False) -> Optional[KeywordInfo]:
|
574
|
+
"""获取单个关键字的详细信息
|
575
|
+
|
576
|
+
Args:
|
577
|
+
keyword_name: 关键字名称
|
578
|
+
include_remote: 是否包含远程关键字
|
579
|
+
|
580
|
+
Returns:
|
581
|
+
关键字信息对象,如果未找到则返回None
|
582
|
+
"""
|
583
|
+
# 加载关键字
|
584
|
+
project_custom_keywords = load_all_keywords(include_remote=include_remote)
|
585
|
+
|
586
|
+
# 获取关键字信息
|
587
|
+
keyword_info = keyword_manager.get_keyword_info(keyword_name)
|
588
|
+
if not keyword_info:
|
589
|
+
return None
|
590
|
+
|
591
|
+
return KeywordInfo(keyword_name, keyword_info, project_custom_keywords)
|
592
|
+
|
593
|
+
|
594
|
+
def search_keywords(pattern: str,
|
595
|
+
include_remote: bool = False) -> List[KeywordInfo]:
|
596
|
+
"""搜索匹配模式的关键字
|
597
|
+
|
598
|
+
Args:
|
599
|
+
pattern: 搜索模式(支持部分匹配)
|
600
|
+
include_remote: 是否包含远程关键字
|
601
|
+
|
602
|
+
Returns:
|
603
|
+
匹配的关键字信息列表
|
604
|
+
"""
|
605
|
+
options = KeywordListOptions(
|
606
|
+
name_filter=pattern,
|
607
|
+
include_remote=include_remote
|
608
|
+
)
|
609
|
+
return keyword_lister.get_keywords(options)
|
pytest_dsl/core/lexer.py
CHANGED
@@ -90,6 +90,7 @@ t_PLACEHOLDER = (r'\$\{[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
|
|
90
90
|
# 添加管道符的正则表达式定义
|
91
91
|
t_PIPE = r'\|'
|
92
92
|
|
93
|
+
|
93
94
|
# 添加@remote关键字的token规则
|
94
95
|
def t_REMOTE_KEYWORD(t):
|
95
96
|
r'@remote'
|
@@ -111,7 +112,14 @@ def t_STRING(t):
|
|
111
112
|
r"""(\'\'\'[\s\S]*?\'\'\'|\"\"\"[\s\S]*?\"\"\"|'[^']*'|\"[^\"]*\")"""
|
112
113
|
# 处理单引号和双引号的多行/单行字符串
|
113
114
|
if t.value.startswith("'''") or t.value.startswith('"""'):
|
115
|
+
# 对于多行字符串,需要正确更新行号
|
116
|
+
original_value = t.value
|
114
117
|
t.value = t.value[3:-3] # 去掉三引号
|
118
|
+
|
119
|
+
# 计算多行字符串包含的换行符数量,更新词法分析器的行号
|
120
|
+
newlines = original_value.count('\n')
|
121
|
+
if newlines > 0:
|
122
|
+
t.lexer.lineno += newlines
|
115
123
|
else:
|
116
124
|
t.value = t.value[1:-1] # 去掉单引号或双引号
|
117
125
|
return t
|
pytest_dsl/core/parser.py
CHANGED
@@ -198,7 +198,12 @@ def p_expr_atom(p):
|
|
198
198
|
elif isinstance(p[1], Node):
|
199
199
|
p[0] = p[1]
|
200
200
|
else:
|
201
|
-
|
201
|
+
# 为基本表达式设置行号信息
|
202
|
+
expr_line = getattr(p.slice[1], 'lineno', None)
|
203
|
+
expr_node = Node('Expression', value=p[1])
|
204
|
+
if expr_line is not None:
|
205
|
+
expr_node.set_position(expr_line)
|
206
|
+
p[0] = expr_node
|
202
207
|
|
203
208
|
|
204
209
|
def p_boolean_expr(p):
|
@@ -265,8 +270,29 @@ def p_keyword_call(p):
|
|
265
270
|
line_number = getattr(p.slice[1], 'lineno', None)
|
266
271
|
|
267
272
|
if len(p) == 6:
|
268
|
-
|
273
|
+
# 对于有参数的关键字调用,尝试获取更精确的行号
|
274
|
+
# 优先使用关键字名称的行号,其次是左括号的行号
|
275
|
+
keyword_line = getattr(p.slice[2], 'lineno', None)
|
276
|
+
if keyword_line is not None:
|
277
|
+
line_number = keyword_line
|
278
|
+
|
279
|
+
keyword_node = Node('KeywordCall', [p[5]], p[2],
|
280
|
+
line_number=line_number)
|
281
|
+
|
282
|
+
# 为参数列表中的每个参数也设置行号信息(如果可用)
|
283
|
+
if p[5] and isinstance(p[5], list):
|
284
|
+
for param in p[5]:
|
285
|
+
if (hasattr(param, 'set_position') and
|
286
|
+
not hasattr(param, 'line_number')):
|
287
|
+
# 如果参数没有行号,使用关键字的行号作为默认值
|
288
|
+
param.set_position(line_number)
|
289
|
+
|
290
|
+
p[0] = keyword_node
|
269
291
|
else:
|
292
|
+
# 对于无参数的关键字调用,也优先使用关键字名称的行号
|
293
|
+
keyword_line = getattr(p.slice[2], 'lineno', None)
|
294
|
+
if keyword_line is not None:
|
295
|
+
line_number = keyword_line
|
270
296
|
p[0] = Node('KeywordCall', [[]], p[2], line_number=line_number)
|
271
297
|
|
272
298
|
|
@@ -286,7 +312,15 @@ def p_parameter_items(p):
|
|
286
312
|
|
287
313
|
def p_parameter_item(p):
|
288
314
|
'''parameter_item : ID COLON expression'''
|
289
|
-
|
315
|
+
# 获取参数名的行号
|
316
|
+
param_line = getattr(p.slice[1], 'lineno', None)
|
317
|
+
param_node = Node('ParameterItem', value=p[1], children=[p[3]])
|
318
|
+
|
319
|
+
# 设置参数节点的行号
|
320
|
+
if param_line is not None:
|
321
|
+
param_node.set_position(param_line)
|
322
|
+
|
323
|
+
p[0] = param_node
|
290
324
|
|
291
325
|
|
292
326
|
def p_teardown(p):
|
@@ -1,5 +1,5 @@
|
|
1
|
-
pytest_dsl/__init__.py,sha256=
|
2
|
-
pytest_dsl/cli.py,sha256=
|
1
|
+
pytest_dsl/__init__.py,sha256=QAn8612I9Fn7SnnuaU34ZiAImZ2sHnuVaLuHi3S_3p4,5254
|
2
|
+
pytest_dsl/cli.py,sha256=bH2z5-Onqpir1ymB-DB-tu-fuI1nWHcwVo_3ve9Hgco,12845
|
3
3
|
pytest_dsl/conftest_adapter.py,sha256=cevEb0oEZKTZfUrGe1-CmkFByxKhUtjuurBJP7kpLc0,149
|
4
4
|
pytest_dsl/main_adapter.py,sha256=pUIPN_EzY3JCDlYK7yF_OeLDVqni8vtG15G7gVzPJXg,181
|
5
5
|
pytest_dsl/plugin.py,sha256=CEwi-ci2rMevaAl9PwBw2WKXWRbXuHI1IkkDV0I0VIo,2224
|
@@ -9,7 +9,7 @@ pytest_dsl/core/auto_decorator.py,sha256=9Mga-GB4AzV5nkB6zpfaq8IuHa0KOH8LlFvnWyH
|
|
9
9
|
pytest_dsl/core/auto_directory.py,sha256=egyTnVxtGs4P75EIivRauLRPJfN9aZpoGVvp_Ma72AM,2714
|
10
10
|
pytest_dsl/core/context.py,sha256=fXpVQon666Lz_PCexjkWMBBr-Xcd1SkDMp2vHar6eIs,664
|
11
11
|
pytest_dsl/core/custom_keyword_manager.py,sha256=F9aSbth4x4-5nHb0N5uG04CpgwGnQv4RDGeRKR2WuIk,16001
|
12
|
-
pytest_dsl/core/dsl_executor.py,sha256=
|
12
|
+
pytest_dsl/core/dsl_executor.py,sha256=aDK7rXbE5xLg4DcQ5VMbexSBuh4YGMDrVqtXENFCbsw,48638
|
13
13
|
pytest_dsl/core/dsl_executor_utils.py,sha256=ZJLSYSsiKHqg53d3Bl--ZF8m9abd5kpqdevWl31KEZg,2276
|
14
14
|
pytest_dsl/core/execution_tracker.py,sha256=Pwcxraxt_xkOouq32KBqola-OVfnbaomCoMTyUIqoN4,9476
|
15
15
|
pytest_dsl/core/global_context.py,sha256=NcEcS2V61MT70tgAsGsFWQq0P3mKjtHQr1rgT3yTcyY,3535
|
@@ -21,8 +21,9 @@ pytest_dsl/core/http_client.py,sha256=hdx8gI4JCmq1-96pbiKeyKzSQUzPAi08cRNmljiPQm
|
|
21
21
|
pytest_dsl/core/http_request.py,sha256=6e-gTztH3wu2eSW27Nc0uPmyWjB6oBwndx8Vqnu5uyg,60030
|
22
22
|
pytest_dsl/core/keyword_loader.py,sha256=3GQ4w5Zf2XdkoJ85uYXh9YB93_8L8OAb7vvuKE3-gVA,13864
|
23
23
|
pytest_dsl/core/keyword_manager.py,sha256=5WZWwlYk74kGHh1T6WjCuVd8eelq2-UEXvDIe4U7rEI,7730
|
24
|
-
pytest_dsl/core/
|
25
|
-
pytest_dsl/core/
|
24
|
+
pytest_dsl/core/keyword_utils.py,sha256=1zIKzbA8Lhifc97skzN4oJV-2Cljzf9aVSutwjU7LaA,19847
|
25
|
+
pytest_dsl/core/lexer.py,sha256=o_EJIadfhgyCImI73Y9ybqlBE9AisgA6nOhxpXNlaMw,4648
|
26
|
+
pytest_dsl/core/parser.py,sha256=LCyFNwrIO0dEup7HubnCkceAPqY7Qt5aoSKQddqG7Os,15898
|
26
27
|
pytest_dsl/core/parsetab.py,sha256=o4XbFKwpsi3fYmfI_F6u5NSM61Qp6gTx-Sfh1jDINxI,31767
|
27
28
|
pytest_dsl/core/plugin_discovery.py,sha256=3pt3EXJ7EPF0rkUlyDZMVHkIiTy2vicdIIQJkrHXZjY,8305
|
28
29
|
pytest_dsl/core/utils.py,sha256=q0AMdw5HO33JvlA3UJeQ7IXh1Z_8K1QlwiM1_yiITrU,5350
|
@@ -72,9 +73,9 @@ pytest_dsl/remote/keyword_client.py,sha256=BL80MOaLroUi0v-9sLtkJ55g1R0Iw9SE1k11I
|
|
72
73
|
pytest_dsl/remote/keyword_server.py,sha256=vGIE3Bhh461xX_u1U-Cf5nrWL2GQFYdtQdcMWfFIYgE,22320
|
73
74
|
pytest_dsl/remote/variable_bridge.py,sha256=dv-d3Gq9ttvvrXM1fdlLtoSOPB6vRp0_GBOwX4wvcy8,7121
|
74
75
|
pytest_dsl/templates/keywords_report.html,sha256=7x84iq6hi08nf1iQ95jZ3izcAUPx6JFm0_8xS85CYws,31241
|
75
|
-
pytest_dsl-0.15.
|
76
|
-
pytest_dsl-0.15.
|
77
|
-
pytest_dsl-0.15.
|
78
|
-
pytest_dsl-0.15.
|
79
|
-
pytest_dsl-0.15.
|
80
|
-
pytest_dsl-0.15.
|
76
|
+
pytest_dsl-0.15.1.dist-info/licenses/LICENSE,sha256=Rguy8cb9sYhK6cmrBdXvwh94rKVDh2tVZEWptsHIsVM,1071
|
77
|
+
pytest_dsl-0.15.1.dist-info/METADATA,sha256=tBZyIhox0tvZCCVK2aaP2EIUNyawij9coLH0NFn8PUo,29655
|
78
|
+
pytest_dsl-0.15.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
79
|
+
pytest_dsl-0.15.1.dist-info/entry_points.txt,sha256=PLOBbH02OGY1XR1JDKIZB1Em87loUvbgMRWaag-5FhY,204
|
80
|
+
pytest_dsl-0.15.1.dist-info/top_level.txt,sha256=4CrSx4uNqxj7NvK6k1y2JZrSrJSzi-UvPZdqpUhumWM,11
|
81
|
+
pytest_dsl-0.15.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|