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.
@@ -15,10 +15,10 @@ parser = get_parser()
15
15
 
16
16
  def read_file(filename):
17
17
  """读取DSL文件内容
18
-
18
+
19
19
  Args:
20
20
  filename: 文件路径
21
-
21
+
22
22
  Returns:
23
23
  str: 文件内容
24
24
  """
@@ -26,28 +26,27 @@ def read_file(filename):
26
26
  return f.read()
27
27
 
28
28
 
29
- def execute_dsl_file(filename: str) -> None:
29
+ def execute_dsl_file(filename: str, executor: DSLExecutor = None) -> None:
30
30
  """执行DSL文件
31
-
31
+
32
32
  Args:
33
33
  filename: DSL文件路径
34
+ executor: 可选的DSL执行器实例,如果不提供则创建新的
34
35
  """
35
36
  try:
36
37
  # 读取文件内容
37
38
  with open(filename, 'r', encoding='utf-8') as f:
38
39
  content = f.read()
39
-
40
- # 创建词法分析器和语法分析器
41
- lexer = get_lexer()
42
- parser = get_parser()
43
-
44
- # 解析DSL文件
45
- ast = parser.parse(content, lexer=lexer)
46
-
47
- # 创建执行器并执行
48
- executor = DSLExecutor()
49
- executor.execute(ast)
50
-
40
+
41
+ # 创建或使用提供的执行器
42
+ if executor is None:
43
+ executor = DSLExecutor(enable_tracking=True)
44
+
45
+ # 使用带跟踪功能的execute_from_content方法
46
+ # 使用文件路径作为dsl_id以便于跟踪
47
+ dsl_id = str(Path(filename).resolve())
48
+ executor.execute_from_content(content, dsl_id=dsl_id)
49
+
51
50
  except Exception as e:
52
51
  # 如果是语法错误,记录并抛出
53
52
  if "语法错误" in str(e):
@@ -59,18 +58,18 @@ def execute_dsl_file(filename: str) -> None:
59
58
 
60
59
  def extract_metadata_from_ast(ast):
61
60
  """从AST中提取元数据
62
-
61
+
63
62
  提取DSL文件中的元数据信息,如@data和@name标记。
64
-
63
+
65
64
  Args:
66
65
  ast: 解析后的抽象语法树
67
-
66
+
68
67
  Returns:
69
68
  tuple: (data_source, test_title) 元组,如果不存在则为None
70
69
  """
71
70
  data_source = None
72
71
  test_title = None
73
-
72
+
74
73
  for child in ast.children:
75
74
  if child.type == 'Metadata':
76
75
  for item in child.children:
@@ -80,5 +79,5 @@ def extract_metadata_from_ast(ast):
80
79
  test_title = item.value
81
80
  if data_source and test_title:
82
81
  break
