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.
- pytest_dsl/__init__.py +190 -3
- pytest_dsl/cli.py +37 -260
- pytest_dsl/core/custom_keyword_manager.py +114 -14
- pytest_dsl/core/dsl_executor.py +549 -166
- pytest_dsl/core/dsl_executor_utils.py +21 -22
- pytest_dsl/core/execution_tracker.py +291 -0
- pytest_dsl/core/hook_manager.py +87 -0
- pytest_dsl/core/hookable_executor.py +134 -0
- pytest_dsl/core/hookable_keyword_manager.py +106 -0
- pytest_dsl/core/hookspecs.py +175 -0
- pytest_dsl/core/keyword_loader.py +402 -0
- pytest_dsl/core/keyword_manager.py +29 -23
- pytest_dsl/core/parser.py +94 -18
- pytest_dsl/core/validator.py +417 -0
- pytest_dsl/core/yaml_loader.py +142 -42
- pytest_dsl/core/yaml_vars.py +90 -7
- pytest_dsl/plugin.py +10 -1
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/METADATA +1 -1
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/RECORD +23 -16
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.13.0.dist-info → pytest_dsl-0.15.0.dist-info}/top_level.txt +0 -0
@@ -18,7 +18,7 @@ class KeywordManager:
|
|
18
18
|
|
19
19
|
def register(self, name: str, parameters: List[Dict], source_info: Optional[Dict] = None):
|
20
20
|
"""关键字注册装饰器
|
21
|
-
|
21
|
+
|
22
22
|
Args:
|
23
23
|
name: 关键字名称
|
24
24
|
parameters: 参数列表
|
@@ -29,23 +29,29 @@ class KeywordManager:
|
|
29
29
|
def wrapper(**kwargs):
|
30
30
|
# 获取自定义步骤名称,如果未指定则使用关键字名称
|
31
31
|
step_name = kwargs.pop('step_name', name)
|
32
|
-
|
32
|
+
|
33
|
+
# 检查是否已经在DSL执行器的步骤中,避免重复记录
|
34
|
+
skip_logging = kwargs.pop('skip_logging', False)
|
35
|
+
|
33
36
|
with allure.step(f"{step_name}"):
|
34
37
|
try:
|
35
38
|
result = func(**kwargs)
|
36
|
-
|
39
|
+
if not skip_logging:
|
40
|
+
self._log_execution(step_name, kwargs, result)
|
37
41
|
return result
|
38
42
|
except Exception as e:
|
39
|
-
|
43
|
+
if not skip_logging:
|
44
|
+
self._log_failure(step_name, kwargs, e)
|
40
45
|
raise
|
41
46
|
|
42
47
|
param_list = [Parameter(**p) for p in parameters]
|
43
48
|
mapping = {p.name: p.mapping for p in param_list}
|
44
|
-
defaults = {
|
45
|
-
|
49
|
+
defaults = {
|
50
|
+
p.mapping: p.default for p in param_list if p.default is not None}
|
51
|
+
|
46
52
|
# 自动添加 step_name 到 mapping 中
|
47
53
|
mapping["步骤名称"] = "step_name"
|
48
|
-
|
54
|
+
|
49
55
|
# 构建关键字信息,包含来源信息
|
50
56
|
keyword_info = {
|
51
57
|
'func': wrapper,
|
@@ -53,14 +59,14 @@ class KeywordManager:
|
|
53
59
|
'parameters': param_list,
|
54
60
|
'defaults': defaults # 存储默认值
|
55
61
|
}
|
56
|
-
|
62
|
+
|
57
63
|
# 添加来源信息
|
58
64
|
if source_info:
|
59
65
|
keyword_info.update(source_info)
|
60
66
|
else:
|
61
67
|
# 尝试从函数模块推断来源信息
|
62
68
|
keyword_info.update(self._infer_source_info(func))
|
63
|
-
|
69
|
+
|
64
70
|
self._keywords[name] = keyword_info
|
65
71
|
return wrapper
|
66
72
|
return decorator
|
@@ -68,11 +74,11 @@ class KeywordManager:
|
|
68
74
|
def _infer_source_info(self, func: Callable) -> Dict:
|
69
75
|
"""从函数推断来源信息"""
|
70
76
|
source_info = {}
|
71
|
-
|
77
|
+
|
72
78
|
if hasattr(func, '__module__'):
|
73
79
|
module_name = func.__module__
|
74
80
|
source_info['module_name'] = module_name
|
75
|
-
|
81
|
+
|
76
82
|
if module_name.startswith('pytest_dsl.keywords'):
|
77
83
|
# 内置关键字
|
78
84
|
source_info['source_type'] = 'builtin'
|
@@ -90,13 +96,13 @@ class KeywordManager:
|
|
90
96
|
source_info['source_name'] = parts[0]
|
91
97
|
else:
|
92
98
|
source_info['source_name'] = module_name
|
93
|
-
|
99
|
+
|
94
100
|
return source_info
|
95
101
|
|
96
|
-
def register_with_source(self, name: str, parameters: List[Dict],
|
97
|
-
|
102
|
+
def register_with_source(self, name: str, parameters: List[Dict],
|
103
|
+
source_type: str, source_name: str, **kwargs):
|
98
104
|
"""带来源信息的关键字注册装饰器
|
99
|
-
|
105
|
+
|
100
106
|
Args:
|
101
107
|
name: 关键字名称
|
102
108
|
parameters: 参数列表
|
@@ -116,18 +122,18 @@ class KeywordManager:
|
|
116
122
|
keyword_info = self._keywords.get(keyword_name)
|
117
123
|
if not keyword_info:
|
118
124
|
raise KeyError(f"未注册的关键字: {keyword_name}")
|
119
|
-
|
125
|
+
|
120
126
|
# 应用默认值
|
121
127
|
final_params = {}
|
122
128
|
defaults = keyword_info.get('defaults', {})
|
123
|
-
|
129
|
+
|
124
130
|
# 首先设置所有默认值
|
125
131
|
for param_key, default_value in defaults.items():
|
126
132
|
final_params[param_key] = default_value
|
127
|
-
|
133
|
+
|
128
134
|
# 然后用传入的参数覆盖默认值
|
129
135
|
final_params.update(params)
|
130
|
-
|
136
|
+
|
131
137
|
return keyword_info['func'](**final_params)
|
132
138
|
|
133
139
|
def get_keyword_info(self, keyword_name: str) -> Dict:
|
@@ -135,7 +141,7 @@ class KeywordManager:
|
|
135
141
|
keyword_info = self._keywords.get(keyword_name)
|
136
142
|
if not keyword_info:
|
137
143
|
return None
|
138
|
-
|
144
|
+
|
139
145
|
# 动态添加step_name参数到参数列表中
|
140
146
|
if not any(p.name == "步骤名称" for p in keyword_info['parameters']):
|
141
147
|
keyword_info['parameters'].append(Parameter(
|
@@ -143,19 +149,19 @@ class KeywordManager:
|
|
143
149
|
mapping="step_name",
|
144
150
|
description="自定义的步骤名称,用于在报告中显示"
|
145
151
|
))
|
146
|
-
|
152
|
+
|
147
153
|
return keyword_info
|
148
154
|
|
149
155
|
def get_keywords_by_source(self) -> Dict[str, List[str]]:
|
150
156
|
"""按来源分组获取关键字"""
|
151
157
|
by_source = {}
|
152
|
-
|
158
|
+
|
153
159
|
for name, info in self._keywords.items():
|
154
160
|
source_name = info.get('source_name', '未知来源')
|
155
161
|
if source_name not in by_source:
|
156
162
|
by_source[source_name] = []
|
157
163
|
by_source[source_name].append(name)
|
158
|
-
|
164
|
+
|
159
165
|
return by_source
|
160
166
|
|
161
167
|
def _log_execution(self, keyword_name: str, params: Dict, result: Any) -> None:
|
pytest_dsl/core/parser.py
CHANGED
@@ -1,12 +1,30 @@
|
|
1
1
|
import ply.yacc as yacc
|
2
|
-
from pytest_dsl.core.lexer import tokens
|
2
|
+
from pytest_dsl.core.lexer import tokens
|
3
3
|
|
4
4
|
|
5
5
|
class Node:
|
6
|
-
def __init__(self, type, children=None, value=None
|
6
|
+
def __init__(self, type, children=None, value=None, line_number=None,
|
7
|
+
column=None):
|
7
8
|
self.type = type
|
8
9
|
self.children = children if children else []
|
9
10
|
self.value = value
|
11
|
+
self.line_number = line_number # 添加行号信息
|
12
|
+
self.column = column # 添加列号信息
|
13
|
+
|
14
|
+
def set_position(self, line_number, column=None):
|
15
|
+
"""设置节点位置信息"""
|
16
|
+
self.line_number = line_number
|
17
|
+
self.column = column
|
18
|
+
return self
|
19
|
+
|
20
|
+
def get_position_info(self):
|
21
|
+
"""获取位置信息的字符串表示"""
|
22
|
+
if self.line_number is not None:
|
23
|
+
if self.column is not None:
|
24
|
+
return f"行{self.line_number}:列{self.column}"
|
25
|
+
else:
|
26
|
+
return f"行{self.line_number}"
|
27
|
+
return "位置未知"
|
10
28
|
|
11
29
|
|
12
30
|
# 定义优先级和结合性
|
@@ -25,17 +43,23 @@ def p_start(p):
|
|
25
43
|
| statements teardown
|
26
44
|
| statements'''
|
27
45
|
|
46
|
+
# 获取起始行号
|
47
|
+
line_number = getattr(p.slice[1], 'lineno', 1) if len(p) > 1 else 1
|
48
|
+
|
28
49
|
if len(p) == 4:
|
29
|
-
p[0] = Node('Start', [p[1], p[2], p[3]])
|
50
|
+
p[0] = Node('Start', [p[1], p[2], p[3]], line_number=line_number)
|
30
51
|
elif len(p) == 3:
|
31
52
|
# 判断第二个元素是teardown还是statements
|
32
53
|
if p[2].type == 'Teardown':
|
33
|
-
p[0] = Node('Start', [Node('Metadata', []
|
54
|
+
p[0] = Node('Start', [Node('Metadata', [],
|
55
|
+
line_number=line_number), p[1], p[2]],
|
56
|
+
line_number=line_number)
|
34
57
|
else:
|
35
|
-
p[0] = Node('Start', [p[1], p[2]])
|
58
|
+
p[0] = Node('Start', [p[1], p[2]], line_number=line_number)
|
36
59
|
else:
|
37
60
|
# 没有metadata和teardown
|
38
|
-
p[0] = Node('Start', [Node('Metadata', []
|
61
|
+
p[0] = Node('Start', [Node('Metadata', [],
|
62
|
+
line_number=line_number), p[1]], line_number=line_number)
|
39
63
|
|
40
64
|
|
41
65
|
def p_metadata(p):
|
@@ -135,12 +159,17 @@ def p_assignment(p):
|
|
135
159
|
'''assignment : ID EQUALS expression
|
136
160
|
| ID EQUALS keyword_call
|
137
161
|
| ID EQUALS remote_keyword_call'''
|
162
|
+
line_number = getattr(p.slice[1], 'lineno', None)
|
163
|
+
|
138
164
|
if isinstance(p[3], Node) and p[3].type == 'KeywordCall':
|
139
|
-
p[0] = Node('AssignmentKeywordCall', [p[3]],
|
165
|
+
p[0] = Node('AssignmentKeywordCall', [p[3]],
|
166
|
+
p[1], line_number=line_number)
|
140
167
|
elif isinstance(p[3], Node) and p[3].type == 'RemoteKeywordCall':
|
141
|
-
p[0] = Node('AssignmentRemoteKeywordCall', [
|
168
|
+
p[0] = Node('AssignmentRemoteKeywordCall', [
|
169
|
+
p[3]], p[1], line_number=line_number)
|
142
170
|
else:
|
143
|
-
p[0] = Node('Assignment', value=p[1], children=[
|
171
|
+
p[0] = Node('Assignment', value=p[1], children=[
|
172
|
+
p[3]], line_number=line_number)
|
144
173
|
|
145
174
|
|
146
175
|
def p_expression(p):
|
@@ -225,17 +254,20 @@ def p_dict_item(p):
|
|
225
254
|
|
226
255
|
|
227
256
|
def p_loop(p):
|
228
|
-
'''loop : FOR ID IN RANGE LPAREN expression COMMA expression RPAREN DO statements END'''
|
229
|
-
|
257
|
+
'''loop : FOR ID IN RANGE LPAREN expression COMMA expression RPAREN DO statements END''' # noqa: E501
|
258
|
+
line_number = getattr(p.slice[1], 'lineno', None)
|
259
|
+
p[0] = Node('ForLoop', [p[6], p[8], p[11]], p[2], line_number=line_number)
|
230
260
|
|
231
261
|
|
232
262
|
def p_keyword_call(p):
|
233
263
|
'''keyword_call : LBRACKET ID RBRACKET COMMA parameter_list
|
234
264
|
| LBRACKET ID RBRACKET'''
|
265
|
+
line_number = getattr(p.slice[1], 'lineno', None)
|
266
|
+
|
235
267
|
if len(p) == 6:
|
236
|
-
p[0] = Node('KeywordCall', [p[5]], p[2])
|
268
|
+
p[0] = Node('KeywordCall', [p[5]], p[2], line_number=line_number)
|
237
269
|
else:
|
238
|
-
p[0] = Node('KeywordCall', [[]], p[2])
|
270
|
+
p[0] = Node('KeywordCall', [[]], p[2], line_number=line_number)
|
239
271
|
|
240
272
|
|
241
273
|
def p_parameter_list(p):
|
@@ -268,7 +300,7 @@ def p_data_source(p):
|
|
268
300
|
|
269
301
|
|
270
302
|
def p_custom_keyword(p):
|
271
|
-
'''custom_keyword : FUNCTION ID LPAREN param_definitions RPAREN DO statements END'''
|
303
|
+
'''custom_keyword : FUNCTION ID LPAREN param_definitions RPAREN DO statements END''' # noqa: E501
|
272
304
|
p[0] = Node('CustomKeyword', [p[4], p[7]], p[2])
|
273
305
|
|
274
306
|
|
@@ -319,7 +351,7 @@ def p_if_statement(p):
|
|
319
351
|
'''if_statement : IF expression DO statements END
|
320
352
|
| IF expression DO statements elif_clauses END
|
321
353
|
| IF expression DO statements ELSE statements END
|
322
|
-
| IF expression DO statements elif_clauses ELSE statements END'''
|
354
|
+
| IF expression DO statements elif_clauses ELSE statements END''' # noqa: E501
|
323
355
|
if len(p) == 6:
|
324
356
|
# if condition do statements end
|
325
357
|
p[0] = Node('IfStatement', [p[2], p[4]], None)
|
@@ -401,17 +433,61 @@ def p_arithmetic_expr(p):
|
|
401
433
|
p[0] = Node('ArithmeticExpr', [p[1], p[3]], operator)
|
402
434
|
|
403
435
|
|
436
|
+
# 全局变量用于存储解析错误
|
437
|
+
_parse_errors = []
|
438
|
+
|
439
|
+
|
404
440
|
def p_error(p):
|
441
|
+
global _parse_errors
|
405
442
|
if p:
|
406
|
-
|
407
|
-
|
443
|
+
error_msg = (f"语法错误: 在第 {p.lineno} 行, 位置 {p.lexpos}, "
|
444
|
+
f"Token {p.type}, 值: {p.value}")
|
445
|
+
_parse_errors.append({
|
446
|
+
'message': error_msg,
|
447
|
+
'line': p.lineno,
|
448
|
+
'position': p.lexpos,
|
449
|
+
'token_type': p.type,
|
450
|
+
'token_value': p.value
|
451
|
+
})
|
452
|
+
# 不再直接打印,而是存储错误信息
|
408
453
|
else:
|
409
|
-
|
454
|
+
error_msg = "语法错误: 在文件末尾"
|
455
|
+
_parse_errors.append({
|
456
|
+
'message': error_msg,
|
457
|
+
'line': None,
|
458
|
+
'position': None,
|
459
|
+
'token_type': None,
|
460
|
+
'token_value': None
|
461
|
+
})
|
410
462
|
|
411
463
|
|
412
464
|
def get_parser(debug=False):
|
413
465
|
return yacc.yacc(debug=debug)
|
414
466
|
|
467
|
+
|
468
|
+
def parse_with_error_handling(content, lexer=None):
|
469
|
+
"""带错误处理的解析函数
|
470
|
+
|
471
|
+
Args:
|
472
|
+
content: DSL内容
|
473
|
+
lexer: 词法分析器实例
|
474
|
+
|
475
|
+
Returns:
|
476
|
+
tuple: (AST节点, 错误列表)
|
477
|
+
"""
|
478
|
+
global _parse_errors
|
479
|
+
_parse_errors = [] # 清空之前的错误
|
480
|
+
|
481
|
+
if lexer is None:
|
482
|
+
from pytest_dsl.core.lexer import get_lexer
|
483
|
+
lexer = get_lexer()
|
484
|
+
|
485
|
+
parser = get_parser()
|
486
|
+
ast = parser.parse(content, lexer=lexer)
|
487
|
+
|
488
|
+
# 返回AST和错误列表
|
489
|
+
return ast, _parse_errors.copy()
|
490
|
+
|
415
491
|
# 定义远程关键字调用的语法规则
|
416
492
|
|
417
493
|
|