pytest-dsl 0.13.0__py3-none-any.whl → 0.15.0__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 +190 -3
- pytest_dsl/cli.py +37 -260
- pytest_dsl/core/custom_keyword_manager.py +114 -14
- pytest_dsl/core/dsl_executor.py +549 -166
- pytest_dsl/core/dsl_executor_utils.py +21 -22
- pytest_dsl/core/execution_tracker.py +291 -0
- pytest_dsl/core/hook_manager.py +87 -0
- pytest_dsl/core/hookable_executor.py +134 -0
- pytest_dsl/core/hookable_keyword_manager.py +106 -0
- pytest_dsl/core/hookspecs.py +175 -0
- pytest_dsl/core/keyword_loader.py +402 -0
- pytest_dsl/core/keyword_manager.py +29 -23
- pytest_dsl/core/parser.py +94 -18
- pytest_dsl/core/validator.py +417 -0
- pytest_dsl/core/yaml_loader.py +142 -42
- pytest_dsl/core/yaml_vars.py +90 -7
- pytest_dsl/plugin.py +10 -1
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/METADATA +1 -1
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/RECORD +23 -16
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/top_level.txt +0 -0
pytest_dsl/core/dsl_executor.py
CHANGED
@@ -2,15 +2,15 @@ import re
|
|
2
2
|
import allure
|
3
3
|
import csv
|
4
4
|
import os
|
5
|
-
import
|
6
|
-
from pytest_dsl.core.
|
7
|
-
from pytest_dsl.core.parser import get_parser, Node
|
5
|
+
from typing import Dict, Any
|
6
|
+
from pytest_dsl.core.parser import Node
|
8
7
|
from pytest_dsl.core.keyword_manager import keyword_manager
|
9
8
|
from pytest_dsl.core.global_context import global_context
|
10
9
|
from pytest_dsl.core.context import TestContext
|
11
|
-
import pytest_dsl.keywords
|
12
|
-
from pytest_dsl.core.yaml_vars import yaml_vars
|
13
10
|
from pytest_dsl.core.variable_utils import VariableReplacer
|
11
|
+
from pytest_dsl.core.execution_tracker import (
|
12
|
+
get_or_create_tracker, ExecutionTracker
|
13
|
+
)
|
14
14
|
|
15
15
|
|
16
16
|
class BreakException(Exception):
|
@@ -39,8 +39,14 @@ class DSLExecutor:
|
|
39
39
|
- PYTEST_DSL_KEEP_VARIABLES=0: (默认) 执行完成后清空变量,用于正常DSL执行
|
40
40
|
"""
|
41
41
|
|
42
|
-
def __init__(self
|
43
|
-
|
42
|
+
def __init__(self, enable_hooks: bool = True,
|
43
|
+
enable_tracking: bool = True):
|
44
|
+
"""初始化DSL执行器
|
45
|
+
|
46
|
+
Args:
|
47
|
+
enable_hooks: 是否启用hook机制,默认True
|
48
|
+
enable_tracking: 是否启用执行跟踪,默认True
|
49
|
+
"""
|
44
50
|
self.variables = {}
|
45
51
|
self.test_context = TestContext()
|
46
52
|
self.test_context.executor = self # 让 test_context 能够访问到 executor
|
@@ -48,6 +54,17 @@ class DSLExecutor:
|
|
48
54
|
self.variables, self.test_context)
|
49
55
|
self.imported_files = set() # 跟踪已导入的文件,避免循环导入
|
50
56
|
|
57
|
+
# Hook相关配置
|
58
|
+
self.enable_hooks = enable_hooks
|
59
|
+
self.current_dsl_id = None # 当前执行的DSL标识符
|
60
|
+
|
61
|
+
# 执行跟踪配置
|
62
|
+
self.enable_tracking = enable_tracking
|
63
|
+
self.execution_tracker: ExecutionTracker = None
|
64
|
+
|
65
|
+
if self.enable_hooks:
|
66
|
+
self._init_hooks()
|
67
|
+
|
51
68
|
def set_current_data(self, data):
|
52
69
|
"""设置当前测试数据集"""
|
53
70
|
if data:
|
@@ -141,7 +158,11 @@ class DSLExecutor:
|
|
141
158
|
return self.variable_replacer.local_variables[value]
|
142
159
|
|
143
160
|
# 定义扩展的变量引用模式,支持数组索引和字典键访问
|
144
|
-
pattern =
|
161
|
+
pattern = (
|
162
|
+
r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
|
163
|
+
r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
|
164
|
+
r'|(?:\[[^\]]+\]))*)\}'
|
165
|
+
)
|
145
166
|
# 检查整个字符串是否完全匹配单一变量引用模式
|
146
167
|
match = re.fullmatch(pattern, value)
|
147
168
|
if match:
|
@@ -198,13 +219,15 @@ class DSLExecutor:
|
|
198
219
|
operator = expr_node.value # 操作符: +, -, *, /, %
|
199
220
|
|
200
221
|
# 尝试类型转换 - 如果是字符串数字则转为数字
|
201
|
-
if isinstance(left_value, str) and
|
222
|
+
if (isinstance(left_value, str) and
|
223
|
+
str(left_value).replace('.', '', 1).isdigit()):
|
202
224
|
left_value = float(left_value)
|
203
225
|
# 如果是整数则转为整数
|
204
226
|
if left_value.is_integer():
|
205
227
|
left_value = int(left_value)
|
206
228
|
|
207
|
-
if isinstance(right_value, str) and
|
229
|
+
if (isinstance(right_value, str) and
|
230
|
+
str(right_value).replace('.', '', 1).isdigit()):
|
208
231
|
right_value = float(right_value)
|
209
232
|
# 如果是整数则转为整数
|
210
233
|
if right_value.is_integer():
|
@@ -220,9 +243,11 @@ class DSLExecutor:
|
|
220
243
|
return left_value - right_value
|
221
244
|
elif operator == '*':
|
222
245
|
# 如果其中一个是字符串,另一个是数字,则进行字符串重复
|
223
|
-
if isinstance(left_value, str) and
|
246
|
+
if (isinstance(left_value, str) and
|
247
|
+
isinstance(right_value, (int, float))):
|
224
248
|
return left_value * int(right_value)
|
225
|
-
elif isinstance(right_value, str) and
|
249
|
+
elif (isinstance(right_value, str) and
|
250
|
+
isinstance(left_value, (int, float))):
|
226
251
|
return right_value * int(left_value)
|
227
252
|
return left_value * right_value
|
228
253
|
elif operator == '/':
|
@@ -287,7 +312,8 @@ class DSLExecutor:
|
|
287
312
|
for stmt in statements_node.children:
|
288
313
|
if stmt.type == 'CustomKeyword':
|
289
314
|
# 导入自定义关键字管理器
|
290
|
-
from pytest_dsl.core.custom_keyword_manager import
|
315
|
+
from pytest_dsl.core.custom_keyword_manager import (
|
316
|
+
custom_keyword_manager)
|
291
317
|
# 注册自定义关键字
|
292
318
|
custom_keyword_manager._register_custom_keyword(
|
293
319
|
stmt, "current_file")
|
@@ -317,7 +343,7 @@ class DSLExecutor:
|
|
317
343
|
elif child.type == 'Teardown':
|
318
344
|
teardown_node = child
|
319
345
|
|
320
|
-
|
346
|
+
# 在_execute_test_iteration之前添加
|
321
347
|
self._handle_custom_keywords_in_file(node)
|
322
348
|
# 执行测试
|
323
349
|
self._execute_test_iteration(metadata, node, teardown_node)
|
@@ -339,8 +365,34 @@ class DSLExecutor:
|
|
339
365
|
|
340
366
|
def _auto_import_resources(self):
|
341
367
|
"""自动导入项目中的resources目录"""
|
368
|
+
# 首先尝试通过hook获取资源列表
|
369
|
+
if (self.enable_hooks and hasattr(self, 'hook_manager') and
|
370
|
+
self.hook_manager):
|
371
|
+
try:
|
372
|
+
cases = []
|
373
|
+
case_results = self.hook_manager.pm.hook.dsl_list_cases()
|
374
|
+
for result in case_results:
|
375
|
+
if result:
|
376
|
+
cases.extend(result)
|
377
|
+
|
378
|
+
# 如果hook返回了资源,导入它们
|
379
|
+
for case in cases:
|
380
|
+
case_id = case.get('id') or case.get('file_path', '')
|
381
|
+
if case_id and case_id not in self.imported_files:
|
382
|
+
try:
|
383
|
+
print(f"通过hook自动导入资源: {case_id}")
|
384
|
+
self._handle_import(case_id)
|
385
|
+
except Exception as e:
|
386
|
+
print(f"通过hook自动导入资源失败: {case_id}, 错误: {str(e)}")
|
387
|
+
continue
|
388
|
+
except Exception as e:
|
389
|
+
print(f"通过hook自动导入资源时出现警告: {str(e)}")
|
390
|
+
|
391
|
+
# 然后进行传统的文件系统自动导入
|
342
392
|
try:
|
343
|
-
from pytest_dsl.core.custom_keyword_manager import
|
393
|
+
from pytest_dsl.core.custom_keyword_manager import (
|
394
|
+
custom_keyword_manager
|
395
|
+
)
|
344
396
|
|
345
397
|
# 尝试从多个可能的项目根目录位置导入resources
|
346
398
|
possible_roots = [
|
@@ -355,14 +407,15 @@ class DSLExecutor:
|
|
355
407
|
pytest_root = pytest.config.rootdir
|
356
408
|
if pytest_root:
|
357
409
|
possible_roots.insert(0, str(pytest_root))
|
358
|
-
except:
|
410
|
+
except Exception:
|
359
411
|
pass
|
360
412
|
|
361
413
|
# 尝试每个可能的根目录
|
362
414
|
for project_root in possible_roots:
|
363
415
|
if project_root and os.path.exists(project_root):
|
364
416
|
resources_dir = os.path.join(project_root, "resources")
|
365
|
-
if os.path.exists(resources_dir) and
|
417
|
+
if (os.path.exists(resources_dir) and
|
418
|
+
os.path.isdir(resources_dir)):
|
366
419
|
custom_keyword_manager.auto_import_resources_directory(
|
367
420
|
project_root)
|
368
421
|
break
|
@@ -382,10 +435,34 @@ class DSLExecutor:
|
|
382
435
|
return
|
383
436
|
|
384
437
|
try:
|
385
|
-
#
|
386
|
-
|
387
|
-
|
388
|
-
|
438
|
+
# 尝试通过hook加载内容
|
439
|
+
content = None
|
440
|
+
if (self.enable_hooks and hasattr(self, 'hook_manager') and
|
441
|
+
self.hook_manager):
|
442
|
+
content_results = (
|
443
|
+
self.hook_manager.pm.hook.dsl_load_content(
|
444
|
+
dsl_id=file_path
|
445
|
+
)
|
446
|
+
)
|
447
|
+
for result in content_results:
|
448
|
+
if result is not None:
|
449
|
+
content = result
|
450
|
+
break
|
451
|
+
|
452
|
+
# 如果hook返回了内容,直接使用DSL解析方式处理
|
453
|
+
if content is not None:
|
454
|
+
ast = self._parse_dsl_content(content)
|
455
|
+
|
456
|
+
# 只处理自定义关键字,不执行测试流程
|
457
|
+
self._handle_custom_keywords_in_file(ast)
|
458
|
+
self.imported_files.add(file_path)
|
459
|
+
else:
|
460
|
+
# 使用传统方式导入文件
|
461
|
+
from pytest_dsl.core.custom_keyword_manager import (
|
462
|
+
custom_keyword_manager
|
463
|
+
)
|
464
|
+
custom_keyword_manager.load_resource_file(file_path)
|
465
|
+
self.imported_files.add(file_path)
|
389
466
|
except Exception as e:
|
390
467
|
print(f"导入资源文件失败: {file_path}, 错误: {str(e)}")
|
391
468
|
raise
|
@@ -442,133 +519,227 @@ class DSLExecutor:
|
|
442
519
|
# 将return异常向上传递,不在这里处理
|
443
520
|
raise e
|
444
521
|
|
445
|
-
@allure.step("变量赋值")
|
446
522
|
def _handle_assignment(self, node):
|
447
523
|
"""处理赋值语句"""
|
448
|
-
|
449
|
-
|
524
|
+
step_name = f"变量赋值: {node.value}"
|
525
|
+
line_info = (f"\n行号: {node.line_number}"
|
526
|
+
if hasattr(node, 'line_number') and node.line_number
|
527
|
+
else "")
|
450
528
|
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
529
|
+
with allure.step(step_name):
|
530
|
+
try:
|
531
|
+
var_name = node.value
|
532
|
+
expr_value = self.eval_expression(node.children[0])
|
533
|
+
|
534
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
535
|
+
if var_name.startswith('g_'):
|
536
|
+
global_context.set_variable(var_name, expr_value)
|
537
|
+
# 记录全局变量赋值,包含行号信息
|
538
|
+
allure.attach(
|
539
|
+
f"全局变量: {var_name}\n值: {expr_value}{line_info}",
|
540
|
+
name="全局变量赋值",
|
541
|
+
attachment_type=allure.attachment_type.TEXT
|
542
|
+
)
|
543
|
+
else:
|
544
|
+
# 存储在本地变量字典和测试上下文中
|
545
|
+
self.variable_replacer.local_variables[
|
546
|
+
var_name] = expr_value
|
547
|
+
self.test_context.set(var_name, expr_value)
|
548
|
+
# 记录变量赋值,包含行号信息
|
549
|
+
allure.attach(
|
550
|
+
f"变量: {var_name}\n值: {expr_value}{line_info}",
|
551
|
+
name="赋值详情",
|
552
|
+
attachment_type=allure.attachment_type.TEXT
|
553
|
+
)
|
554
|
+
except Exception as e:
|
555
|
+
# 记录赋值失败,包含行号信息
|
556
|
+
allure.attach(
|
557
|
+
f"变量: {var_name}\n错误: {str(e)}{line_info}",
|
558
|
+
name="赋值失败",
|
559
|
+
attachment_type=allure.attachment_type.TEXT
|
560
|
+
)
|
561
|
+
raise
|
468
562
|
|
469
|
-
@allure.step("关键字调用赋值")
|
470
563
|
def _handle_assignment_keyword_call(self, node):
|
471
564
|
"""处理关键字调用赋值"""
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
if
|
486
|
-
|
565
|
+
step_name = f"关键字调用赋值: {node.value}"
|
566
|
+
line_info = (f"\n行号: {node.line_number}"
|
567
|
+
if hasattr(node, 'line_number') and node.line_number
|
568
|
+
else "")
|
569
|
+
|
570
|
+
with allure.step(step_name):
|
571
|
+
try:
|
572
|
+
var_name = node.value
|
573
|
+
keyword_call_node = node.children[0]
|
574
|
+
result = self.execute(keyword_call_node)
|
575
|
+
|
576
|
+
if result is not None:
|
577
|
+
# 处理新的统一返回格式(支持远程关键字模式)
|
578
|
+
if isinstance(result, dict) and 'result' in result:
|
579
|
+
# 提取主要返回值
|
580
|
+
main_result = result['result']
|
581
|
+
|
582
|
+
# 处理captures字段中的变量
|
583
|
+
captures = result.get('captures', {})
|
584
|
+
for capture_var, capture_value in captures.items():
|
585
|
+
if capture_var.startswith('g_'):
|
586
|
+
global_context.set_variable(
|
587
|
+
capture_var, capture_value)
|
588
|
+
else:
|
589
|
+
self.variable_replacer.local_variables[
|
590
|
+
capture_var] = capture_value
|
591
|
+
self.test_context.set(
|
592
|
+
capture_var, capture_value)
|
593
|
+
|
594
|
+
# 将主要结果赋值给指定变量
|
595
|
+
actual_result = main_result
|
487
596
|
else:
|
488
|
-
|
489
|
-
|
597
|
+
# 传统格式,直接使用结果
|
598
|
+
actual_result = result
|
490
599
|
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
600
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
601
|
+
if var_name.startswith('g_'):
|
602
|
+
global_context.set_variable(var_name, actual_result)
|
603
|
+
allure.attach(
|
604
|
+
f"全局变量: {var_name}\n值: {actual_result}"
|
605
|
+
f"{line_info}",
|
606
|
+
name="关键字调用赋值",
|
607
|
+
attachment_type=allure.attachment_type.TEXT
|
608
|
+
)
|
609
|
+
else:
|
610
|
+
# 存储在本地变量字典和测试上下文中
|
611
|
+
self.variable_replacer.local_variables[
|
612
|
+
var_name] = actual_result
|
613
|
+
self.test_context.set(var_name, actual_result)
|
614
|
+
allure.attach(
|
615
|
+
f"变量: {var_name}\n值: {actual_result}"
|
616
|
+
f"{line_info}",
|
617
|
+
name="关键字调用赋值",
|
618
|
+
attachment_type=allure.attachment_type.TEXT
|
619
|
+
)
|
620
|
+
else:
|
621
|
+
error_msg = f"关键字 {keyword_call_node.value} 没有返回结果"
|
622
|
+
allure.attach(
|
623
|
+
f"变量: {var_name}\n错误: {error_msg}{line_info}",
|
624
|
+
name="关键字调用赋值失败",
|
625
|
+
attachment_type=allure.attachment_type.TEXT
|
626
|
+
)
|
627
|
+
raise Exception(error_msg)
|
628
|
+
except Exception as e:
|
629
|
+
# 如果异常不是我们刚才抛出的,记录异常信息
|
630
|
+
if "没有返回结果" not in str(e):
|
631
|
+
allure.attach(
|
632
|
+
f"变量: {var_name}\n错误: {str(e)}{line_info}",
|
633
|
+
name="关键字调用赋值失败",
|
634
|
+
attachment_type=allure.attachment_type.TEXT
|
635
|
+
)
|
636
|
+
raise
|
637
|
+
|
638
|
+
def _handle_for_loop(self, node):
|
639
|
+
"""处理for循环"""
|
640
|
+
step_name = f"For循环: {node.value}"
|
641
|
+
line_info = (f"\n行号: {node.line_number}"
|
642
|
+
if hasattr(node, 'line_number') and node.line_number
|
643
|
+
else "")
|
496
644
|
|
497
|
-
|
498
|
-
|
499
|
-
|
645
|
+
with allure.step(step_name):
|
646
|
+
try:
|
647
|
+
var_name = node.value
|
648
|
+
start = self.eval_expression(node.children[0])
|
649
|
+
end = self.eval_expression(node.children[1])
|
650
|
+
|
651
|
+
# 记录循环信息,包含行号
|
500
652
|
allure.attach(
|
501
|
-
f"
|
502
|
-
name="
|
653
|
+
f"循环变量: {var_name}\n范围: {start} 到 {end}{line_info}",
|
654
|
+
name="For循环",
|
503
655
|
attachment_type=allure.attachment_type.TEXT
|
504
656
|
)
|
505
|
-
|
506
|
-
#
|
507
|
-
self.variable_replacer.local_variables[var_name] = actual_result
|
508
|
-
self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
|
657
|
+
except Exception as e:
|
658
|
+
# 记录循环初始化失败
|
509
659
|
allure.attach(
|
510
|
-
f"
|
511
|
-
name="
|
660
|
+
f"循环变量: {var_name}\n错误: {str(e)}{line_info}",
|
661
|
+
name="For循环初始化失败",
|
512
662
|
attachment_type=allure.attachment_type.TEXT
|
513
663
|
)
|
514
|
-
|
515
|
-
raise Exception(f"关键字 {keyword_call_node.value} 没有返回结果")
|
664
|
+
raise
|
516
665
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
# 遇到return语句,将异常向上传递
|
549
|
-
allure.attach(
|
550
|
-
f"在 {var_name} = {i} 时遇到return语句,退出函数",
|
551
|
-
name="循环Return",
|
552
|
-
attachment_type=allure.attachment_type.TEXT
|
553
|
-
)
|
554
|
-
raise e
|
666
|
+
for i in range(int(start), int(end)):
|
667
|
+
# 存储在本地变量字典和测试上下文中
|
668
|
+
self.variable_replacer.local_variables[var_name] = i
|
669
|
+
self.test_context.set(var_name, i)
|
670
|
+
with allure.step(f"循环轮次: {var_name} = {i}"):
|
671
|
+
try:
|
672
|
+
self.execute(node.children[2])
|
673
|
+
except BreakException:
|
674
|
+
# 遇到break语句,退出循环
|
675
|
+
allure.attach(
|
676
|
+
f"在 {var_name} = {i} 时遇到break语句,退出循环",
|
677
|
+
name="循环Break",
|
678
|
+
attachment_type=allure.attachment_type.TEXT
|
679
|
+
)
|
680
|
+
break
|
681
|
+
except ContinueException:
|
682
|
+
# 遇到continue语句,跳过本次循环
|
683
|
+
allure.attach(
|
684
|
+
f"在 {var_name} = {i} 时遇到continue语句,跳过本次循环",
|
685
|
+
name="循环Continue",
|
686
|
+
attachment_type=allure.attachment_type.TEXT
|
687
|
+
)
|
688
|
+
continue
|
689
|
+
except ReturnException as e:
|
690
|
+
# 遇到return语句,将异常向上传递
|
691
|
+
allure.attach(
|
692
|
+
f"在 {var_name} = {i} 时遇到return语句,退出函数",
|
693
|
+
name="循环Return",
|
694
|
+
attachment_type=allure.attachment_type.TEXT
|
695
|
+
)
|
696
|
+
raise e
|
555
697
|
|
556
698
|
def _execute_keyword_call(self, node):
|
557
699
|
"""执行关键字调用"""
|
558
700
|
keyword_name = node.value
|
701
|
+
line_info = (f"\n行号: {node.line_number}"
|
702
|
+
if hasattr(node, 'line_number') and node.line_number
|
703
|
+
else "")
|
704
|
+
|
705
|
+
# 先检查关键字是否存在
|
559
706
|
keyword_info = keyword_manager.get_keyword_info(keyword_name)
|
560
707
|
if not keyword_info:
|
561
|
-
|
708
|
+
error_msg = f"未注册的关键字: {keyword_name}"
|
709
|
+
allure.attach(
|
710
|
+
f"关键字: {keyword_name}\n错误: {error_msg}{line_info}",
|
711
|
+
name="关键字调用失败",
|
712
|
+
attachment_type=allure.attachment_type.TEXT
|
713
|
+
)
|
714
|
+
raise Exception(error_msg)
|
562
715
|
|
563
716
|
kwargs = self._prepare_keyword_params(node, keyword_info)
|
717
|
+
step_name = f"调用关键字: {keyword_name}"
|
564
718
|
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
719
|
+
with allure.step(step_name):
|
720
|
+
try:
|
721
|
+
# 传递自定义步骤名称给KeywordManager,避免重复的allure步骤嵌套
|
722
|
+
kwargs['step_name'] = keyword_name # 内层步骤只显示关键字名称
|
723
|
+
# 避免KeywordManager重复记录,由DSL执行器统一记录
|
724
|
+
kwargs['skip_logging'] = True
|
725
|
+
result = keyword_manager.execute(keyword_name, **kwargs)
|
726
|
+
|
727
|
+
# 执行成功后记录关键字信息,包含行号
|
728
|
+
allure.attach(
|
729
|
+
f"关键字: {keyword_name}\n执行结果: 成功{line_info}",
|
730
|
+
name="关键字调用",
|
731
|
+
attachment_type=allure.attachment_type.TEXT
|
732
|
+
)
|
733
|
+
|
734
|
+
return result
|
735
|
+
except Exception as e:
|
736
|
+
# 记录关键字执行失败,包含行号信息
|
737
|
+
allure.attach(
|
738
|
+
f"关键字: {keyword_name}\n错误: {str(e)}{line_info}",
|
739
|
+
name="关键字调用失败",
|
740
|
+
attachment_type=allure.attachment_type.TEXT
|
741
|
+
)
|
742
|
+
raise
|
572
743
|
|
573
744
|
def _prepare_keyword_params(self, node, keyword_info):
|
574
745
|
"""准备关键字调用参数"""
|
@@ -681,6 +852,9 @@ class DSLExecutor:
|
|
681
852
|
call_info = node.value
|
682
853
|
alias = call_info['alias']
|
683
854
|
keyword_name = call_info['keyword']
|
855
|
+
line_info = (f"\n行号: {node.line_number}"
|
856
|
+
if hasattr(node, 'line_number') and node.line_number
|
857
|
+
else "")
|
684
858
|
|
685
859
|
# 准备参数
|
686
860
|
params = []
|
@@ -703,7 +877,7 @@ class DSLExecutor:
|
|
703
877
|
alias, keyword_name, **kwargs)
|
704
878
|
allure.attach(
|
705
879
|
f"远程关键字参数: {kwargs}\n"
|
706
|
-
f"远程关键字结果: {result}",
|
880
|
+
f"远程关键字结果: {result}{line_info}",
|
707
881
|
name="远程关键字执行详情",
|
708
882
|
attachment_type=allure.attachment_type.TEXT
|
709
883
|
)
|
@@ -711,7 +885,7 @@ class DSLExecutor:
|
|
711
885
|
except Exception as e:
|
712
886
|
# 记录错误并重新抛出
|
713
887
|
allure.attach(
|
714
|
-
f"远程关键字执行失败: {str(e)}",
|
888
|
+
f"远程关键字执行失败: {str(e)}{line_info}",
|
715
889
|
name="远程关键字错误",
|
716
890
|
attachment_type=allure.attachment_type.TEXT
|
717
891
|
)
|
@@ -724,54 +898,85 @@ class DSLExecutor:
|
|
724
898
|
node: AssignmentRemoteKeywordCall节点
|
725
899
|
"""
|
726
900
|
var_name = node.value
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
if result is not None:
|
731
|
-
# 注意:远程关键字客户端已经处理了新格式的返回值,
|
732
|
-
# 这里接收到的result应该已经是主要返回值,而不是完整的字典格式
|
733
|
-
# 但为了保险起见,我们仍然检查是否为新格式
|
734
|
-
if isinstance(result, dict) and 'result' in result:
|
735
|
-
# 如果仍然是新格式(可能是嵌套的远程调用),提取主要返回值
|
736
|
-
main_result = result['result']
|
737
|
-
|
738
|
-
# 处理captures字段中的变量
|
739
|
-
captures = result.get('captures', {})
|
740
|
-
for capture_var, capture_value in captures.items():
|
741
|
-
if capture_var.startswith('g_'):
|
742
|
-
global_context.set_variable(capture_var, capture_value)
|
743
|
-
else:
|
744
|
-
self.variable_replacer.local_variables[capture_var] = capture_value
|
745
|
-
self.test_context.set(capture_var, capture_value)
|
901
|
+
line_info = (f"\n行号: {node.line_number}"
|
902
|
+
if hasattr(node, 'line_number') and node.line_number
|
903
|
+
else "")
|
746
904
|
|
747
|
-
|
748
|
-
|
905
|
+
try:
|
906
|
+
remote_keyword_call_node = node.children[0]
|
907
|
+
result = self.execute(remote_keyword_call_node)
|
908
|
+
|
909
|
+
if result is not None:
|
910
|
+
# 注意:远程关键字客户端已经处理了新格式的返回值,
|
911
|
+
# 这里接收到的result应该已经是主要返回值,而不是完整的字典格式
|
912
|
+
# 但为了保险起见,我们仍然检查是否为新格式
|
913
|
+
if isinstance(result, dict) and 'result' in result:
|
914
|
+
# 如果仍然是新格式(可能是嵌套的远程调用),提取主要返回值
|
915
|
+
main_result = result['result']
|
916
|
+
|
917
|
+
# 处理captures字段中的变量
|
918
|
+
captures = result.get('captures', {})
|
919
|
+
for capture_var, capture_value in captures.items():
|
920
|
+
if capture_var.startswith('g_'):
|
921
|
+
global_context.set_variable(
|
922
|
+
capture_var, capture_value)
|
923
|
+
else:
|
924
|
+
self.variable_replacer.local_variables[
|
925
|
+
capture_var] = capture_value
|
926
|
+
self.test_context.set(capture_var, capture_value)
|
927
|
+
|
928
|
+
# 将主要结果赋值给指定变量
|
929
|
+
actual_result = main_result
|
930
|
+
else:
|
931
|
+
# 传统格式或已经处理过的格式,直接使用结果
|
932
|
+
actual_result = result
|
933
|
+
|
934
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
935
|
+
if var_name.startswith('g_'):
|
936
|
+
global_context.set_variable(var_name, actual_result)
|
937
|
+
allure.attach(
|
938
|
+
f"全局变量: {var_name}\n值: {actual_result}{line_info}",
|
939
|
+
name="远程关键字赋值",
|
940
|
+
attachment_type=allure.attachment_type.TEXT
|
941
|
+
)
|
942
|
+
else:
|
943
|
+
# 存储在本地变量字典和测试上下文中
|
944
|
+
self.variable_replacer.local_variables[
|
945
|
+
var_name] = actual_result
|
946
|
+
self.test_context.set(var_name, actual_result)
|
947
|
+
allure.attach(
|
948
|
+
f"变量: {var_name}\n值: {actual_result}{line_info}",
|
949
|
+
name="远程关键字赋值",
|
950
|
+
attachment_type=allure.attachment_type.TEXT
|
951
|
+
)
|
749
952
|
else:
|
750
|
-
|
751
|
-
actual_result = result
|
752
|
-
|
753
|
-
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
754
|
-
if var_name.startswith('g_'):
|
755
|
-
global_context.set_variable(var_name, actual_result)
|
953
|
+
error_msg = "远程关键字没有返回结果"
|
756
954
|
allure.attach(
|
757
|
-
f"
|
758
|
-
name="
|
955
|
+
f"变量: {var_name}\n错误: {error_msg}{line_info}",
|
956
|
+
name="远程关键字赋值失败",
|
759
957
|
attachment_type=allure.attachment_type.TEXT
|
760
958
|
)
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
959
|
+
raise Exception(error_msg)
|
960
|
+
except Exception as e:
|
961
|
+
# 如果异常不是我们刚才抛出的,记录异常信息
|
962
|
+
if "没有返回结果" not in str(e):
|
765
963
|
allure.attach(
|
766
|
-
f"变量: {var_name}\n
|
767
|
-
name="
|
964
|
+
f"变量: {var_name}\n错误: {str(e)}{line_info}",
|
965
|
+
name="远程关键字赋值失败",
|
768
966
|
attachment_type=allure.attachment_type.TEXT
|
769
967
|
)
|
770
|
-
|
771
|
-
raise Exception(f"远程关键字没有返回结果")
|
968
|
+
raise
|
772
969
|
|
773
970
|
def execute(self, node):
|
774
971
|
"""执行AST节点"""
|
972
|
+
# 执行跟踪
|
973
|
+
if self.enable_tracking and self.execution_tracker:
|
974
|
+
line_number = getattr(node, 'line_number', None)
|
975
|
+
if line_number:
|
976
|
+
description = self._get_node_description(node)
|
977
|
+
self.execution_tracker.start_step(
|
978
|
+
line_number, node.type, description)
|
979
|
+
|
775
980
|
handlers = {
|
776
981
|
'Start': self._handle_start,
|
777
982
|
'Metadata': lambda _: None,
|
@@ -786,15 +991,193 @@ class DSLExecutor:
|
|
786
991
|
'CustomKeyword': lambda _: None, # 添加对CustomKeyword节点的处理,只需注册不需执行
|
787
992
|
'RemoteImport': self._handle_remote_import,
|
788
993
|
'RemoteKeywordCall': self._execute_remote_keyword_call,
|
789
|
-
'AssignmentRemoteKeywordCall':
|
994
|
+
'AssignmentRemoteKeywordCall': (
|
995
|
+
self._handle_assignment_remote_keyword_call),
|
790
996
|
'Break': self._handle_break,
|
791
997
|
'Continue': self._handle_continue
|
792
998
|
}
|
793
999
|
|
794
1000
|
handler = handlers.get(node.type)
|
795
|
-
if handler:
|
796
|
-
|
797
|
-
|
1001
|
+
if not handler:
|
1002
|
+
error_msg = f"未知的节点类型: {node.type}"
|
1003
|
+
if self.enable_tracking and self.execution_tracker:
|
1004
|
+
self.execution_tracker.finish_current_step(error=error_msg)
|
1005
|
+
raise Exception(error_msg)
|
1006
|
+
|
1007
|
+
try:
|
1008
|
+
result = handler(node)
|
1009
|
+
# 执行成功
|
1010
|
+
if self.enable_tracking and self.execution_tracker:
|
1011
|
+
self.execution_tracker.finish_current_step(result=result)
|
1012
|
+
return result
|
1013
|
+
except Exception as e:
|
1014
|
+
# 执行失败
|
1015
|
+
if self.enable_tracking and self.execution_tracker:
|
1016
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
1017
|
+
if hasattr(node, 'line_number') and node.line_number:
|
1018
|
+
error_msg += f" (行{node.line_number})"
|
1019
|
+
self.execution_tracker.finish_current_step(error=error_msg)
|
1020
|
+
raise
|
1021
|
+
|
1022
|
+
def _get_remote_keyword_description(self, node):
|
1023
|
+
"""获取远程关键字调用的描述"""
|
1024
|
+
if isinstance(getattr(node, 'value', None), dict):
|
1025
|
+
keyword = node.value.get('keyword', '')
|
1026
|
+
return f"调用远程关键字: {keyword}"
|
1027
|
+
return "调用远程关键字"
|
1028
|
+
|
1029
|
+
def _get_node_description(self, node):
|
1030
|
+
"""获取节点的描述信息"""
|
1031
|
+
descriptions = {
|
1032
|
+
'Assignment': f"变量赋值: {getattr(node, 'value', '')}",
|
1033
|
+
'AssignmentKeywordCall': f"关键字赋值: {getattr(node, 'value', '')}",
|
1034
|
+
'AssignmentRemoteKeywordCall': (
|
1035
|
+
f"远程关键字赋值: {getattr(node, 'value', '')}"),
|
1036
|
+
'KeywordCall': f"调用关键字: {getattr(node, 'value', '')}",
|
1037
|
+
'RemoteKeywordCall': self._get_remote_keyword_description(node),
|
1038
|
+
'ForLoop': f"For循环: {getattr(node, 'value', '')}",
|
1039
|
+
'IfStatement': "条件分支",
|
1040
|
+
'Return': "返回语句",
|
1041
|
+
'Break': "Break语句",
|
1042
|
+
'Continue': "Continue语句",
|
1043
|
+
'Teardown': "清理操作",
|
1044
|
+
'Start': "开始执行",
|
1045
|
+
'Statements': "语句块"
|
1046
|
+
}
|
1047
|
+
|
1048
|
+
return descriptions.get(node.type, f"执行{node.type}")
|
1049
|
+
|
1050
|
+
def __repr__(self):
|
1051
|
+
"""返回DSL执行器的字符串表示"""
|
1052
|
+
return (f"DSLExecutor(variables={len(self.variables)}, "
|
1053
|
+
f"hooks_enabled={self.enable_hooks}, "
|
1054
|
+
f"tracking_enabled={self.enable_tracking})")
|
1055
|
+
|
1056
|
+
def _init_hooks(self):
|
1057
|
+
"""初始化hook机制"""
|
1058
|
+
try:
|
1059
|
+
from .hook_manager import hook_manager
|
1060
|
+
hook_manager.initialize()
|
1061
|
+
# 调用hook注册自定义关键字
|
1062
|
+
hook_manager.pm.hook.dsl_register_custom_keywords()
|
1063
|
+
self.hook_manager = hook_manager
|
1064
|
+
except ImportError:
|
1065
|
+
# 如果没有安装pluggy,禁用hook
|
1066
|
+
self.enable_hooks = False
|
1067
|
+
self.hook_manager = None
|
1068
|
+
|
1069
|
+
def execute_from_content(self, content: str, dsl_id: str = None,
|
1070
|
+
context: Dict[str, Any] = None) -> Any:
|
1071
|
+
"""从内容执行DSL,支持hook扩展
|
1072
|
+
|
1073
|
+
Args:
|
1074
|
+
content: DSL内容,如果为空字符串将尝试通过hook加载
|
1075
|
+
dsl_id: DSL标识符(可选)
|
1076
|
+
context: 执行上下文(可选)
|
1077
|
+
|
1078
|
+
Returns:
|
1079
|
+
执行结果
|
1080
|
+
"""
|
1081
|
+
self.current_dsl_id = dsl_id
|
1082
|
+
|
1083
|
+
# 初始化执行跟踪器
|
1084
|
+
if self.enable_tracking:
|
1085
|
+
self.execution_tracker = get_or_create_tracker(dsl_id)
|
1086
|
+
self.execution_tracker.start_execution()
|
1087
|
+
|
1088
|
+
# 如果content为空且有dsl_id,尝试通过hook加载内容
|
1089
|
+
if (not content and dsl_id and self.enable_hooks and
|
1090
|
+
hasattr(self, 'hook_manager') and self.hook_manager):
|
1091
|
+
content_results = self.hook_manager.pm.hook.dsl_load_content(
|
1092
|
+
dsl_id=dsl_id)
|
1093
|
+
for result in content_results:
|
1094
|
+
if result is not None:
|
1095
|
+
content = result
|
1096
|
+
break
|
1097
|
+
|
1098
|
+
if not content:
|
1099
|
+
raise ValueError(f"无法获取DSL内容: {dsl_id}")
|
1100
|
+
|
1101
|
+
# 应用执行上下文
|
1102
|
+
if context:
|
1103
|
+
self.variables.update(context)
|
1104
|
+
for key, value in context.items():
|
1105
|
+
self.test_context.set(key, value)
|
1106
|
+
self.variable_replacer = VariableReplacer(
|
1107
|
+
self.variables, self.test_context
|
1108
|
+
)
|
1109
|
+
|
1110
|
+
# 执行前hook
|
1111
|
+
if self.enable_hooks and self.hook_manager:
|
1112
|
+
self.hook_manager.pm.hook.dsl_before_execution(
|
1113
|
+
dsl_id=dsl_id, context=context or {}
|
1114
|
+
)
|
1115
|
+
|
1116
|
+
result = None
|
1117
|
+
exception = None
|
1118
|
+
|
1119
|
+
try:
|
1120
|
+
# 解析并执行
|
1121
|
+
ast = self._parse_dsl_content(content)
|
1122
|
+
result = self.execute(ast)
|
1123
|
+
|
1124
|
+
except Exception as e:
|
1125
|
+
exception = e
|
1126
|
+
# 执行后hook(在异常情况下)
|
1127
|
+
if self.enable_hooks and self.hook_manager:
|
1128
|
+
try:
|
1129
|
+
self.hook_manager.pm.hook.dsl_after_execution(
|
1130
|
+
dsl_id=dsl_id,
|
1131
|
+
context=context or {},
|
1132
|
+
result=result,
|
1133
|
+
exception=exception
|
1134
|
+
)
|
1135
|
+
except Exception as hook_error:
|
1136
|
+
print(f"Hook执行失败: {hook_error}")
|
1137
|
+
raise
|
1138
|
+
else:
|
1139
|
+
# 执行后hook(在成功情况下)
|
1140
|
+
if self.enable_hooks and self.hook_manager:
|
1141
|
+
try:
|
1142
|
+
self.hook_manager.pm.hook.dsl_after_execution(
|
1143
|
+
dsl_id=dsl_id,
|
1144
|
+
context=context or {},
|
1145
|
+
result=result,
|
1146
|
+
exception=None
|
1147
|
+
)
|
1148
|
+
except Exception as hook_error:
|
1149
|
+
print(f"Hook执行失败: {hook_error}")
|
1150
|
+
finally:
|
1151
|
+
# 完成执行跟踪
|
1152
|
+
if self.enable_tracking and self.execution_tracker:
|
1153
|
+
self.execution_tracker.finish_execution()
|
1154
|
+
|
1155
|
+
return result
|
1156
|
+
|
1157
|
+
def _parse_dsl_content(self, content: str) -> Node:
|
1158
|
+
"""解析DSL内容为AST(公共方法)
|
1159
|
+
|
1160
|
+
Args:
|
1161
|
+
content: DSL文本内容
|
1162
|
+
|
1163
|
+
Returns:
|
1164
|
+
Node: 解析后的AST根节点
|
1165
|
+
|
1166
|
+
Raises:
|
1167
|
+
Exception: 解析失败时抛出异常
|
1168
|
+
"""
|
1169
|
+
from pytest_dsl.core.parser import parse_with_error_handling
|
1170
|
+
from pytest_dsl.core.lexer import get_lexer
|
1171
|
+
|
1172
|
+
lexer = get_lexer()
|
1173
|
+
ast, parse_errors = parse_with_error_handling(content, lexer)
|
1174
|
+
|
1175
|
+
if parse_errors:
|
1176
|
+
# 如果有解析错误,抛出异常
|
1177
|
+
error_messages = [error['message'] for error in parse_errors]
|
1178
|
+
raise Exception(f"DSL解析失败: {'; '.join(error_messages)}")
|
1179
|
+
|
1180
|
+
return ast
|
798
1181
|
|
799
1182
|
|
800
1183
|
def read_file(filename):
|