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.
@@ -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
- var_name = node.value
517
- expr_value = self.eval_expression(node.children[0])
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
- # 检查变量名是否以g_开头,如果是则设置为全局变量
520
- if var_name.startswith('g_'):
521
- global_context.set_variable(var_name, expr_value)
522
- allure.attach(
523
- f"全局变量: {var_name}\n值: {expr_value}",
524
- name="全局变量赋值",
525
- attachment_type=allure.attachment_type.TEXT
526
- )
527
- else:
528
- # 存储在本地变量字典和测试上下文中
529
- self.variable_replacer.local_variables[var_name] = expr_value
530
- self.test_context.set(var_name, expr_value) # 同时添加到测试上下文
531
- allure.attach(
532
- f"变量: {var_name}\n值: {expr_value}",
533
- name="赋值详情",
534
- attachment_type=allure.attachment_type.TEXT
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
- var_name = node.value
541
- keyword_call_node = node.children[0]
542
- result = self.execute(keyword_call_node)
543
-
544
- if result is not None:
545
- # 处理新的统一返回格式(支持远程关键字模式)
546
- if isinstance(result, dict) and 'result' in result:
547
- # 提取主要返回值
548
- main_result = result['result']
549
-
550
- # 处理captures字段中的变量
551
- captures = result.get('captures', {})
552
- for capture_var, capture_value in captures.items():
553
- if capture_var.startswith('g_'):
554
- global_context.set_variable(capture_var, capture_value)
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
- capture_var] = capture_value
558
- self.test_context.set(capture_var, capture_value)
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
- actual_result = main_result
562
- else:
563
- # 传统格式,直接使用结果
564
- actual_result = result
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
- # 检查变量名是否以g_开头,如果是则设置为全局变量
567
- if var_name.startswith('g_'):
568
- global_context.set_variable(var_name, actual_result)
649
+ # 记录循环信息,包含行号
569
650
  allure.attach(
570
- f"全局变量: {var_name}\n值: {actual_result}",
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
- else:
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"变量: {var_name}\n值: {actual_result}",
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
- else:
585
- raise Exception(f"关键字 {keyword_call_node.value} 没有返回结果")
662
+ raise
586
663
 
587
- @allure.step("执行循环")
588
- def _handle_for_loop(self, node):
589
- """处理for循环"""
590
- var_name = node.value
591
- start = self.eval_expression(node.children[0])
592
- end = self.eval_expression(node.children[1])
593
-
594
- for i in range(int(start), int(end)):
595
- # 存储在本地变量字典和测试上下文中
596
- self.variable_replacer.local_variables[var_name] = i
597
- self.test_context.set(var_name, i) # 同时添加到测试上下文
598
- with allure.step(f"循环轮次: {var_name} = {i}"):
599
- try:
600
- self.execute(node.children[2])
601
- except BreakException:
602
- # 遇到break语句,退出循环
603
- allure.attach(
604
- f"在 {var_name} = {i} 时遇到break语句,退出循环",
605
- name="循环Break",
606
- attachment_type=allure.attachment_type.TEXT
607
- )
608
- break
609
- except ContinueException:
610
- # 遇到continue语句,跳过本次循环
611
- allure.attach(
612
- f"在 {var_name} = {i} 时遇到continue语句,跳过本次循环",
613
- name="循环Continue",
614
- attachment_type=allure.attachment_type.TEXT
615
- )
616
- continue
617
- except ReturnException as e:
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
- raise Exception(f"未注册的关键字: {keyword_name}")
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
- try:
636
- # 由于KeywordManager中的wrapper已经添加了allure.step和日志,这里不再重复添加
637
- result = keyword_manager.execute(keyword_name, **kwargs)
638
- return result
639
- except Exception:
640
- # 异常会在KeywordManager的wrapper中记录,这里只需要向上抛出
641
- raise
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
- remote_keyword_call_node = node.children[0]
798
- result = self.execute(remote_keyword_call_node)
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
- actual_result = main_result
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"全局变量: {var_name}\n值: {actual_result}",
829
- name="全局变量赋值",
971
+ f"变量: {var_name}\n错误: {error_msg}{line_info}",
972
+ name="远程关键字赋值失败",
830
973
  attachment_type=allure.attachment_type.TEXT
831
974
  )
832
- else:
833
- # 存储在本地变量字典和测试上下文中
834
- self.variable_replacer.local_variables[
835
- var_name] = actual_result
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值: {actual_result}",
839
- name="赋值详情",
980
+ f"变量: {var_name}\n错误: {str(e)}{line_info}",
981
+ name="远程关键字赋值失败",
840
982
  attachment_type=allure.attachment_type.TEXT
841
983
  )
842
- else:
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
- return handler(node)
870
- raise Exception(f"未知的节点类型: {node.type}")
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
- from pytest_dsl.core.parser import get_parser
983
-
1187
+
984
1188
  lexer = get_lexer()
985
- parser = get_parser()
986
- return parser.parse(content, lexer=lexer)
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):