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.
@@ -2,15 +2,15 @@ import re
2
2
  import allure
3
3
  import csv
4
4
  import os
5
- import pytest
6
- from pytest_dsl.core.lexer import get_lexer
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
- """初始化DSL执行器"""
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 = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)|(?:\[[^\]]+\]))*)\}'
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 str(left_value).replace('.', '', 1).isdigit():
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 str(right_value).replace('.', '', 1).isdigit():
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 isinstance(right_value, (int, float)):
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 isinstance(left_value, (int, float)):
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 custom_keyword_manager
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
- # 在_execute_test_iteration之前添加
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 custom_keyword_manager
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 os.path.isdir(resources_dir):
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
- from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
387
- custom_keyword_manager.load_resource_file(file_path)
388
- self.imported_files.add(file_path)
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
- var_name = node.value
449
- expr_value = self.eval_expression(node.children[0])
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
- # 检查变量名是否以g_开头,如果是则设置为全局变量
452
- if var_name.startswith('g_'):
453
- global_context.set_variable(var_name, expr_value)
454
- allure.attach(
455
- f"全局变量: {var_name}\n值: {expr_value}",
456
- name="全局变量赋值",
457
- attachment_type=allure.attachment_type.TEXT
458
- )
459
- else:
460
- # 存储在本地变量字典和测试上下文中
461
- self.variable_replacer.local_variables[var_name] = expr_value
462
- self.test_context.set(var_name, expr_value) # 同时添加到测试上下文
463
- allure.attach(
464
- f"变量: {var_name}\n值: {expr_value}",
465
- name="赋值详情",
466
- attachment_type=allure.attachment_type.TEXT
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
- var_name = node.value
473
- keyword_call_node = node.children[0]
474
- result = self.execute(keyword_call_node)
475
-
476
- if result is not None:
477
- # 处理新的统一返回格式(支持远程关键字模式)
478
- if isinstance(result, dict) and 'result' in result:
479
- # 提取主要返回值
480
- main_result = result['result']
481
-
482
- # 处理captures字段中的变量
483
- captures = result.get('captures', {})
484
- for capture_var, capture_value in captures.items():
485
- if capture_var.startswith('g_'):
486
- global_context.set_variable(capture_var, capture_value)
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
- self.variable_replacer.local_variables[capture_var] = capture_value
489
- self.test_context.set(capture_var, capture_value)
597
+ # 传统格式,直接使用结果
598
+ actual_result = result
490
599
 
491
- # 将主要结果赋值给指定变量
492
- actual_result = main_result
493
- else:
494
- # 传统格式,直接使用结果
495
- actual_result = result
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
- # 检查变量名是否以g_开头,如果是则设置为全局变量
498
- if var_name.startswith('g_'):
499
- global_context.set_variable(var_name, actual_result)
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"全局变量: {var_name}\n值: {actual_result}",
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
- else:
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"变量: {var_name}\n值: {actual_result}",
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
- else:
515
- raise Exception(f"关键字 {keyword_call_node.value} 没有返回结果")
664
+ raise
516
665
 
517
- @allure.step("执行循环")
518
- def _handle_for_loop(self, node):
519
- """处理for循环"""
520
- var_name = node.value
521
- start = self.eval_expression(node.children[0])
522
- end = self.eval_expression(node.children[1])
523
-
524
- for i in range(int(start), int(end)):
525
- # 存储在本地变量字典和测试上下文中
526
- self.variable_replacer.local_variables[var_name] = i
527
- self.test_context.set(var_name, i) # 同时添加到测试上下文
528
- with allure.step(f"循环轮次: {var_name} = {i}"):
529
- try:
530
- self.execute(node.children[2])
531
- except BreakException:
532
- # 遇到break语句,退出循环
533
- allure.attach(
534
- f"在 {var_name} = {i} 时遇到break语句,退出循环",
535
- name="循环Break",
536
- attachment_type=allure.attachment_type.TEXT
537
- )
538
- break
539
- except ContinueException:
540
- # 遇到continue语句,跳过本次循环
541
- allure.attach(
542
- f"在 {var_name} = {i} 时遇到continue语句,跳过本次循环",
543
- name="循环Continue",
544
- attachment_type=allure.attachment_type.TEXT
545
- )
546
- continue
547
- except ReturnException as e:
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
- raise Exception(f"未注册的关键字: {keyword_name}")
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
- try:
566
- # 由于KeywordManager中的wrapper已经添加了allure.step和日志,这里不再重复添加
567
- result = keyword_manager.execute(keyword_name, **kwargs)
568
- return result
569
- except Exception as e:
570
- # 异常会在KeywordManager的wrapper中记录,这里只需要向上抛出
571
- raise
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
- remote_keyword_call_node = node.children[0]
728
- result = self.execute(remote_keyword_call_node)
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
- actual_result = main_result
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"全局变量: {var_name}\n值: {actual_result}",
758
- name="全局变量赋值",
955
+ f"变量: {var_name}\n错误: {error_msg}{line_info}",
956
+ name="远程关键字赋值失败",
759
957
  attachment_type=allure.attachment_type.TEXT
760
958
  )
761
- else:
762
- # 存储在本地变量字典和测试上下文中
763
- self.variable_replacer.local_variables[var_name] = actual_result
764
- self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
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值: {actual_result}",
767
- name="赋值详情",
964
+ f"变量: {var_name}\n错误: {str(e)}{line_info}",
965
+ name="远程关键字赋值失败",
768
966
  attachment_type=allure.attachment_type.TEXT
769
967
  )
770
- else:
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': self._handle_assignment_remote_keyword_call,
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
- return handler(node)
797
- raise Exception(f"未知的节点类型: {node.type}")
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):