pytest-dsl 0.15.1__py3-none-any.whl → 0.15.3__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/core/dsl_executor.py +595 -352
- pytest_dsl/core/parser.py +20 -2
- pytest_dsl/core/validator.py +71 -1
- {pytest_dsl-0.15.1.dist-info → pytest_dsl-0.15.3.dist-info}/METADATA +1 -1
- {pytest_dsl-0.15.1.dist-info → pytest_dsl-0.15.3.dist-info}/RECORD +9 -9
- {pytest_dsl-0.15.1.dist-info → pytest_dsl-0.15.3.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.15.1.dist-info → pytest_dsl-0.15.3.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.15.1.dist-info → pytest_dsl-0.15.3.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.15.1.dist-info → pytest_dsl-0.15.3.dist-info}/top_level.txt +0 -0
pytest_dsl/core/dsl_executor.py
CHANGED
@@ -31,6 +31,28 @@ class ReturnException(Exception):
|
|
31
31
|
super().__init__(f"Return with value: {return_value}")
|
32
32
|
|
33
33
|
|
34
|
+
class DSLExecutionError(Exception):
|
35
|
+
"""DSL执行异常,包含行号信息"""
|
36
|
+
|
37
|
+
def __init__(self, message: str, line_number: int = None, node_type: str = None,
|
38
|
+
original_exception: Exception = None):
|
39
|
+
self.line_number = line_number
|
40
|
+
self.node_type = node_type
|
41
|
+
self.original_exception = original_exception
|
42
|
+
|
43
|
+
# 构建详细的错误消息
|
44
|
+
error_parts = [message]
|
45
|
+
if line_number:
|
46
|
+
error_parts.append(f"行号: {line_number}")
|
47
|
+
if node_type:
|
48
|
+
error_parts.append(f"节点类型: {node_type}")
|
49
|
+
if original_exception:
|
50
|
+
error_parts.append(
|
51
|
+
f"原始异常: {type(original_exception).__name__}: {str(original_exception)}")
|
52
|
+
|
53
|
+
super().__init__(" | ".join(error_parts))
|
54
|
+
|
55
|
+
|
34
56
|
class DSLExecutor:
|
35
57
|
"""DSL执行器,负责执行解析后的AST
|
36
58
|
|
@@ -62,9 +84,140 @@ class DSLExecutor:
|
|
62
84
|
self.enable_tracking = enable_tracking
|
63
85
|
self.execution_tracker: ExecutionTracker = None
|
64
86
|
|
87
|
+
# 当前执行节点(用于异常处理时获取行号)
|
88
|
+
self._current_node = None
|
89
|
+
# 节点调用栈,用于追踪有行号信息的节点
|
90
|
+
self._node_stack = []
|
91
|
+
|
65
92
|
if self.enable_hooks:
|
66
93
|
self._init_hooks()
|
67
94
|
|
95
|
+
def _get_line_info(self, node=None):
|
96
|
+
"""获取行号信息字符串
|
97
|
+
|
98
|
+
Args:
|
99
|
+
node: 可选的节点,如果不提供则使用当前节点
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
包含行号信息的字符串
|
103
|
+
"""
|
104
|
+
target_node = node or self._current_node
|
105
|
+
|
106
|
+
# 尝试从当前节点获取行号
|
107
|
+
if target_node and hasattr(target_node, 'line_number') and target_node.line_number:
|
108
|
+
return f"\n行号: {target_node.line_number}"
|
109
|
+
|
110
|
+
# 如果当前节点没有行号,从节点栈中查找最近的有行号的节点
|
111
|
+
for stack_node in reversed(self._node_stack):
|
112
|
+
if hasattr(stack_node, 'line_number') and stack_node.line_number:
|
113
|
+
return f"\n行号: {stack_node.line_number}"
|
114
|
+
|
115
|
+
# 如果当前节点没有行号,尝试从当前执行的节点获取
|
116
|
+
if self._current_node and hasattr(self._current_node, 'line_number') and self._current_node.line_number:
|
117
|
+
return f"\n行号: {self._current_node.line_number}"
|
118
|
+
|
119
|
+
return ""
|
120
|
+
|
121
|
+
def _handle_exception_with_line_info(self, e: Exception, node=None, context_info: str = "", skip_allure_logging: bool = False):
|
122
|
+
"""统一处理异常并记录行号信息
|
123
|
+
|
124
|
+
Args:
|
125
|
+
e: 原始异常
|
126
|
+
node: 可选的节点,用于获取行号
|
127
|
+
context_info: 额外的上下文信息
|
128
|
+
skip_allure_logging: 是否跳过Allure日志记录,避免重复记录
|
129
|
+
|
130
|
+
Raises:
|
131
|
+
DSLExecutionError: 包含行号信息的DSL执行异常
|
132
|
+
"""
|
133
|
+
target_node = node or self._current_node
|
134
|
+
line_number = None
|
135
|
+
node_type = None
|
136
|
+
|
137
|
+
# 尝试从目标节点获取行号
|
138
|
+
if target_node:
|
139
|
+
line_number = getattr(target_node, 'line_number', None)
|
140
|
+
node_type = getattr(target_node, 'type', None)
|
141
|
+
|
142
|
+
# 如果目标节点没有行号,从节点栈中查找最近的有行号的节点
|
143
|
+
if not line_number:
|
144
|
+
for stack_node in reversed(self._node_stack):
|
145
|
+
stack_line = getattr(stack_node, 'line_number', None)
|
146
|
+
if stack_line:
|
147
|
+
line_number = stack_line
|
148
|
+
if not node_type:
|
149
|
+
node_type = getattr(stack_node, 'type', None)
|
150
|
+
break
|
151
|
+
|
152
|
+
# 如果还是没有行号,尝试从当前执行节点获取
|
153
|
+
if not line_number and self._current_node:
|
154
|
+
line_number = getattr(self._current_node, 'line_number', None)
|
155
|
+
if not node_type:
|
156
|
+
node_type = getattr(self._current_node, 'type', None)
|
157
|
+
|
158
|
+
# 构建错误消息
|
159
|
+
error_msg = str(e)
|
160
|
+
if context_info:
|
161
|
+
error_msg = f"{context_info}: {error_msg}"
|
162
|
+
|
163
|
+
# 只有在没有跳过Allure日志记录时才记录到Allure
|
164
|
+
if not skip_allure_logging:
|
165
|
+
# 记录到Allure
|
166
|
+
line_info = self._get_line_info(target_node)
|
167
|
+
error_details = f"{error_msg}{line_info}"
|
168
|
+
if context_info:
|
169
|
+
error_details += f"\n上下文: {context_info}"
|
170
|
+
|
171
|
+
allure.attach(
|
172
|
+
error_details,
|
173
|
+
name="DSL执行异常",
|
174
|
+
attachment_type=allure.attachment_type.TEXT
|
175
|
+
)
|
176
|
+
|
177
|
+
# 如果原始异常已经是DSLExecutionError,不要重复封装
|
178
|
+
if isinstance(e, DSLExecutionError):
|
179
|
+
raise e
|
180
|
+
|
181
|
+
# 对于控制流异常,直接重抛,不封装
|
182
|
+
if isinstance(e, (BreakException, ContinueException, ReturnException)):
|
183
|
+
raise e
|
184
|
+
|
185
|
+
# 对于断言错误,保持原样但添加行号信息
|
186
|
+
if isinstance(e, AssertionError):
|
187
|
+
enhanced_msg = f"{str(e)}{self._get_line_info(target_node)}"
|
188
|
+
raise AssertionError(enhanced_msg) from e
|
189
|
+
|
190
|
+
# 其他异常封装为DSLExecutionError
|
191
|
+
raise DSLExecutionError(
|
192
|
+
message=error_msg,
|
193
|
+
line_number=line_number,
|
194
|
+
node_type=node_type,
|
195
|
+
original_exception=e
|
196
|
+
) from e
|
197
|
+
|
198
|
+
def _execute_with_error_handling(self, func, node, *args, **kwargs):
|
199
|
+
"""在错误处理包装器中执行函数
|
200
|
+
|
201
|
+
Args:
|
202
|
+
func: 要执行的函数
|
203
|
+
node: 当前节点
|
204
|
+
*args: 函数参数
|
205
|
+
**kwargs: 函数关键字参数
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
函数执行结果
|
209
|
+
"""
|
210
|
+
old_node = self._current_node
|
211
|
+
self._current_node = node
|
212
|
+
|
213
|
+
try:
|
214
|
+
return func(*args, **kwargs)
|
215
|
+
except Exception as e:
|
216
|
+
self._handle_exception_with_line_info(
|
217
|
+
e, node, f"执行{getattr(node, 'type', '未知节点')}")
|
218
|
+
finally:
|
219
|
+
self._current_node = old_node
|
220
|
+
|
68
221
|
def set_current_data(self, data):
|
69
222
|
"""设置当前测试数据集"""
|
70
223
|
if data:
|
@@ -112,67 +265,99 @@ class DSLExecutor:
|
|
112
265
|
|
113
266
|
:param expr_node: AST中的表达式节点
|
114
267
|
:return: 表达式求值后的结果
|
115
|
-
:raises
|
268
|
+
:raises DSLExecutionError: 当遇到未定义变量或无法求值的类型时抛出异常
|
116
269
|
"""
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
270
|
+
def _eval_expression_impl():
|
271
|
+
if expr_node.type == 'Expression':
|
272
|
+
value = self._eval_expression_value(expr_node.value)
|
273
|
+
# 统一处理变量替换
|
274
|
+
return self.variable_replacer.replace_in_value(value)
|
275
|
+
elif expr_node.type == 'StringLiteral':
|
276
|
+
# 字符串字面量,如果包含变量占位符则进行替换,否则直接返回
|
277
|
+
if '${' in expr_node.value:
|
278
|
+
return self.variable_replacer.replace_in_string(expr_node.value)
|
279
|
+
else:
|
280
|
+
return expr_node.value
|
281
|
+
elif expr_node.type == 'NumberLiteral':
|
282
|
+
# 数字字面量,直接返回值
|
283
|
+
return expr_node.value
|
284
|
+
elif expr_node.type == 'VariableRef':
|
285
|
+
# 变量引用,从变量存储中获取值
|
286
|
+
var_name = expr_node.value
|
287
|
+
try:
|
288
|
+
return self.variable_replacer.get_variable(var_name)
|
289
|
+
except KeyError:
|
290
|
+
raise KeyError(f"变量 '{var_name}' 不存在")
|
291
|
+
elif expr_node.type == 'PlaceholderRef':
|
292
|
+
# 变量占位符 ${var},进行变量替换
|
293
|
+
return self.variable_replacer.replace_in_string(expr_node.value)
|
294
|
+
elif expr_node.type == 'KeywordCall':
|
295
|
+
return self.execute(expr_node)
|
296
|
+
elif expr_node.type == 'ListExpr':
|
297
|
+
# 处理列表表达式
|
298
|
+
result = []
|
299
|
+
for item in expr_node.children:
|
300
|
+
item_value = self.eval_expression(item)
|
301
|
+
result.append(item_value)
|
302
|
+
return result
|
303
|
+
elif expr_node.type == 'DictExpr':
|
304
|
+
# 处理字典表达式
|
305
|
+
result = {}
|
306
|
+
for item in expr_node.children:
|
307
|
+
# 每个item是DictItem节点,包含键和值
|
308
|
+
key_value = self.eval_expression(item.children[0])
|
309
|
+
value_value = self.eval_expression(item.children[1])
|
310
|
+
result[key_value] = value_value
|
311
|
+
return result
|
312
|
+
elif expr_node.type == 'BooleanExpr':
|
313
|
+
# 处理布尔值表达式
|
314
|
+
return expr_node.value
|
315
|
+
elif expr_node.type == 'ComparisonExpr':
|
316
|
+
# 处理比较表达式
|
317
|
+
return self._eval_comparison_expr(expr_node)
|
318
|
+
elif expr_node.type == 'ArithmeticExpr':
|
319
|
+
# 处理算术表达式
|
320
|
+
return self._eval_arithmetic_expr(expr_node)
|
321
|
+
else:
|
322
|
+
raise Exception(f"无法求值的表达式类型: {expr_node.type}")
|
323
|
+
|
324
|
+
return self._execute_with_error_handling(_eval_expression_impl, expr_node)
|
150
325
|
|
151
326
|
def _eval_expression_value(self, value):
|
152
327
|
"""处理表达式值的具体逻辑"""
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
328
|
+
try:
|
329
|
+
if isinstance(value, Node):
|
330
|
+
return self.eval_expression(value)
|
331
|
+
elif isinstance(value, str):
|
332
|
+
# 定义扩展的变量引用模式,支持数组索引和字典键访问
|
333
|
+
pattern = (
|
334
|
+
r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
|
335
|
+
r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
|
336
|
+
r'|(?:\[[^\]]+\]))*)\}'
|
337
|
+
)
|
338
|
+
# 检查整个字符串是否完全匹配单一变量引用模式
|
339
|
+
match = re.fullmatch(pattern, value)
|
340
|
+
if match:
|
341
|
+
var_ref = match.group(1)
|
342
|
+
# 使用新的变量路径解析器
|
343
|
+
return self.variable_replacer._parse_variable_path(var_ref)
|
344
|
+
elif '${' in value:
|
345
|
+
# 如果包含变量占位符,则替换字符串中的所有变量引用
|
346
|
+
return self.variable_replacer.replace_in_string(value)
|
347
|
+
else:
|
348
|
+
# 对于不包含 ${} 的普通字符串,检查是否为单纯的变量名
|
349
|
+
# 只有当字符串是有效的变量名格式且确实存在该变量时,才当作变量处理
|
350
|
+
if (re.match(r'^[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*$', value) and
|
351
|
+
value in self.variable_replacer.local_variables):
|
352
|
+
return self.variable_replacer.local_variables[value]
|
353
|
+
else:
|
354
|
+
# 否则当作字符串字面量处理
|
355
|
+
return value
|
356
|
+
return value
|
357
|
+
except Exception as e:
|
358
|
+
# 为变量解析异常添加更多上下文信息
|
359
|
+
context_info = f"解析表达式值 '{value}'"
|
360
|
+
self._handle_exception_with_line_info(e, context_info=context_info)
|
176
361
|
|
177
362
|
def _eval_comparison_expr(self, expr_node):
|
178
363
|
"""
|
@@ -181,31 +366,35 @@ class DSLExecutor:
|
|
181
366
|
:param expr_node: 比较表达式节点
|
182
367
|
:return: 比较结果(布尔值)
|
183
368
|
"""
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
left_value
|
191
|
-
|
192
|
-
right_value
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
369
|
+
try:
|
370
|
+
left_value = self.eval_expression(expr_node.children[0])
|
371
|
+
right_value = self.eval_expression(expr_node.children[1])
|
372
|
+
operator = expr_node.value # 操作符: >, <, >=, <=, ==, !=
|
373
|
+
|
374
|
+
# 尝试类型转换
|
375
|
+
if isinstance(left_value, str) and str(left_value).isdigit():
|
376
|
+
left_value = int(left_value)
|
377
|
+
if isinstance(right_value, str) and str(right_value).isdigit():
|
378
|
+
right_value = int(right_value)
|
379
|
+
|
380
|
+
# 根据操作符执行相应的比较操作
|
381
|
+
if operator == '>':
|
382
|
+
return left_value > right_value
|
383
|
+
elif operator == '<':
|
384
|
+
return left_value < right_value
|
385
|
+
elif operator == '>=':
|
386
|
+
return left_value >= right_value
|
387
|
+
elif operator == '<=':
|
388
|
+
return left_value <= right_value
|
389
|
+
elif operator == '==':
|
390
|
+
return left_value == right_value
|
391
|
+
elif operator == '!=':
|
392
|
+
return left_value != right_value
|
393
|
+
else:
|
394
|
+
raise Exception(f"未知的比较操作符: {operator}")
|
395
|
+
except Exception as e:
|
396
|
+
context_info = f"比较表达式求值 '{operator}'"
|
397
|
+
self._handle_exception_with_line_info(e, expr_node, context_info)
|
209
398
|
|
210
399
|
def _eval_arithmetic_expr(self, expr_node):
|
211
400
|
"""
|
@@ -214,54 +403,58 @@ class DSLExecutor:
|
|
214
403
|
:param expr_node: 算术表达式节点
|
215
404
|
:return: 计算结果
|
216
405
|
"""
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
# 尝试类型转换 - 如果是字符串数字则转为数字
|
222
|
-
if (isinstance(left_value, str) and
|
223
|
-
str(left_value).replace('.', '', 1).isdigit()):
|
224
|
-
left_value = float(left_value)
|
225
|
-
# 如果是整数则转为整数
|
226
|
-
if left_value.is_integer():
|
227
|
-
left_value = int(left_value)
|
228
|
-
|
229
|
-
if (isinstance(right_value, str) and
|
230
|
-
str(right_value).replace('.', '', 1).isdigit()):
|
231
|
-
right_value = float(right_value)
|
232
|
-
# 如果是整数则转为整数
|
233
|
-
if right_value.is_integer():
|
234
|
-
right_value = int(right_value)
|
406
|
+
try:
|
407
|
+
left_value = self.eval_expression(expr_node.children[0])
|
408
|
+
right_value = self.eval_expression(expr_node.children[1])
|
409
|
+
operator = expr_node.value # 操作符: +, -, *, /, %
|
235
410
|
|
236
|
-
|
237
|
-
if operator == '+':
|
238
|
-
# 对于字符串,+是连接操作
|
239
|
-
if isinstance(left_value, str) or isinstance(right_value, str):
|
240
|
-
return str(left_value) + str(right_value)
|
241
|
-
return left_value + right_value
|
242
|
-
elif operator == '-':
|
243
|
-
return left_value - right_value
|
244
|
-
elif operator == '*':
|
245
|
-
# 如果其中一个是字符串,另一个是数字,则进行字符串重复
|
411
|
+
# 尝试类型转换 - 如果是字符串数字则转为数字
|
246
412
|
if (isinstance(left_value, str) and
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
413
|
+
str(left_value).replace('.', '', 1).isdigit()):
|
414
|
+
left_value = float(left_value)
|
415
|
+
# 如果是整数则转为整数
|
416
|
+
if left_value.is_integer():
|
417
|
+
left_value = int(left_value)
|
418
|
+
|
419
|
+
if (isinstance(right_value, str) and
|
420
|
+
str(right_value).replace('.', '', 1).isdigit()):
|
421
|
+
right_value = float(right_value)
|
422
|
+
# 如果是整数则转为整数
|
423
|
+
if right_value.is_integer():
|
424
|
+
right_value = int(right_value)
|
425
|
+
|
426
|
+
# 进行相应的算术运算
|
427
|
+
if operator == '+':
|
428
|
+
# 对于字符串,+是连接操作
|
429
|
+
if isinstance(left_value, str) or isinstance(right_value, str):
|
430
|
+
return str(left_value) + str(right_value)
|
431
|
+
return left_value + right_value
|
432
|
+
elif operator == '-':
|
433
|
+
return left_value - right_value
|
434
|
+
elif operator == '*':
|
435
|
+
# 如果其中一个是字符串,另一个是数字,则进行字符串重复
|
436
|
+
if (isinstance(left_value, str) and
|
437
|
+
isinstance(right_value, (int, float))):
|
438
|
+
return left_value * int(right_value)
|
439
|
+
elif (isinstance(right_value, str) and
|
440
|
+
isinstance(left_value, (int, float))):
|
441
|
+
return right_value * int(left_value)
|
442
|
+
return left_value * right_value
|
443
|
+
elif operator == '/':
|
444
|
+
# 除法时检查除数是否为0
|
445
|
+
if right_value == 0:
|
446
|
+
raise Exception("除法错误: 除数不能为0")
|
447
|
+
return left_value / right_value
|
448
|
+
elif operator == '%':
|
449
|
+
# 模运算时检查除数是否为0
|
450
|
+
if right_value == 0:
|
451
|
+
raise Exception("模运算错误: 除数不能为0")
|
452
|
+
return left_value % right_value
|
453
|
+
else:
|
454
|
+
raise Exception(f"未知的算术操作符: {operator}")
|
455
|
+
except Exception as e:
|
456
|
+
context_info = f"算术表达式求值 '{operator}'"
|
457
|
+
self._handle_exception_with_line_info(e, expr_node, context_info)
|
265
458
|
|
266
459
|
def _get_variable(self, var_name):
|
267
460
|
"""获取变量值,优先从本地变量获取,如果不存在则尝试从全局上下文获取"""
|
@@ -520,14 +713,18 @@ class DSLExecutor:
|
|
520
713
|
def _handle_assignment(self, node):
|
521
714
|
"""处理赋值语句"""
|
522
715
|
step_name = f"变量赋值: {node.value}"
|
523
|
-
line_info = (
|
524
|
-
if hasattr(node, 'line_number') and node.line_number
|
525
|
-
else "")
|
716
|
+
line_info = self._get_line_info(node)
|
526
717
|
|
527
718
|
with allure.step(step_name):
|
528
719
|
try:
|
529
720
|
var_name = node.value
|
530
|
-
|
721
|
+
# 在求值表达式之前,确保当前节点设置正确
|
722
|
+
old_current_node = self._current_node
|
723
|
+
self._current_node = node
|
724
|
+
try:
|
725
|
+
expr_value = self.eval_expression(node.children[0])
|
726
|
+
finally:
|
727
|
+
self._current_node = old_current_node
|
531
728
|
|
532
729
|
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
533
730
|
if var_name.startswith('g_'):
|
@@ -550,172 +747,163 @@ class DSLExecutor:
|
|
550
747
|
attachment_type=allure.attachment_type.TEXT
|
551
748
|
)
|
552
749
|
except Exception as e:
|
553
|
-
#
|
750
|
+
# 在步骤内部记录异常详情
|
751
|
+
error_details = f"执行Assignment节点: {str(e)}{line_info}\n上下文: 执行Assignment节点"
|
554
752
|
allure.attach(
|
555
|
-
|
556
|
-
name="
|
753
|
+
error_details,
|
754
|
+
name="DSL执行异常",
|
557
755
|
attachment_type=allure.attachment_type.TEXT
|
558
756
|
)
|
757
|
+
# 重新抛出异常,让外层的统一异常处理机制处理
|
559
758
|
raise
|
560
759
|
|
561
760
|
def _handle_assignment_keyword_call(self, node):
|
562
|
-
"""处理关键字调用赋值
|
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 "")
|
761
|
+
"""处理关键字调用赋值
|
567
762
|
|
568
|
-
|
763
|
+
Args:
|
764
|
+
node: AssignmentKeywordCall节点
|
765
|
+
"""
|
766
|
+
var_name = node.value
|
767
|
+
line_info = self._get_line_info(node)
|
768
|
+
|
769
|
+
with allure.step(f"关键字赋值: {var_name}"):
|
569
770
|
try:
|
570
|
-
var_name = node.value
|
571
771
|
keyword_call_node = node.children[0]
|
572
772
|
result = self.execute(keyword_call_node)
|
573
773
|
|
574
|
-
|
575
|
-
|
576
|
-
|
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
|
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
|
-
# 存储在本地变量字典和测试上下文中
|
609
|
-
self.variable_replacer.local_variables[
|
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} 没有返回结果"
|
774
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
775
|
+
if var_name.startswith('g_'):
|
776
|
+
global_context.set_variable(var_name, result)
|
620
777
|
allure.attach(
|
621
|
-
f"
|
622
|
-
name="
|
778
|
+
f"全局变量: {var_name}\n值: {result}{line_info}",
|
779
|
+
name="关键字赋值详情",
|
623
780
|
attachment_type=allure.attachment_type.TEXT
|
624
781
|
)
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
782
|
+
else:
|
783
|
+
# 存储在本地变量字典和测试上下文中
|
784
|
+
self.variable_replacer.local_variables[var_name] = result
|
785
|
+
self.test_context.set(var_name, result)
|
786
|
+
# 记录关键字赋值,包含行号信息
|
629
787
|
allure.attach(
|
630
|
-
f"变量: {var_name}\n
|
631
|
-
name="
|
788
|
+
f"变量: {var_name}\n值: {result}{line_info}",
|
789
|
+
name="关键字赋值详情",
|
632
790
|
attachment_type=allure.attachment_type.TEXT
|
633
791
|
)
|
792
|
+
except Exception as e:
|
793
|
+
# 在步骤内部记录异常详情
|
794
|
+
error_details = f"执行AssignmentKeywordCall节点: {str(e)}{line_info}\n上下文: 执行AssignmentKeywordCall节点"
|
795
|
+
allure.attach(
|
796
|
+
error_details,
|
797
|
+
name="DSL执行异常",
|
798
|
+
attachment_type=allure.attachment_type.TEXT
|
799
|
+
)
|
800
|
+
# 重新抛出异常,让外层的统一异常处理机制处理
|
634
801
|
raise
|
635
802
|
|
636
803
|
def _handle_for_loop(self, node):
|
637
804
|
"""处理for循环"""
|
638
|
-
step_name = f"
|
639
|
-
line_info = (
|
640
|
-
if hasattr(node, 'line_number') and node.line_number
|
641
|
-
else "")
|
805
|
+
step_name = f"执行循环: {node.value}"
|
806
|
+
line_info = self._get_line_info(node)
|
642
807
|
|
643
808
|
with allure.step(step_name):
|
644
809
|
try:
|
645
810
|
var_name = node.value
|
646
|
-
|
647
|
-
|
811
|
+
# 计算循环范围
|
812
|
+
loop_range = self.eval_expression(node.children[0])
|
813
|
+
|
814
|
+
# 如果是range对象,转换为列表
|
815
|
+
if hasattr(loop_range, '__iter__'):
|
816
|
+
loop_items = list(loop_range)
|
817
|
+
else:
|
818
|
+
loop_items = [loop_range]
|
648
819
|
|
649
|
-
# 记录循环信息,包含行号
|
650
820
|
allure.attach(
|
651
|
-
f"循环变量: {var_name}\n
|
652
|
-
name="
|
821
|
+
f"循环变量: {var_name}\n循环范围: {loop_items}{line_info}",
|
822
|
+
name="循环信息",
|
653
823
|
attachment_type=allure.attachment_type.TEXT
|
654
824
|
)
|
825
|
+
|
826
|
+
for i in loop_items:
|
827
|
+
# 设置循环变量
|
828
|
+
self.variable_replacer.local_variables[var_name] = i
|
829
|
+
self.test_context.set(var_name, i)
|
830
|
+
|
831
|
+
with allure.step(f"循环轮次: {var_name} = {i}"):
|
832
|
+
try:
|
833
|
+
self.execute(node.children[2])
|
834
|
+
except BreakException:
|
835
|
+
# 遇到break语句,退出循环
|
836
|
+
allure.attach(
|
837
|
+
f"在 {var_name} = {i} 时遇到break语句,退出循环",
|
838
|
+
name="循环Break",
|
839
|
+
attachment_type=allure.attachment_type.TEXT
|
840
|
+
)
|
841
|
+
break
|
842
|
+
except ContinueException:
|
843
|
+
# 遇到continue语句,跳过本次循环
|
844
|
+
allure.attach(
|
845
|
+
f"在 {var_name} = {i} 时遇到continue语句,跳过本次循环",
|
846
|
+
name="循环Continue",
|
847
|
+
attachment_type=allure.attachment_type.TEXT
|
848
|
+
)
|
849
|
+
continue
|
850
|
+
except ReturnException as e:
|
851
|
+
# 遇到return语句,将异常向上传递
|
852
|
+
allure.attach(
|
853
|
+
f"在 {var_name} = {i} 时遇到return语句,退出函数",
|
854
|
+
name="循环Return",
|
855
|
+
attachment_type=allure.attachment_type.TEXT
|
856
|
+
)
|
857
|
+
raise e
|
858
|
+
except Exception as e:
|
859
|
+
# 在循环轮次内部记录异常详情
|
860
|
+
error_details = f"循环执行异常 ({var_name} = {i}): {str(e)}{line_info}\n上下文: 执行ForLoop节点"
|
861
|
+
allure.attach(
|
862
|
+
error_details,
|
863
|
+
name="DSL执行异常",
|
864
|
+
attachment_type=allure.attachment_type.TEXT
|
865
|
+
)
|
866
|
+
# 重新抛出异常
|
867
|
+
raise
|
868
|
+
except (BreakException, ContinueException, ReturnException):
|
869
|
+
# 这些控制流异常应该继续向上传递
|
870
|
+
raise
|
655
871
|
except Exception as e:
|
656
|
-
#
|
872
|
+
# 在步骤内部记录异常详情
|
873
|
+
error_details = f"执行ForLoop节点: {str(e)}{line_info}\n上下文: 执行ForLoop节点"
|
657
874
|
allure.attach(
|
658
|
-
|
659
|
-
name="
|
875
|
+
error_details,
|
876
|
+
name="DSL执行异常",
|
660
877
|
attachment_type=allure.attachment_type.TEXT
|
661
878
|
)
|
879
|
+
# 重新抛出异常,让外层的统一异常处理机制处理
|
662
880
|
raise
|
663
881
|
|
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
|
695
|
-
|
696
882
|
def _execute_keyword_call(self, node):
|
697
883
|
"""执行关键字调用"""
|
698
884
|
keyword_name = node.value
|
699
|
-
line_info = (
|
700
|
-
if hasattr(node, 'line_number') and node.line_number
|
701
|
-
else "")
|
885
|
+
line_info = self._get_line_info(node)
|
702
886
|
|
703
887
|
# 先检查关键字是否存在
|
704
888
|
keyword_info = keyword_manager.get_keyword_info(keyword_name)
|
705
889
|
if not keyword_info:
|
706
890
|
error_msg = f"未注册的关键字: {keyword_name}"
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
891
|
+
# 在步骤内部记录异常
|
892
|
+
with allure.step(f"调用关键字: {keyword_name}"):
|
893
|
+
allure.attach(
|
894
|
+
f"执行KeywordCall节点: 未注册的关键字: {keyword_name}{line_info}\n上下文: 执行KeywordCall节点",
|
895
|
+
name="DSL执行异常",
|
896
|
+
attachment_type=allure.attachment_type.TEXT
|
897
|
+
)
|
712
898
|
raise Exception(error_msg)
|
713
899
|
|
714
|
-
kwargs = self._prepare_keyword_params(node, keyword_info)
|
715
900
|
step_name = f"调用关键字: {keyword_name}"
|
716
901
|
|
717
902
|
with allure.step(step_name):
|
718
903
|
try:
|
904
|
+
# 准备参数(这里可能抛出参数解析异常)
|
905
|
+
kwargs = self._prepare_keyword_params(node, keyword_info)
|
906
|
+
|
719
907
|
# 传递自定义步骤名称给KeywordManager,避免重复的allure步骤嵌套
|
720
908
|
kwargs['step_name'] = keyword_name # 内层步骤只显示关键字名称
|
721
909
|
# 避免KeywordManager重复记录,由DSL执行器统一记录
|
@@ -732,44 +920,62 @@ class DSLExecutor:
|
|
732
920
|
|
733
921
|
return result
|
734
922
|
except Exception as e:
|
735
|
-
#
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
923
|
+
# 统一在关键字调用层级记录异常,包含行号信息
|
924
|
+
if "参数解析异常" in str(e) or "无法解析变量引用" in str(e):
|
925
|
+
# 参数解析异常,提取核心错误信息
|
926
|
+
core_error = str(e)
|
927
|
+
if "参数解析异常" in core_error:
|
928
|
+
# 提取参数名和具体错误
|
929
|
+
import re
|
930
|
+
match = re.search(r'参数解析异常 \(([^)]+)\): (.+)', core_error)
|
931
|
+
if match:
|
932
|
+
param_name, detailed_error = match.groups()
|
933
|
+
error_details = f"参数解析失败 ({param_name}): {detailed_error}{line_info}\n上下文: 执行KeywordCall节点"
|
934
|
+
else:
|
935
|
+
error_details = f"参数解析失败: {core_error}{line_info}\n上下文: 执行KeywordCall节点"
|
936
|
+
else:
|
937
|
+
error_details = f"参数解析失败: {core_error}{line_info}\n上下文: 执行KeywordCall节点"
|
938
|
+
else:
|
939
|
+
# 其他异常
|
940
|
+
error_details = f"执行KeywordCall节点: {str(e)}{line_info}\n上下文: 执行KeywordCall节点"
|
752
941
|
|
753
942
|
allure.attach(
|
754
943
|
error_details,
|
755
|
-
name="
|
944
|
+
name="DSL执行异常",
|
756
945
|
attachment_type=allure.attachment_type.TEXT
|
757
946
|
)
|
947
|
+
# 重新抛出异常,让外层的统一异常处理机制处理
|
758
948
|
raise
|
759
949
|
|
760
950
|
def _prepare_keyword_params(self, node, keyword_info):
|
761
951
|
"""准备关键字调用参数"""
|
762
952
|
mapping = keyword_info.get('mapping', {})
|
763
953
|
kwargs = {'context': self.test_context} # 默认传入context参数
|
954
|
+
line_info = self._get_line_info(node)
|
764
955
|
|
765
956
|
# 检查是否有参数列表
|
766
957
|
if node.children[0]:
|
767
958
|
for param in node.children[0]:
|
768
959
|
param_name = param.value
|
769
960
|
english_param_name = mapping.get(param_name, param_name)
|
770
|
-
|
771
|
-
|
772
|
-
|
961
|
+
|
962
|
+
# 在子步骤中处理参数值解析,但不记录异常详情
|
963
|
+
with allure.step(f"解析参数: {param_name}"):
|
964
|
+
try:
|
965
|
+
# 对参数值进行变量替换
|
966
|
+
param_value = self.eval_expression(param.children[0])
|
967
|
+
kwargs[english_param_name] = param_value
|
968
|
+
|
969
|
+
# 只记录参数解析成功的简要信息
|
970
|
+
allure.attach(
|
971
|
+
f"参数名: {param_name}\n"
|
972
|
+
f"参数值: {param_value}",
|
973
|
+
name="参数解析详情",
|
974
|
+
attachment_type=allure.attachment_type.TEXT
|
975
|
+
)
|
976
|
+
except Exception as e:
|
977
|
+
# 将异常重新包装,添加参数名信息,但不在这里记录到allure
|
978
|
+
raise Exception(f"参数解析异常 ({param_name}): {str(e)}")
|
773
979
|
|
774
980
|
return kwargs
|
775
981
|
|
@@ -868,26 +1074,24 @@ class DSLExecutor:
|
|
868
1074
|
call_info = node.value
|
869
1075
|
alias = call_info['alias']
|
870
1076
|
keyword_name = call_info['keyword']
|
871
|
-
line_info = (
|
872
|
-
if hasattr(node, 'line_number') and node.line_number
|
873
|
-
else "")
|
1077
|
+
line_info = self._get_line_info(node)
|
874
1078
|
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
1079
|
+
with allure.step(f"执行远程关键字: {alias}|{keyword_name}"):
|
1080
|
+
try:
|
1081
|
+
# 准备参数
|
1082
|
+
params = []
|
1083
|
+
if node.children and node.children[0]:
|
1084
|
+
params = node.children[0]
|
879
1085
|
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
1086
|
+
kwargs = {}
|
1087
|
+
for param in params:
|
1088
|
+
param_name = param.value
|
1089
|
+
param_value = self.eval_expression(param.children[0])
|
1090
|
+
kwargs[param_name] = param_value
|
885
1091
|
|
886
|
-
|
887
|
-
|
1092
|
+
# 添加测试上下文
|
1093
|
+
kwargs['context'] = self.test_context
|
888
1094
|
|
889
|
-
with allure.step(f"执行远程关键字: {alias}|{keyword_name}"):
|
890
|
-
try:
|
891
1095
|
# 执行远程关键字
|
892
1096
|
result = remote_keyword_manager.execute_remote_keyword(
|
893
1097
|
alias, keyword_name, **kwargs)
|
@@ -899,12 +1103,14 @@ class DSLExecutor:
|
|
899
1103
|
)
|
900
1104
|
return result
|
901
1105
|
except Exception as e:
|
902
|
-
#
|
1106
|
+
# 在步骤内部记录异常详情
|
1107
|
+
error_details = f"执行RemoteKeywordCall节点: {str(e)}{line_info}\n上下文: 执行RemoteKeywordCall节点"
|
903
1108
|
allure.attach(
|
904
|
-
|
905
|
-
name="
|
1109
|
+
error_details,
|
1110
|
+
name="DSL执行异常",
|
906
1111
|
attachment_type=allure.attachment_type.TEXT
|
907
1112
|
)
|
1113
|
+
# 重新抛出异常,让外层的统一异常处理机制处理
|
908
1114
|
raise
|
909
1115
|
|
910
1116
|
def _handle_assignment_remote_keyword_call(self, node):
|
@@ -914,74 +1120,69 @@ class DSLExecutor:
|
|
914
1120
|
node: AssignmentRemoteKeywordCall节点
|
915
1121
|
"""
|
916
1122
|
var_name = node.value
|
917
|
-
line_info = (
|
918
|
-
if hasattr(node, 'line_number') and node.line_number
|
919
|
-
else "")
|
1123
|
+
line_info = self._get_line_info(node)
|
920
1124
|
|
921
|
-
|
922
|
-
|
923
|
-
|
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)
|
1125
|
+
with allure.step(f"远程关键字赋值: {var_name}"):
|
1126
|
+
try:
|
1127
|
+
remote_keyword_call_node = node.children[0]
|
1128
|
+
result = self.execute(remote_keyword_call_node)
|
943
1129
|
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
#
|
948
|
-
|
1130
|
+
if result is not None:
|
1131
|
+
# 注意:远程关键字客户端已经处理了新格式的返回值,
|
1132
|
+
# 这里接收到的result应该已经是主要返回值,而不是完整的字典格式
|
1133
|
+
# 但为了保险起见,我们仍然检查是否为新格式
|
1134
|
+
if isinstance(result, dict) and 'result' in result:
|
1135
|
+
# 如果仍然是新格式(可能是嵌套的远程调用),提取主要返回值
|
1136
|
+
main_result = result['result']
|
949
1137
|
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
1138
|
+
# 处理captures字段中的变量
|
1139
|
+
captures = result.get('captures', {})
|
1140
|
+
for capture_var, capture_value in captures.items():
|
1141
|
+
if capture_var.startswith('g_'):
|
1142
|
+
global_context.set_variable(
|
1143
|
+
capture_var, capture_value)
|
1144
|
+
else:
|
1145
|
+
self.variable_replacer.local_variables[
|
1146
|
+
capture_var] = capture_value
|
1147
|
+
self.test_context.set(capture_var, capture_value)
|
1148
|
+
|
1149
|
+
# 将主要结果赋值给指定变量
|
1150
|
+
actual_result = main_result
|
1151
|
+
else:
|
1152
|
+
# 传统格式,直接使用结果
|
1153
|
+
actual_result = result
|
1154
|
+
|
1155
|
+
# 检查变量名是否以g_开头,如果是则设置为全局变量
|
1156
|
+
if var_name.startswith('g_'):
|
1157
|
+
global_context.set_variable(var_name, actual_result)
|
1158
|
+
allure.attach(
|
1159
|
+
f"全局变量: {var_name}\n值: {actual_result}{line_info}",
|
1160
|
+
name="远程关键字赋值",
|
1161
|
+
attachment_type=allure.attachment_type.TEXT
|
1162
|
+
)
|
1163
|
+
else:
|
1164
|
+
# 存储在本地变量字典和测试上下文中
|
1165
|
+
self.variable_replacer.local_variables[
|
1166
|
+
var_name] = actual_result
|
1167
|
+
self.test_context.set(var_name, actual_result)
|
1168
|
+
allure.attach(
|
1169
|
+
f"变量: {var_name}\n值: {actual_result}{line_info}",
|
1170
|
+
name="远程关键字赋值",
|
1171
|
+
attachment_type=allure.attachment_type.TEXT
|
1172
|
+
)
|
958
1173
|
else:
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
f"变量: {var_name}\n值: {actual_result}{line_info}",
|
965
|
-
name="远程关键字赋值",
|
966
|
-
attachment_type=allure.attachment_type.TEXT
|
967
|
-
)
|
968
|
-
else:
|
969
|
-
error_msg = "远程关键字没有返回结果"
|
970
|
-
allure.attach(
|
971
|
-
f"变量: {var_name}\n错误: {error_msg}{line_info}",
|
972
|
-
name="远程关键字赋值失败",
|
973
|
-
attachment_type=allure.attachment_type.TEXT
|
974
|
-
)
|
975
|
-
raise Exception(error_msg)
|
976
|
-
except Exception as e:
|
977
|
-
# 如果异常不是我们刚才抛出的,记录异常信息
|
978
|
-
if "没有返回结果" not in str(e):
|
1174
|
+
error_msg = "远程关键字没有返回结果"
|
1175
|
+
raise Exception(error_msg)
|
1176
|
+
except Exception as e:
|
1177
|
+
# 在步骤内部记录异常详情
|
1178
|
+
error_details = f"执行AssignmentRemoteKeywordCall节点: {str(e)}{line_info}\n上下文: 执行AssignmentRemoteKeywordCall节点"
|
979
1179
|
allure.attach(
|
980
|
-
|
981
|
-
name="
|
1180
|
+
error_details,
|
1181
|
+
name="DSL执行异常",
|
982
1182
|
attachment_type=allure.attachment_type.TEXT
|
983
1183
|
)
|
984
|
-
|
1184
|
+
# 重新抛出异常,让外层的统一异常处理机制处理
|
1185
|
+
raise
|
985
1186
|
|
986
1187
|
def execute(self, node):
|
987
1188
|
"""执行AST节点"""
|
@@ -1018,7 +1219,20 @@ class DSLExecutor:
|
|
1018
1219
|
error_msg = f"未知的节点类型: {node.type}"
|
1019
1220
|
if self.enable_tracking and self.execution_tracker:
|
1020
1221
|
self.execution_tracker.finish_current_step(error=error_msg)
|
1021
|
-
|
1222
|
+
# 使用统一的异常处理机制
|
1223
|
+
self._handle_exception_with_line_info(
|
1224
|
+
Exception(error_msg), node, f"执行节点 {node.type}")
|
1225
|
+
|
1226
|
+
# 管理节点栈 - 将有行号的节点推入栈
|
1227
|
+
if hasattr(node, 'line_number') and node.line_number:
|
1228
|
+
self._node_stack.append(node)
|
1229
|
+
stack_pushed = True
|
1230
|
+
else:
|
1231
|
+
stack_pushed = False
|
1232
|
+
|
1233
|
+
# 设置当前节点
|
1234
|
+
old_node = self._current_node
|
1235
|
+
self._current_node = node
|
1022
1236
|
|
1023
1237
|
try:
|
1024
1238
|
result = handler(node)
|
@@ -1033,7 +1247,36 @@ class DSLExecutor:
|
|
1033
1247
|
if hasattr(node, 'line_number') and node.line_number:
|
1034
1248
|
error_msg += f" (行{node.line_number})"
|
1035
1249
|
self.execution_tracker.finish_current_step(error=error_msg)
|
1036
|
-
|
1250
|
+
|
1251
|
+
# 如果是控制流异常或已经是DSLExecutionError,直接重抛
|
1252
|
+
if isinstance(e, (BreakException, ContinueException, ReturnException, DSLExecutionError)):
|
1253
|
+
raise
|
1254
|
+
|
1255
|
+
# 如果是断言异常,保持原样但可能添加行号信息
|
1256
|
+
if isinstance(e, AssertionError):
|
1257
|
+
# 检查是否已经包含行号信息
|
1258
|
+
if not ("行号:" in str(e) or "行" in str(e)):
|
1259
|
+
line_info = self._get_line_info(node)
|
1260
|
+
if line_info:
|
1261
|
+
enhanced_msg = f"{str(e)}{line_info}"
|
1262
|
+
raise AssertionError(enhanced_msg) from e
|
1263
|
+
raise
|
1264
|
+
|
1265
|
+
# 其他异常使用统一处理机制
|
1266
|
+
# 对于这些节点类型,异常已经在步骤中记录过了,跳过重复记录
|
1267
|
+
step_handled_nodes = {
|
1268
|
+
'KeywordCall', 'Assignment', 'AssignmentKeywordCall',
|
1269
|
+
'ForLoop', 'RemoteKeywordCall', 'AssignmentRemoteKeywordCall'
|
1270
|
+
}
|
1271
|
+
skip_logging = node.type in step_handled_nodes
|
1272
|
+
self._handle_exception_with_line_info(
|
1273
|
+
e, node, f"执行{node.type}节点", skip_allure_logging=skip_logging)
|
1274
|
+
finally:
|
1275
|
+
# 恢复之前的节点
|
1276
|
+
self._current_node = old_node
|
1277
|
+
# 从栈中弹出节点
|
1278
|
+
if stack_pushed:
|
1279
|
+
self._node_stack.pop()
|
1037
1280
|
|
1038
1281
|
def _get_remote_keyword_description(self, node):
|
1039
1282
|
"""获取远程关键字调用的描述"""
|