pytest-dsl 0.14.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 +202 -3
- pytest_dsl/cli.py +15 -682
- pytest_dsl/core/dsl_executor.py +393 -183
- pytest_dsl/core/dsl_executor_utils.py +21 -22
- pytest_dsl/core/execution_tracker.py +291 -0
- pytest_dsl/core/keyword_loader.py +402 -0
- pytest_dsl/core/keyword_manager.py +29 -23
- pytest_dsl/core/keyword_utils.py +609 -0
- pytest_dsl/core/lexer.py +8 -0
- pytest_dsl/core/parser.py +130 -20
- pytest_dsl/core/validator.py +417 -0
- pytest_dsl/plugin.py +10 -1
- {pytest_dsl-0.14.0.dist-info → pytest_dsl-0.15.1.dist-info}/METADATA +1 -1
- {pytest_dsl-0.14.0.dist-info → pytest_dsl-0.15.1.dist-info}/RECORD +18 -14
- {pytest_dsl-0.14.0.dist-info → pytest_dsl-0.15.1.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.14.0.dist-info → pytest_dsl-0.15.1.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.14.0.dist-info → pytest_dsl-0.15.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.14.0.dist-info → pytest_dsl-0.15.1.dist-info}/top_level.txt +0 -0
pytest_dsl/core/dsl_executor.py
CHANGED
@@ -8,6 +8,9 @@ from pytest_dsl.core.keyword_manager import keyword_manager
|
|
8
8
|
from pytest_dsl.core.global_context import global_context
|
9
9
|
from pytest_dsl.core.context import TestContext
|
10
10
|
from pytest_dsl.core.variable_utils import VariableReplacer
|
11
|
+
from pytest_dsl.core.execution_tracker import (
|
12
|
+
get_or_create_tracker, ExecutionTracker
|
13
|
+
)
|
11
14
|
|
12
15
|
|
13
16
|
class BreakException(Exception):
|
@@ -36,11 +39,13 @@ class DSLExecutor:
|
|
36
39
|
- PYTEST_DSL_KEEP_VARIABLES=0: (默认) 执行完成后清空变量,用于正常DSL执行
|
37
40
|
"""
|
38
41
|
|
39
|
-
def __init__(self, enable_hooks: bool = True
|
42
|
+
def __init__(self, enable_hooks: bool = True,
|
43
|
+
enable_tracking: bool = True):
|
40
44
|
"""初始化DSL执行器
|
41
|
-
|
45
|
+
|
42
46
|
Args:
|
43
47
|
enable_hooks: 是否启用hook机制,默认True
|
48
|
+
enable_tracking: 是否启用执行跟踪,默认True
|
44
49
|
"""
|
45
50
|
self.variables = {}
|
46
51
|
self.test_context = TestContext()
|
@@ -48,11 +53,15 @@ class DSLExecutor:
|
|
48
53
|
self.variable_replacer = VariableReplacer(
|
49
54
|
self.variables, self.test_context)
|
50
55
|
self.imported_files = set() # 跟踪已导入的文件,避免循环导入
|
51
|
-
|
56
|
+
|
52
57
|
# Hook相关配置
|
53
58
|
self.enable_hooks = enable_hooks
|
54
59
|
self.current_dsl_id = None # 当前执行的DSL标识符
|
55
|
-
|
60
|
+
|
61
|
+
# 执行跟踪配置
|
62
|
+
self.enable_tracking = enable_tracking
|
63
|
+
self.execution_tracker: ExecutionTracker = None
|
64
|
+
|
56
65
|
if self.enable_hooks:
|
57
66
|
self._init_hooks()
|
58
67
|
|
@@ -312,8 +321,6 @@ class DSLExecutor:
|
|
312
321
|
def _handle_start(self, node):
|
313
322
|
"""处理开始节点"""
|
314
323
|
try:
|
315
|
-
# 清空上下文,确保每个测试用例都有一个新的上下文
|
316
|
-
self.test_context.clear()
|
317
324
|
metadata = {}
|
318
325
|
teardown_node = None
|
319
326
|
|
@@ -365,7 +372,7 @@ class DSLExecutor:
|
|
365
372
|
for result in case_results:
|
366
373
|
if result:
|
367
374
|
cases.extend(result)
|
368
|
-
|
375
|
+
|
369
376
|
# 如果hook返回了资源,导入它们
|
370
377
|
for case in cases:
|
371
378
|
case_id = case.get('id') or case.get('file_path', '')
|
@@ -378,7 +385,7 @@ class DSLExecutor:
|
|
378
385
|
continue
|
379
386
|
except Exception as e:
|
380
387
|
print(f"通过hook自动导入资源时出现警告: {str(e)}")
|
381
|
-
|
388
|
+
|
382
389
|
# 然后进行传统的文件系统自动导入
|
383
390
|
try:
|
384
391
|
from pytest_dsl.core.custom_keyword_manager import (
|
@@ -439,11 +446,11 @@ class DSLExecutor:
|
|
439
446
|
if result is not None:
|
440
447
|
content = result
|
441
448
|
break
|
442
|
-
|
449
|
+
|
443
450
|
# 如果hook返回了内容,直接使用DSL解析方式处理
|
444
451
|
if content is not None:
|
445
452
|
ast = self._parse_dsl_content(content)
|
446
|
-
|
453
|
+
|
447
454
|
# 只处理自定义关键字,不执行测试流程
|
448
455
|
self._handle_custom_keywords_in_file(ast)
|
449
456
|
self.imported_files.add(file_path)
|
@@ -510,135 +517,245 @@ class DSLExecutor:
|
|
510
517
|
# 将return异常向上传递,不在这里处理
|
511
518
|
raise e
|
512
519
|
|
513
|
-
@allure.step("变量赋值")
|
514
520
|
def _handle_assignment(self, node):
|
515
521
|
"""处理赋值语句"""
|
516
|
-
|
517
|
-
|
522
|
+
step_name = f"变量赋值: {node.value}"
|
523
|
+
line_info = (f"\n行号: {node.line_number}"
|
524
|
+
if hasattr(node, 'line_number') and node.line_number
|
525
|
+
else "")
|
518
526
|
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
527
|
+
with allure.step(step_name):
|
528
|
+
try:
|
529
|
+
var_name = node.value
|
530
|
+
expr_value = self.eval_expression(node.children[0])
|
531
|
+
|
532
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
533
|
+
if var_name.startswith('g_'):
|
534
|
+
global_context.set_variable(var_name, expr_value)
|
535
|
+
# 记录全局变量赋值,包含行号信息
|
536
|
+
allure.attach(
|
537
|
+
f"全局变量: {var_name}\n值: {expr_value}{line_info}",
|
538
|
+
name="全局变量赋值",
|
539
|
+
attachment_type=allure.attachment_type.TEXT
|
540
|
+
)
|
541
|
+
else:
|
542
|
+
# 存储在本地变量字典和测试上下文中
|
543
|
+
self.variable_replacer.local_variables[
|
544
|
+
var_name] = expr_value
|
545
|
+
self.test_context.set(var_name, expr_value)
|
546
|
+
# 记录变量赋值,包含行号信息
|
547
|
+
allure.attach(
|
548
|
+
f"变量: {var_name}\n值: {expr_value}{line_info}",
|
549
|
+
name="赋值详情",
|
550
|
+
attachment_type=allure.attachment_type.TEXT
|
551
|
+
)
|
552
|
+
except Exception as e:
|
553
|
+
# 记录赋值失败,包含行号信息
|
554
|
+
allure.attach(
|
555
|
+
f"变量: {var_name}\n错误: {str(e)}{line_info}",
|
556
|
+
name="赋值失败",
|
557
|
+
attachment_type=allure.attachment_type.TEXT
|
558
|
+
)
|
559
|
+
raise
|
536
560
|
|
537
|
-
@allure.step("关键字调用赋值")
|
538
561
|
def _handle_assignment_keyword_call(self, node):
|
539
562
|
"""处理关键字调用赋值"""
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
if
|
554
|
-
|
563
|
+
step_name = f"关键字调用赋值: {node.value}"
|
564
|
+
line_info = (f"\n行号: {node.line_number}"
|
565
|
+
if hasattr(node, 'line_number') and node.line_number
|
566
|
+
else "")
|
567
|
+
|
568
|
+
with allure.step(step_name):
|
569
|
+
try:
|
570
|
+
var_name = node.value
|
571
|
+
keyword_call_node = node.children[0]
|
572
|
+
result = self.execute(keyword_call_node)
|
573
|
+
|
574
|
+
if result is not None:
|
575
|
+
# 处理新的统一返回格式(支持远程关键字模式)
|
576
|
+
if isinstance(result, dict) and 'result' in result:
|
577
|
+
# 提取主要返回值
|
578
|
+
main_result = result['result']
|
579
|
+
|
580
|
+
# 处理captures字段中的变量
|
581
|
+
captures = result.get('captures', {})
|
582
|
+
for capture_var, capture_value in captures.items():
|
583
|
+
if capture_var.startswith('g_'):
|
584
|
+
global_context.set_variable(
|
585
|
+
capture_var, capture_value)
|
586
|
+
else:
|
587
|
+
self.variable_replacer.local_variables[
|
588
|
+
capture_var] = capture_value
|
589
|
+
self.test_context.set(
|
590
|
+
capture_var, capture_value)
|
591
|
+
|
592
|
+
# 将主要结果赋值给指定变量
|
593
|
+
actual_result = main_result
|
555
594
|
else:
|
595
|
+
# 传统格式,直接使用结果
|
596
|
+
actual_result = result
|
597
|
+
|
598
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
599
|
+
if var_name.startswith('g_'):
|
600
|
+
global_context.set_variable(var_name, actual_result)
|
601
|
+
allure.attach(
|
602
|
+
f"全局变量: {var_name}\n值: {actual_result}"
|
603
|
+
f"{line_info}",
|
604
|
+
name="关键字调用赋值",
|
605
|
+
attachment_type=allure.attachment_type.TEXT
|
606
|
+
)
|
607
|
+
else:
|
608
|
+
# 存储在本地变量字典和测试上下文中
|
556
609
|
self.variable_replacer.local_variables[
|
557
|
-
|
558
|
-
self.test_context.set(
|
610
|
+
var_name] = actual_result
|
611
|
+
self.test_context.set(var_name, actual_result)
|
612
|
+
allure.attach(
|
613
|
+
f"变量: {var_name}\n值: {actual_result}"
|
614
|
+
f"{line_info}",
|
615
|
+
name="关键字调用赋值",
|
616
|
+
attachment_type=allure.attachment_type.TEXT
|
617
|
+
)
|
618
|
+
else:
|
619
|
+
error_msg = f"关键字 {keyword_call_node.value} 没有返回结果"
|
620
|
+
allure.attach(
|
621
|
+
f"变量: {var_name}\n错误: {error_msg}{line_info}",
|
622
|
+
name="关键字调用赋值失败",
|
623
|
+
attachment_type=allure.attachment_type.TEXT
|
624
|
+
)
|
625
|
+
raise Exception(error_msg)
|
626
|
+
except Exception as e:
|
627
|
+
# 如果异常不是我们刚才抛出的,记录异常信息
|
628
|
+
if "没有返回结果" not in str(e):
|
629
|
+
allure.attach(
|
630
|
+
f"变量: {var_name}\n错误: {str(e)}{line_info}",
|
631
|
+
name="关键字调用赋值失败",
|
632
|
+
attachment_type=allure.attachment_type.TEXT
|
633
|
+
)
|
634
|
+
raise
|
559
635
|
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
636
|
+
def _handle_for_loop(self, node):
|
637
|
+
"""处理for循环"""
|
638
|
+
step_name = f"For循环: {node.value}"
|
639
|
+
line_info = (f"\n行号: {node.line_number}"
|
640
|
+
if hasattr(node, 'line_number') and node.line_number
|
641
|
+
else "")
|
642
|
+
|
643
|
+
with allure.step(step_name):
|
644
|
+
try:
|
645
|
+
var_name = node.value
|
646
|
+
start = self.eval_expression(node.children[0])
|
647
|
+
end = self.eval_expression(node.children[1])
|
565
648
|
|
566
|
-
|
567
|
-
if var_name.startswith('g_'):
|
568
|
-
global_context.set_variable(var_name, actual_result)
|
649
|
+
# 记录循环信息,包含行号
|
569
650
|
allure.attach(
|
570
|
-
f"
|
571
|
-
name="
|
651
|
+
f"循环变量: {var_name}\n范围: {start} 到 {end}{line_info}",
|
652
|
+
name="For循环",
|
572
653
|
attachment_type=allure.attachment_type.TEXT
|
573
654
|
)
|
574
|
-
|
575
|
-
#
|
576
|
-
self.variable_replacer.local_variables[
|
577
|
-
var_name] = actual_result
|
578
|
-
self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
|
655
|
+
except Exception as e:
|
656
|
+
# 记录循环初始化失败
|
579
657
|
allure.attach(
|
580
|
-
f"
|
581
|
-
name="
|
658
|
+
f"循环变量: {var_name}\n错误: {str(e)}{line_info}",
|
659
|
+
name="For循环初始化失败",
|
582
660
|
attachment_type=allure.attachment_type.TEXT
|
583
661
|
)
|
584
|
-
|
585
|
-
raise Exception(f"关键字 {keyword_call_node.value} 没有返回结果")
|
662
|
+
raise
|
586
663
|
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
# 遇到return语句,将异常向上传递
|
619
|
-
allure.attach(
|
620
|
-
f"在 {var_name} = {i} 时遇到return语句,退出函数",
|
621
|
-
name="循环Return",
|
622
|
-
attachment_type=allure.attachment_type.TEXT
|
623
|
-
)
|
624
|
-
raise e
|
664
|
+
for i in range(int(start), int(end)):
|
665
|
+
# 存储在本地变量字典和测试上下文中
|
666
|
+
self.variable_replacer.local_variables[var_name] = i
|
667
|
+
self.test_context.set(var_name, i)
|
668
|
+
with allure.step(f"循环轮次: {var_name} = {i}"):
|
669
|
+
try:
|
670
|
+
self.execute(node.children[2])
|
671
|
+
except BreakException:
|
672
|
+
# 遇到break语句,退出循环
|
673
|
+
allure.attach(
|
674
|
+
f"在 {var_name} = {i} 时遇到break语句,退出循环",
|
675
|
+
name="循环Break",
|
676
|
+
attachment_type=allure.attachment_type.TEXT
|
677
|
+
)
|
678
|
+
break
|
679
|
+
except ContinueException:
|
680
|
+
# 遇到continue语句,跳过本次循环
|
681
|
+
allure.attach(
|
682
|
+
f"在 {var_name} = {i} 时遇到continue语句,跳过本次循环",
|
683
|
+
name="循环Continue",
|
684
|
+
attachment_type=allure.attachment_type.TEXT
|
685
|
+
)
|
686
|
+
continue
|
687
|
+
except ReturnException as e:
|
688
|
+
# 遇到return语句,将异常向上传递
|
689
|
+
allure.attach(
|
690
|
+
f"在 {var_name} = {i} 时遇到return语句,退出函数",
|
691
|
+
name="循环Return",
|
692
|
+
attachment_type=allure.attachment_type.TEXT
|
693
|
+
)
|
694
|
+
raise e
|
625
695
|
|
626
696
|
def _execute_keyword_call(self, node):
|
627
697
|
"""执行关键字调用"""
|
628
698
|
keyword_name = node.value
|
699
|
+
line_info = (f"\n行号: {node.line_number}"
|
700
|
+
if hasattr(node, 'line_number') and node.line_number
|
701
|
+
else "")
|
702
|
+
|
703
|
+
# 先检查关键字是否存在
|
629
704
|
keyword_info = keyword_manager.get_keyword_info(keyword_name)
|
630
705
|
if not keyword_info:
|
631
|
-
|
706
|
+
error_msg = f"未注册的关键字: {keyword_name}"
|
707
|
+
allure.attach(
|
708
|
+
f"关键字: {keyword_name}\n错误: {error_msg}{line_info}",
|
709
|
+
name="关键字调用失败",
|
710
|
+
attachment_type=allure.attachment_type.TEXT
|
711
|
+
)
|
712
|
+
raise Exception(error_msg)
|
632
713
|
|
633
714
|
kwargs = self._prepare_keyword_params(node, keyword_info)
|
715
|
+
step_name = f"调用关键字: {keyword_name}"
|
634
716
|
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
717
|
+
with allure.step(step_name):
|
718
|
+
try:
|
719
|
+
# 传递自定义步骤名称给KeywordManager,避免重复的allure步骤嵌套
|
720
|
+
kwargs['step_name'] = keyword_name # 内层步骤只显示关键字名称
|
721
|
+
# 避免KeywordManager重复记录,由DSL执行器统一记录
|
722
|
+
kwargs['skip_logging'] = True
|
723
|
+
|
724
|
+
result = keyword_manager.execute(keyword_name, **kwargs)
|
725
|
+
|
726
|
+
# 执行成功后记录关键字信息,包含行号
|
727
|
+
allure.attach(
|
728
|
+
f"关键字: {keyword_name}\n执行结果: 成功{line_info}",
|
729
|
+
name="关键字调用",
|
730
|
+
attachment_type=allure.attachment_type.TEXT
|
731
|
+
)
|
732
|
+
|
733
|
+
return result
|
734
|
+
except Exception as e:
|
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
|
+
|
753
|
+
allure.attach(
|
754
|
+
error_details,
|
755
|
+
name="关键字调用失败",
|
756
|
+
attachment_type=allure.attachment_type.TEXT
|
757
|
+
)
|
758
|
+
raise
|
642
759
|
|
643
760
|
def _prepare_keyword_params(self, node, keyword_info):
|
644
761
|
"""准备关键字调用参数"""
|
@@ -751,6 +868,9 @@ class DSLExecutor:
|
|
751
868
|
call_info = node.value
|
752
869
|
alias = call_info['alias']
|
753
870
|
keyword_name = call_info['keyword']
|
871
|
+
line_info = (f"\n行号: {node.line_number}"
|
872
|
+
if hasattr(node, 'line_number') and node.line_number
|
873
|
+
else "")
|
754
874
|
|
755
875
|
# 准备参数
|
756
876
|
params = []
|
@@ -773,7 +893,7 @@ class DSLExecutor:
|
|
773
893
|
alias, keyword_name, **kwargs)
|
774
894
|
allure.attach(
|
775
895
|
f"远程关键字参数: {kwargs}\n"
|
776
|
-
f"远程关键字结果: {result}",
|
896
|
+
f"远程关键字结果: {result}{line_info}",
|
777
897
|
name="远程关键字执行详情",
|
778
898
|
attachment_type=allure.attachment_type.TEXT
|
779
899
|
)
|
@@ -781,7 +901,7 @@ class DSLExecutor:
|
|
781
901
|
except Exception as e:
|
782
902
|
# 记录错误并重新抛出
|
783
903
|
allure.attach(
|
784
|
-
f"远程关键字执行失败: {str(e)}",
|
904
|
+
f"远程关键字执行失败: {str(e)}{line_info}",
|
785
905
|
name="远程关键字错误",
|
786
906
|
attachment_type=allure.attachment_type.TEXT
|
787
907
|
)
|
@@ -794,56 +914,85 @@ class DSLExecutor:
|
|
794
914
|
node: AssignmentRemoteKeywordCall节点
|
795
915
|
"""
|
796
916
|
var_name = node.value
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
if result is not None:
|
801
|
-
# 注意:远程关键字客户端已经处理了新格式的返回值,
|
802
|
-
# 这里接收到的result应该已经是主要返回值,而不是完整的字典格式
|
803
|
-
# 但为了保险起见,我们仍然检查是否为新格式
|
804
|
-
if isinstance(result, dict) and 'result' in result:
|
805
|
-
# 如果仍然是新格式(可能是嵌套的远程调用),提取主要返回值
|
806
|
-
main_result = result['result']
|
807
|
-
|
808
|
-
# 处理captures字段中的变量
|
809
|
-
captures = result.get('captures', {})
|
810
|
-
for capture_var, capture_value in captures.items():
|
811
|
-
if capture_var.startswith('g_'):
|
812
|
-
global_context.set_variable(capture_var, capture_value)
|
813
|
-
else:
|
814
|
-
self.variable_replacer.local_variables[
|
815
|
-
capture_var] = capture_value
|
816
|
-
self.test_context.set(capture_var, capture_value)
|
917
|
+
line_info = (f"\n行号: {node.line_number}"
|
918
|
+
if hasattr(node, 'line_number') and node.line_number
|
919
|
+
else "")
|
817
920
|
|
818
|
-
|
819
|
-
|
921
|
+
try:
|
922
|
+
remote_keyword_call_node = node.children[0]
|
923
|
+
result = self.execute(remote_keyword_call_node)
|
924
|
+
|
925
|
+
if result is not None:
|
926
|
+
# 注意:远程关键字客户端已经处理了新格式的返回值,
|
927
|
+
# 这里接收到的result应该已经是主要返回值,而不是完整的字典格式
|
928
|
+
# 但为了保险起见,我们仍然检查是否为新格式
|
929
|
+
if isinstance(result, dict) and 'result' in result:
|
930
|
+
# 如果仍然是新格式(可能是嵌套的远程调用),提取主要返回值
|
931
|
+
main_result = result['result']
|
932
|
+
|
933
|
+
# 处理captures字段中的变量
|
934
|
+
captures = result.get('captures', {})
|
935
|
+
for capture_var, capture_value in captures.items():
|
936
|
+
if capture_var.startswith('g_'):
|
937
|
+
global_context.set_variable(
|
938
|
+
capture_var, capture_value)
|
939
|
+
else:
|
940
|
+
self.variable_replacer.local_variables[
|
941
|
+
capture_var] = capture_value
|
942
|
+
self.test_context.set(capture_var, capture_value)
|
943
|
+
|
944
|
+
# 将主要结果赋值给指定变量
|
945
|
+
actual_result = main_result
|
946
|
+
else:
|
947
|
+
# 传统格式或已经处理过的格式,直接使用结果
|
948
|
+
actual_result = result
|
949
|
+
|
950
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
951
|
+
if var_name.startswith('g_'):
|
952
|
+
global_context.set_variable(var_name, actual_result)
|
953
|
+
allure.attach(
|
954
|
+
f"全局变量: {var_name}\n值: {actual_result}{line_info}",
|
955
|
+
name="远程关键字赋值",
|
956
|
+
attachment_type=allure.attachment_type.TEXT
|
957
|
+
)
|
958
|
+
else:
|
959
|
+
# 存储在本地变量字典和测试上下文中
|
960
|
+
self.variable_replacer.local_variables[
|
961
|
+
var_name] = actual_result
|
962
|
+
self.test_context.set(var_name, actual_result)
|
963
|
+
allure.attach(
|
964
|
+
f"变量: {var_name}\n值: {actual_result}{line_info}",
|
965
|
+
name="远程关键字赋值",
|
966
|
+
attachment_type=allure.attachment_type.TEXT
|
967
|
+
)
|
820
968
|
else:
|
821
|
-
|
822
|
-
actual_result = result
|
823
|
-
|
824
|
-
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
825
|
-
if var_name.startswith('g_'):
|
826
|
-
global_context.set_variable(var_name, actual_result)
|
969
|
+
error_msg = "远程关键字没有返回结果"
|
827
970
|
allure.attach(
|
828
|
-
f"
|
829
|
-
name="
|
971
|
+
f"变量: {var_name}\n错误: {error_msg}{line_info}",
|
972
|
+
name="远程关键字赋值失败",
|
830
973
|
attachment_type=allure.attachment_type.TEXT
|
831
974
|
)
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
|
975
|
+
raise Exception(error_msg)
|
976
|
+
except Exception as e:
|
977
|
+
# 如果异常不是我们刚才抛出的,记录异常信息
|
978
|
+
if "没有返回结果" not in str(e):
|
837
979
|
allure.attach(
|
838
|
-
f"变量: {var_name}\n
|
839
|
-
name="
|
980
|
+
f"变量: {var_name}\n错误: {str(e)}{line_info}",
|
981
|
+
name="远程关键字赋值失败",
|
840
982
|
attachment_type=allure.attachment_type.TEXT
|
841
983
|
)
|
842
|
-
|
843
|
-
raise Exception("远程关键字没有返回结果")
|
984
|
+
raise
|
844
985
|
|
845
986
|
def execute(self, node):
|
846
987
|
"""执行AST节点"""
|
988
|
+
# 执行跟踪
|
989
|
+
if self.enable_tracking and self.execution_tracker:
|
990
|
+
line_number = getattr(node, 'line_number', None)
|
991
|
+
if line_number:
|
992
|
+
description = self._get_node_description(node)
|
993
|
+
self.execution_tracker.start_step(
|
994
|
+
line_number, node.type, description)
|
995
|
+
|
847
996
|
handlers = {
|
848
997
|
'Start': self._handle_start,
|
849
998
|
'Metadata': lambda _: None,
|
@@ -865,15 +1014,61 @@ class DSLExecutor:
|
|
865
1014
|
}
|
866
1015
|
|
867
1016
|
handler = handlers.get(node.type)
|
868
|
-
if handler:
|
869
|
-
|
870
|
-
|
1017
|
+
if not handler:
|
1018
|
+
error_msg = f"未知的节点类型: {node.type}"
|
1019
|
+
if self.enable_tracking and self.execution_tracker:
|
1020
|
+
self.execution_tracker.finish_current_step(error=error_msg)
|
1021
|
+
raise Exception(error_msg)
|
1022
|
+
|
1023
|
+
try:
|
1024
|
+
result = handler(node)
|
1025
|
+
# 执行成功
|
1026
|
+
if self.enable_tracking and self.execution_tracker:
|
1027
|
+
self.execution_tracker.finish_current_step(result=result)
|
1028
|
+
return result
|
1029
|
+
except Exception as e:
|
1030
|
+
# 执行失败
|
1031
|
+
if self.enable_tracking and self.execution_tracker:
|
1032
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
1033
|
+
if hasattr(node, 'line_number') and node.line_number:
|
1034
|
+
error_msg += f" (行{node.line_number})"
|
1035
|
+
self.execution_tracker.finish_current_step(error=error_msg)
|
1036
|
+
raise
|
1037
|
+
|
1038
|
+
def _get_remote_keyword_description(self, node):
|
1039
|
+
"""获取远程关键字调用的描述"""
|
1040
|
+
if isinstance(getattr(node, 'value', None), dict):
|
1041
|
+
keyword = node.value.get('keyword', '')
|
1042
|
+
return f"调用远程关键字: {keyword}"
|
1043
|
+
return "调用远程关键字"
|
1044
|
+
|
1045
|
+
def _get_node_description(self, node):
|
1046
|
+
"""获取节点的描述信息"""
|
1047
|
+
descriptions = {
|
1048
|
+
'Assignment': f"变量赋值: {getattr(node, 'value', '')}",
|
1049
|
+
'AssignmentKeywordCall': f"关键字赋值: {getattr(node, 'value', '')}",
|
1050
|
+
'AssignmentRemoteKeywordCall': (
|
1051
|
+
f"远程关键字赋值: {getattr(node, 'value', '')}"),
|
1052
|
+
'KeywordCall': f"调用关键字: {getattr(node, 'value', '')}",
|
1053
|
+
'RemoteKeywordCall': self._get_remote_keyword_description(node),
|
1054
|
+
'ForLoop': f"For循环: {getattr(node, 'value', '')}",
|
1055
|
+
'IfStatement': "条件分支",
|
1056
|
+
'Return': "返回语句",
|
1057
|
+
'Break': "Break语句",
|
1058
|
+
'Continue': "Continue语句",
|
1059
|
+
'Teardown': "清理操作",
|
1060
|
+
'Start': "开始执行",
|
1061
|
+
'Statements': "语句块"
|
1062
|
+
}
|
1063
|
+
|
1064
|
+
return descriptions.get(node.type, f"执行{node.type}")
|
871
1065
|
|
872
1066
|
def __repr__(self):
|
873
1067
|
"""返回DSL执行器的字符串表示"""
|
874
1068
|
return (f"DSLExecutor(variables={len(self.variables)}, "
|
875
|
-
f"hooks_enabled={self.enable_hooks}
|
876
|
-
|
1069
|
+
f"hooks_enabled={self.enable_hooks}, "
|
1070
|
+
f"tracking_enabled={self.enable_tracking})")
|
1071
|
+
|
877
1072
|
def _init_hooks(self):
|
878
1073
|
"""初始化hook机制"""
|
879
1074
|
try:
|
@@ -886,21 +1081,26 @@ class DSLExecutor:
|
|
886
1081
|
# 如果没有安装pluggy,禁用hook
|
887
1082
|
self.enable_hooks = False
|
888
1083
|
self.hook_manager = None
|
889
|
-
|
1084
|
+
|
890
1085
|
def execute_from_content(self, content: str, dsl_id: str = None,
|
891
1086
|
context: Dict[str, Any] = None) -> Any:
|
892
1087
|
"""从内容执行DSL,支持hook扩展
|
893
|
-
|
1088
|
+
|
894
1089
|
Args:
|
895
1090
|
content: DSL内容,如果为空字符串将尝试通过hook加载
|
896
1091
|
dsl_id: DSL标识符(可选)
|
897
1092
|
context: 执行上下文(可选)
|
898
|
-
|
1093
|
+
|
899
1094
|
Returns:
|
900
1095
|
执行结果
|
901
1096
|
"""
|
902
1097
|
self.current_dsl_id = dsl_id
|
903
|
-
|
1098
|
+
|
1099
|
+
# 初始化执行跟踪器
|
1100
|
+
if self.enable_tracking:
|
1101
|
+
self.execution_tracker = get_or_create_tracker(dsl_id)
|
1102
|
+
self.execution_tracker.start_execution()
|
1103
|
+
|
904
1104
|
# 如果content为空且有dsl_id,尝试通过hook加载内容
|
905
1105
|
if (not content and dsl_id and self.enable_hooks and
|
906
1106
|
hasattr(self, 'hook_manager') and self.hook_manager):
|
@@ -910,10 +1110,10 @@ class DSLExecutor:
|
|
910
1110
|
if result is not None:
|
911
1111
|
content = result
|
912
1112
|
break
|
913
|
-
|
1113
|
+
|
914
1114
|
if not content:
|
915
1115
|
raise ValueError(f"无法获取DSL内容: {dsl_id}")
|
916
|
-
|
1116
|
+
|
917
1117
|
# 应用执行上下文
|
918
1118
|
if context:
|
919
1119
|
self.variables.update(context)
|
@@ -922,30 +1122,30 @@ class DSLExecutor:
|
|
922
1122
|
self.variable_replacer = VariableReplacer(
|
923
1123
|
self.variables, self.test_context
|
924
1124
|
)
|
925
|
-
|
1125
|
+
|
926
1126
|
# 执行前hook
|
927
1127
|
if self.enable_hooks and self.hook_manager:
|
928
1128
|
self.hook_manager.pm.hook.dsl_before_execution(
|
929
1129
|
dsl_id=dsl_id, context=context or {}
|
930
1130
|
)
|
931
|
-
|
1131
|
+
|
932
1132
|
result = None
|
933
1133
|
exception = None
|
934
|
-
|
1134
|
+
|
935
1135
|
try:
|
936
1136
|
# 解析并执行
|
937
1137
|
ast = self._parse_dsl_content(content)
|
938
1138
|
result = self.execute(ast)
|
939
|
-
|
1139
|
+
|
940
1140
|
except Exception as e:
|
941
1141
|
exception = e
|
942
1142
|
# 执行后hook(在异常情况下)
|
943
1143
|
if self.enable_hooks and self.hook_manager:
|
944
1144
|
try:
|
945
1145
|
self.hook_manager.pm.hook.dsl_after_execution(
|
946
|
-
dsl_id=dsl_id,
|
947
|
-
context=context or {},
|
948
|
-
result=result,
|
1146
|
+
dsl_id=dsl_id,
|
1147
|
+
context=context or {},
|
1148
|
+
result=result,
|
949
1149
|
exception=exception
|
950
1150
|
)
|
951
1151
|
except Exception as hook_error:
|
@@ -956,34 +1156,44 @@ class DSLExecutor:
|
|
956
1156
|
if self.enable_hooks and self.hook_manager:
|
957
1157
|
try:
|
958
1158
|
self.hook_manager.pm.hook.dsl_after_execution(
|
959
|
-
dsl_id=dsl_id,
|
960
|
-
context=context or {},
|
961
|
-
result=result,
|
1159
|
+
dsl_id=dsl_id,
|
1160
|
+
context=context or {},
|
1161
|
+
result=result,
|
962
1162
|
exception=None
|
963
1163
|
)
|
964
1164
|
except Exception as hook_error:
|
965
1165
|
print(f"Hook执行失败: {hook_error}")
|
966
|
-
|
1166
|
+
finally:
|
1167
|
+
# 完成执行跟踪
|
1168
|
+
if self.enable_tracking and self.execution_tracker:
|
1169
|
+
self.execution_tracker.finish_execution()
|
1170
|
+
|
967
1171
|
return result
|
968
1172
|
|
969
1173
|
def _parse_dsl_content(self, content: str) -> Node:
|
970
1174
|
"""解析DSL内容为AST(公共方法)
|
971
|
-
|
1175
|
+
|
972
1176
|
Args:
|
973
1177
|
content: DSL文本内容
|
974
|
-
|
1178
|
+
|
975
1179
|
Returns:
|
976
1180
|
Node: 解析后的AST根节点
|
977
|
-
|
1181
|
+
|
978
1182
|
Raises:
|
979
1183
|
Exception: 解析失败时抛出异常
|
980
1184
|
"""
|
1185
|
+
from pytest_dsl.core.parser import parse_with_error_handling
|
981
1186
|
from pytest_dsl.core.lexer import get_lexer
|
982
|
-
|
983
|
-
|
1187
|
+
|
984
1188
|
lexer = get_lexer()
|
985
|
-
|
986
|
-
|
1189
|
+
ast, parse_errors = parse_with_error_handling(content, lexer)
|
1190
|
+
|
1191
|
+
if parse_errors:
|
1192
|
+
# 如果有解析错误,抛出异常
|
1193
|
+
error_messages = [error['message'] for error in parse_errors]
|
1194
|
+
raise Exception(f"DSL解析失败: {'; '.join(error_messages)}")
|
1195
|
+
|
1196
|
+
return ast
|
987
1197
|
|
988
1198
|
|
989
1199
|
def read_file(filename):
|