83
-
84
- return data_source, test_title
82
+
83
+ return data_source, test_title
@@ -0,0 +1,291 @@
1
+ """
2
+ DSL执行跟踪器
3
+ 负责跟踪DSL执行过程中的行号、状态等信息,并提供回调接口
4
+ """
5
+
6
+ import time
7
+ import threading
8
+ from typing import Dict, Any, List, Optional, Callable
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+
12
+
13
+ class ExecutionStatus(Enum):
14
+ """执行状态枚举"""
15
+ PENDING = "pending" # 等待执行
16
+ RUNNING = "running" # 正在执行
17
+ SUCCESS = "success" # 执行成功
18
+ FAILED = "failed" # 执行失败
19
+ SKIPPED = "skipped" # 跳过执行
20
+
21
+
22
+ @dataclass
23
+ class ExecutionStep:
24
+ """执行步骤信息"""
25
+ line_number: int
26
+ node_type: str
27
+ description: str
28
+ status: ExecutionStatus = ExecutionStatus.PENDING
29
+ start_time: Optional[float] = None
30
+ end_time: Optional[float] = None
31
+ error: Optional[str] = None
32
+ result: Optional[Any] = None
33
+ metadata: Dict[str, Any] = field(default_factory=dict)
34
+
35
+ @property
36
+ def duration(self) -> Optional[float]:
37
+ """计算执行耗时"""
38
+ if self.start_time and self.end_time:
39
+ return self.end_time - self.start_time
40
+ return None
41
+
42
+ def start(self):
43
+ """开始执行"""
44
+ self.status = ExecutionStatus.RUNNING
45
+ self.start_time = time.time()
46
+
47
+ def finish(self, result: Any = None, error: str = None):
48
+ """完成执行"""
49
+ self.end_time = time.time()
50
+ if error:
51
+ self.status = ExecutionStatus.FAILED
52
+ self.error = error
53
+ else:
54
+ self.status = ExecutionStatus.SUCCESS
55
+ self.result = result
56
+
57
+ def skip(self, reason: str = None):
58
+ """跳过执行"""
59
+ self.status = ExecutionStatus.SKIPPED
60
+ if reason:
61
+ self.metadata['skip_reason'] = reason
62
+
63
+
64
+ class ExecutionTracker:
65
+ """DSL执行跟踪器"""
66
+
67
+ def __init__(self, dsl_id: str = None):
68
+ self.dsl_id = dsl_id
69
+ self.steps: List[ExecutionStep] = []
70
+ self.current_step: Optional[ExecutionStep] = None
71
+ self.execution_start_time: Optional[float] = None
72
+ self.execution_end_time: Optional[float] = None
73
+ self.callbacks: Dict[str, List[Callable]] = {
74
+ 'step_start': [],
75
+ 'step_finish': [],
76
+ 'execution_start': [],
77
+ 'execution_finish': [],
78
+ 'line_change': []
79
+ }
80
+ self._lock = threading.Lock()
81
+
82
+ # 执行统计
83
+ self.total_steps = 0
84
+ self.completed_steps = 0
85
+ self.failed_steps = 0
86
+
87
+ def register_callback(self, event: str, callback: Callable):
88
+ """注册回调函数
89
+
90
+ Args:
91
+ event: 事件类型 (step_start, step_finish, execution_start,
92
+ execution_finish, line_change)
93
+ callback: 回调函数
94
+ """
95
+ if event in self.callbacks:
96
+ self.callbacks[event].append(callback)
97
+
98
+ def _trigger_callbacks(self, event: str, **kwargs):
99
+ """触发回调函数"""
100
+ for callback in self.callbacks.get(event, []):
101
+ try:
102
+ callback(tracker=self, **kwargs)
103
+ except Exception as e:
104
+ # 回调异常不应影响主执行流程
105
+ print(f"回调函数执行异常: {e}")
106
+
107
+ def start_execution(self):
108
+ """开始执行"""
109
+ with self._lock:
110
+ self.execution_start_time = time.time()
111
+ self._trigger_callbacks('execution_start')
112
+
113
+ def finish_execution(self):
114
+ """完成执行"""
115
+ with self._lock:
116
+ self.execution_end_time = time.time()
117
+ self._trigger_callbacks('execution_finish')
118
+
119
+ def start_step(self, line_number: int, node_type: str,
120
+ description: str, **metadata) -> ExecutionStep:
121
+ """开始执行步骤"""
122
+ with self._lock:
123
+ step = ExecutionStep(
124
+ line_number=line_number,
125
+ node_type=node_type,
126
+ description=description,
127
+ metadata=metadata
128
+ )
129
+ step.start()
130
+
131
+ self.steps.append(step)
132
+ self.current_step = step
133
+ self.total_steps += 1
134
+
135
+ # 触发回调
136
+ self._trigger_callbacks('step_start', step=step)
137
+ self._trigger_callbacks('line_change',
138
+ line_number=line_number, step=step)
139
+
140
+ return step
141
+
142
+ def finish_current_step(self, result: Any = None, error: str = None):
143
+ """完成当前步骤"""
144
+ with self._lock:
145
+ if self.current_step:
146
+ self.current_step.finish(result, error)
147
+
148
+ if error:
149
+ self.failed_steps += 1
150
+ else:
151
+ self.completed_steps += 1
152
+
153
+ # 触发回调
154
+ self._trigger_callbacks('step_finish', step=self.current_step)
155
+
156
+ # 记录到Allure
157
+ self._log_step_to_allure(self.current_step)
158
+
159
+ self.current_step = None
160
+
161
+ def skip_current_step(self, reason: str = None):
162
+ """跳过当前步骤"""
163
+ with self._lock:
164
+ if self.current_step:
165
+ self.current_step.skip(reason)
166
+ self.completed_steps += 1
167
+
168
+ # 触发回调
169
+ self._trigger_callbacks('step_finish', step=self.current_step)
170
+
171
+ # 记录到Allure
172
+ self._log_step_to_allure(self.current_step)
173
+
174
+ self.current_step = None
175
+
176
+ def get_current_line(self) -> Optional[int]:
177
+ """获取当前执行行号"""
178
+ return self.current_step.line_number if self.current_step else None
179
+
180
+ def get_execution_progress(self) -> Dict[str, Any]:
181
+ """获取执行进度信息"""
182
+ return {
183
+ 'dsl_id': self.dsl_id,
184
+ 'total_steps': self.total_steps,
185
+ 'completed_steps': self.completed_steps,
186
+ 'failed_steps': self.failed_steps,
187
+ 'current_line': self.get_current_line(),
188
+ 'progress_percentage': (
189
+ (self.completed_steps / self.total_steps * 100)
190
+ if self.total_steps > 0 else 0),
191
+ 'execution_time': self.get_execution_time()
192
+ }
193
+
194
+ def get_execution_time(self) -> Optional[float]:
195
+ """获取执行时间"""
196
+ if self.execution_start_time:
197
+ end_time = self.execution_end_time or time.time()
198
+ return end_time - self.execution_start_time
199
+ return None
200
+
201
+ def get_steps_summary(self) -> Dict[str, Any]:
202
+ """获取步骤执行摘要"""
203
+ status_counts = {}
204
+ line_ranges = []
205
+
206
+ for step in self.steps:
207
+ # 统计状态
208
+ status = step.status.value
209
+ status_counts[status] = status_counts.get(status, 0) + 1
210
+
211
+ # 收集行号范围
212
+ line_ranges.append(step.line_number)
213
+
214
+ return {
215
+ 'total_steps': len(self.steps),
216
+ 'status_counts': status_counts,
217
+ 'line_range': {
218
+ 'start': min(line_ranges) if line_ranges else None,
219
+ 'end': max(line_ranges) if line_ranges else None
220
+ },
221
+ 'execution_time': self.get_execution_time(),
222
+ 'steps': [
223
+ {
224
+ 'line': step.line_number,
225
+ 'type': step.node_type,
226
+ 'status': step.status.value,
227
+ 'duration': step.duration,
228
+ 'description': step.description
229
+ } for step in self.steps
230
+ ]
231
+ }
232
+
233
+ def _log_step_to_allure(self, step: ExecutionStep):
234
+ """将步骤信息记录到Allure报告"""
235
+ # 不再创建额外的allure attachment,避免重复记录
236
+ # DSL执行器中已经有专门的attachment记录行号信息
237
+ pass
238
+
239
+ def export_execution_report(self) -> Dict[str, Any]:
240
+ """导出执行报告"""
241
+ return {
242
+ 'dsl_id': self.dsl_id,
243
+ 'execution_summary': self.get_execution_progress(),
244
+ 'steps_summary': self.get_steps_summary(),
245
+ 'execution_timeline': [
246
+ {
247
+ 'line_number': step.line_number,
248
+ 'node_type': step.node_type,
249
+ 'description': step.description,
250
+ 'status': step.status.value,
251
+ 'start_time': step.start_time,
252
+ 'end_time': step.end_time,
253
+ 'duration': step.duration,
254
+ 'error': step.error,
255
+ 'metadata': step.metadata
256
+ } for step in self.steps
257
+ ]
258
+ }
259
+
260
+
261
+ # 全局跟踪器注册表
262
+ _global_trackers: Dict[str, ExecutionTracker] = {}
263
+ _tracker_lock = threading.Lock()
264
+
265
+
266
+ def get_or_create_tracker(dsl_id: str = None) -> ExecutionTracker:
267
+ """获取或创建执行跟踪器"""
268
+ with _tracker_lock:
269
+ if dsl_id is None:
270
+ dsl_id = f"tracker_{int(time.time() * 1000)}"
271
+
272
+ if dsl_id not in _global_trackers:
273
+ _global_trackers[dsl_id] = ExecutionTracker(dsl_id)
274
+
275
+ return _global_trackers[dsl_id]
276
+
277
+
278
+ def get_tracker(dsl_id: str) -> Optional[ExecutionTracker]:
279
+ """获取指定的执行跟踪器"""
280
+ return _global_trackers.get(dsl_id)
281
+
282
+
283
+ def remove_tracker(dsl_id: str):
284
+ """移除执行跟踪器"""
285
+ with _tracker_lock:
286
+ _global_trackers.pop(dsl_id, None)
287
+
288
+
289
+ def list_active_trackers() -> List[str]:
290
+ """列出所有活跃的跟踪器"""
291
+ return list(_global_trackers.keys())