pytest-dsl 0.6.0__py3-none-any.whl → 0.8.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/core/dsl_executor.py +96 -15
- pytest_dsl/core/lexer.py +21 -5
- pytest_dsl/core/parser.py +68 -5
- pytest_dsl/core/parsetab.py +71 -55
- pytest_dsl/core/utils.py +43 -23
- pytest_dsl/core/variable_utils.py +215 -70
- pytest_dsl-0.8.0.dist-info/METADATA +1088 -0
- {pytest_dsl-0.6.0.dist-info → pytest_dsl-0.8.0.dist-info}/RECORD +12 -13
- pytest_dsl/parsetab.py +0 -69
- pytest_dsl-0.6.0.dist-info/METADATA +0 -725
- {pytest_dsl-0.6.0.dist-info → pytest_dsl-0.8.0.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.6.0.dist-info → pytest_dsl-0.8.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.6.0.dist-info → pytest_dsl-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.6.0.dist-info → pytest_dsl-0.8.0.dist-info}/top_level.txt +0 -0
pytest_dsl/core/dsl_executor.py
CHANGED
@@ -13,6 +13,16 @@ from pytest_dsl.core.yaml_vars import yaml_vars
|
|
13
13
|
from pytest_dsl.core.variable_utils import VariableReplacer
|
14
14
|
|
15
15
|
|
16
|
+
class BreakException(Exception):
|
17
|
+
"""Break控制流异常"""
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
class ContinueException(Exception):
|
22
|
+
"""Continue控制流异常"""
|
23
|
+
pass
|
24
|
+
|
25
|
+
|
16
26
|
class DSLExecutor:
|
17
27
|
"""DSL执行器,负责执行解析后的AST
|
18
28
|
|
@@ -90,6 +100,15 @@ class DSLExecutor:
|
|
90
100
|
item_value = self.eval_expression(item)
|
91
101
|
result.append(item_value)
|
92
102
|
return result
|
103
|
+
elif expr_node.type == 'DictExpr':
|
104
|
+
# 处理字典表达式
|
105
|
+
result = {}
|
106
|
+
for item in expr_node.children:
|
107
|
+
# 每个item是DictItem节点,包含键和值
|
108
|
+
key_value = self.eval_expression(item.children[0])
|
109
|
+
value_value = self.eval_expression(item.children[1])
|
110
|
+
result[key_value] = value_value
|
111
|
+
return result
|
93
112
|
elif expr_node.type == 'BooleanExpr':
|
94
113
|
# 处理布尔值表达式
|
95
114
|
return expr_node.value
|
@@ -111,13 +130,14 @@ class DSLExecutor:
|
|
111
130
|
if value in self.variable_replacer.local_variables:
|
112
131
|
return self.variable_replacer.local_variables[value]
|
113
132
|
|
114
|
-
#
|
115
|
-
pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)\}'
|
133
|
+
# 定义扩展的变量引用模式,支持数组索引和字典键访问
|
134
|
+
pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)|(?:\[[^\]]+\]))*)\}'
|
116
135
|
# 检查整个字符串是否完全匹配单一变量引用模式
|
117
136
|
match = re.fullmatch(pattern, value)
|
118
137
|
if match:
|
119
|
-
|
120
|
-
|
138
|
+
var_ref = match.group(1)
|
139
|
+
# 使用新的变量路径解析器
|
140
|
+
return self.variable_replacer._parse_variable_path(var_ref)
|
121
141
|
else:
|
122
142
|
# 如果不是单一变量,则替换字符串中的所有变量引用
|
123
143
|
return self.variable_replacer.replace_in_string(value)
|
@@ -165,7 +185,7 @@ class DSLExecutor:
|
|
165
185
|
"""
|
166
186
|
left_value = self.eval_expression(expr_node.children[0])
|
167
187
|
right_value = self.eval_expression(expr_node.children[1])
|
168
|
-
operator = expr_node.value # 操作符: +, -, *,
|
188
|
+
operator = expr_node.value # 操作符: +, -, *, /, %
|
169
189
|
|
170
190
|
# 尝试类型转换 - 如果是字符串数字则转为数字
|
171
191
|
if isinstance(left_value, str) and str(left_value).replace('.', '', 1).isdigit():
|
@@ -200,6 +220,11 @@ class DSLExecutor:
|
|
200
220
|
if right_value == 0:
|
201
221
|
raise Exception("除法错误: 除数不能为0")
|
202
222
|
return left_value / right_value
|
223
|
+
elif operator == '%':
|
224
|
+
# 模运算时检查除数是否为0
|
225
|
+
if right_value == 0:
|
226
|
+
raise Exception("模运算错误: 除数不能为0")
|
227
|
+
return left_value % right_value
|
203
228
|
else:
|
204
229
|
raise Exception(f"未知的算术操作符: {operator}")
|
205
230
|
|
@@ -448,7 +473,24 @@ class DSLExecutor:
|
|
448
473
|
self.variable_replacer.local_variables[var_name] = i
|
449
474
|
self.test_context.set(var_name, i) # 同时添加到测试上下文
|
450
475
|
with allure.step(f"循环轮次: {var_name} = {i}"):
|
451
|
-
|
476
|
+
try:
|
477
|
+
self.execute(node.children[2])
|
478
|
+
except BreakException:
|
479
|
+
# 遇到break语句,退出循环
|
480
|
+
allure.attach(
|
481
|
+
f"在 {var_name} = {i} 时遇到break语句,退出循环",
|
482
|
+
name="循环Break",
|
483
|
+
attachment_type=allure.attachment_type.TEXT
|
484
|
+
)
|
485
|
+
break
|
486
|
+
except ContinueException:
|
487
|
+
# 遇到continue语句,跳过本次循环
|
488
|
+
allure.attach(
|
489
|
+
f"在 {var_name} = {i} 时遇到continue语句,跳过本次循环",
|
490
|
+
name="循环Continue",
|
491
|
+
attachment_type=allure.attachment_type.TEXT
|
492
|
+
)
|
493
|
+
continue
|
452
494
|
|
453
495
|
def _execute_keyword_call(self, node):
|
454
496
|
"""执行关键字调用"""
|
@@ -501,26 +543,63 @@ class DSLExecutor:
|
|
501
543
|
expr_node = node.children[0]
|
502
544
|
return self.eval_expression(expr_node)
|
503
545
|
|
546
|
+
@allure.step("执行break语句")
|
547
|
+
def _handle_break(self, node):
|
548
|
+
"""处理break语句
|
549
|
+
|
550
|
+
Args:
|
551
|
+
node: Break节点
|
552
|
+
|
553
|
+
Raises:
|
554
|
+
BreakException: 抛出异常来实现break控制流
|
555
|
+
"""
|
556
|
+
raise BreakException()
|
557
|
+
|
558
|
+
@allure.step("执行continue语句")
|
559
|
+
def _handle_continue(self, node):
|
560
|
+
"""处理continue语句
|
561
|
+
|
562
|
+
Args:
|
563
|
+
node: Continue节点
|
564
|
+
|
565
|
+
Raises:
|
566
|
+
ContinueException: 抛出异常来实现continue控制流
|
567
|
+
"""
|
568
|
+
raise ContinueException()
|
569
|
+
|
504
570
|
@allure.step("执行条件语句")
|
505
571
|
def _handle_if_statement(self, node):
|
506
|
-
"""处理if-else语句
|
572
|
+
"""处理if-elif-else语句
|
507
573
|
|
508
574
|
Args:
|
509
|
-
node: IfStatement节点,包含条件表达式、if分支和可选的else分支
|
575
|
+
node: IfStatement节点,包含条件表达式、if分支、可选的elif分支和可选的else分支
|
510
576
|
"""
|
577
|
+
# 首先检查if条件
|
511
578
|
condition = self.eval_expression(node.children[0])
|
512
579
|
|
513
|
-
# 将条件转换为布尔值进行评估
|
514
580
|
if condition:
|
515
581
|
# 执行if分支
|
516
582
|
with allure.step("执行if分支"):
|
517
583
|
return self.execute(node.children[1])
|
518
|
-
elif len(node.children) > 2:
|
519
|
-
# 如果存在else分支且条件为假,则执行else分支
|
520
|
-
with allure.step("执行else分支"):
|
521
|
-
return self.execute(node.children[2])
|
522
584
|
|
523
|
-
#
|
585
|
+
# 如果if条件为假,检查elif分支
|
586
|
+
for i in range(2, len(node.children)):
|
587
|
+
child = node.children[i]
|
588
|
+
|
589
|
+
# 如果是ElifClause节点
|
590
|
+
if hasattr(child, 'type') and child.type == 'ElifClause':
|
591
|
+
elif_condition = self.eval_expression(child.children[0])
|
592
|
+
if elif_condition:
|
593
|
+
with allure.step(f"执行elif分支 {i-1}"):
|
594
|
+
return self.execute(child.children[1])
|
595
|
+
|
596
|
+
# 如果是普通的statements节点(else分支)
|
597
|
+
elif not hasattr(child, 'type') or child.type == 'Statements':
|
598
|
+
# 这是else分支,只有在所有前面的条件都为假时才执行
|
599
|
+
with allure.step("执行else分支"):
|
600
|
+
return self.execute(child)
|
601
|
+
|
602
|
+
# 如果所有条件都为假且没有else分支,则不执行任何操作
|
524
603
|
return None
|
525
604
|
|
526
605
|
def _execute_remote_keyword_call(self, node):
|
@@ -641,7 +720,9 @@ class DSLExecutor:
|
|
641
720
|
'CustomKeyword': lambda _: None, # 添加对CustomKeyword节点的处理,只需注册不需执行
|
642
721
|
'RemoteImport': self._handle_remote_import,
|
643
722
|
'RemoteKeywordCall': self._execute_remote_keyword_call,
|
644
|
-
'AssignmentRemoteKeywordCall': self._handle_assignment_remote_keyword_call
|
723
|
+
'AssignmentRemoteKeywordCall': self._handle_assignment_remote_keyword_call,
|
724
|
+
'Break': self._handle_break,
|
725
|
+
'Continue': self._handle_continue
|
645
726
|
}
|
646
727
|
|
647
728
|
handler = handlers.get(node.type)
|
pytest_dsl/core/lexer.py
CHANGED
@@ -12,10 +12,13 @@ reserved = {
|
|
12
12
|
'False': 'FALSE', # 添加布尔值支持
|
13
13
|
'return': 'RETURN', # 添加return关键字支持
|
14
14
|
'else': 'ELSE', # 添加else关键字支持
|
15
|
+
'elif': 'ELIF', # 添加elif关键字支持
|
15
16
|
'if': 'IF', # 添加if关键字支持
|
16
17
|
'as': 'AS', # 添加as关键字支持,用于远程关键字别名
|
17
18
|
'function': 'FUNCTION', # 添加function关键字支持,用于自定义关键字定义
|
18
|
-
'teardown': 'TEARDOWN' # 添加teardown关键字支持,用于清理操作
|
19
|
+
'teardown': 'TEARDOWN', # 添加teardown关键字支持,用于清理操作
|
20
|
+
'break': 'BREAK', # 添加break关键字支持,用于循环控制
|
21
|
+
'continue': 'CONTINUE' # 添加continue关键字支持,用于循环控制
|
19
22
|
}
|
20
23
|
|
21
24
|
# token 名称列表
|
@@ -29,6 +32,8 @@ tokens = [
|
|
29
32
|
'RPAREN',
|
30
33
|
'LBRACKET',
|
31
34
|
'RBRACKET',
|
35
|
+
'LBRACE', # 左大括号 {,用于字典字面量
|
36
|
+
'RBRACE', # 右大括号 },用于字典字面量
|
32
37
|
'COLON',
|
33
38
|
'COMMA',
|
34
39
|
'PLACEHOLDER',
|
@@ -50,6 +55,7 @@ tokens = [
|
|
50
55
|
'MINUS', # 减法 -
|
51
56
|
'TIMES', # 乘法 *
|
52
57
|
'DIVIDE', # 除法 /
|
58
|
+
'MODULO', # 模运算 %
|
53
59
|
'PIPE', # 管道符 |,用于远程关键字调用
|
54
60
|
] + list(reserved.values())
|
55
61
|
|
@@ -58,6 +64,8 @@ t_LPAREN = r'\('
|
|
58
64
|
t_RPAREN = r'\)'
|
59
65
|
t_LBRACKET = r'\['
|
60
66
|
t_RBRACKET = r'\]'
|
67
|
+
t_LBRACE = r'\{'
|
68
|
+
t_RBRACE = r'\}'
|
61
69
|
t_COLON = r':'
|
62
70
|
t_COMMA = r','
|
63
71
|
t_EQUALS = r'='
|
@@ -71,9 +79,13 @@ t_PLUS = r'\+'
|
|
71
79
|
t_MINUS = r'-'
|
72
80
|
t_TIMES = r'\*'
|
73
81
|
t_DIVIDE = r'/'
|
82
|
+
t_MODULO = r'%'
|
74
83
|
|
75
|
-
# 增加PLACEHOLDER规则,匹配 ${变量名}
|
76
|
-
|
84
|
+
# 增加PLACEHOLDER规则,匹配 ${变量名} 格式,支持点号、数组索引和字典键访问
|
85
|
+
# 匹配: ${variable}, ${obj.prop}, ${arr[0]}, ${dict["key"]}, ${obj[0].prop} 等
|
86
|
+
t_PLACEHOLDER = (r'\$\{[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
|
87
|
+
r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
|
88
|
+
r'|(?:\[[^\]]+\]))*\}')
|
77
89
|
|
78
90
|
# 添加管道符的正则表达式定义
|
79
91
|
t_PIPE = r'\|'
|
@@ -143,8 +155,12 @@ def t_IMPORT_KEYWORD(t):
|
|
143
155
|
|
144
156
|
|
145
157
|
def t_NUMBER(t):
|
146
|
-
r'\d+'
|
147
|
-
|
158
|
+
r'\d+(\.\d+)?'
|
159
|
+
# 如果包含小数点,转换为浮点数;否则转换为整数
|
160
|
+
if '.' in t.value:
|
161
|
+
t.value = float(t.value)
|
162
|
+
else:
|
163
|
+
t.value = int(t.value)
|
148
164
|
return t
|
149
165
|
|
150
166
|
|
pytest_dsl/core/parser.py
CHANGED
@@ -14,7 +14,7 @@ precedence = (
|
|
14
14
|
('left', 'COMMA'),
|
15
15
|
('left', 'GT', 'LT', 'GE', 'LE', 'EQ', 'NE'), # 比较运算符优先级
|
16
16
|
('left', 'PLUS', 'MINUS'), # 加减运算符优先级
|
17
|
-
('left', 'TIMES', 'DIVIDE'), #
|
17
|
+
('left', 'TIMES', 'DIVIDE', 'MODULO'), # 乘除模运算符优先级
|
18
18
|
('right', 'EQUALS'),
|
19
19
|
)
|
20
20
|
|
@@ -124,7 +124,9 @@ def p_statement(p):
|
|
124
124
|
| loop
|
125
125
|
| custom_keyword
|
126
126
|
| return_statement
|
127
|
-
| if_statement
|
127
|
+
| if_statement
|
128
|
+
| break_statement
|
129
|
+
| continue_statement'''
|
128
130
|
p[0] = p[1]
|
129
131
|
|
130
132
|
|
@@ -158,6 +160,7 @@ def p_expr_atom(p):
|
|
158
160
|
| ID
|
159
161
|
| boolean_expr
|
160
162
|
| list_expr
|
163
|
+
| dict_expr
|
161
164
|
| LPAREN expression RPAREN'''
|
162
165
|
if p[1] == '(':
|
163
166
|
# 处理括号表达式,直接返回括号内的表达式节点
|
@@ -197,6 +200,29 @@ def p_list_item(p):
|
|
197
200
|
p[0] = p[1]
|
198
201
|
|
199
202
|
|
203
|
+
def p_dict_expr(p):
|
204
|
+
'''dict_expr : LBRACE dict_items RBRACE
|
205
|
+
| LBRACE RBRACE'''
|
206
|
+
if len(p) == 4:
|
207
|
+
p[0] = Node('DictExpr', children=p[2])
|
208
|
+
else:
|
209
|
+
p[0] = Node('DictExpr', children=[]) # 空字典
|
210
|
+
|
211
|
+
|
212
|
+
def p_dict_items(p):
|
213
|
+
'''dict_items : dict_item
|
214
|
+
| dict_item COMMA dict_items'''
|
215
|
+
if len(p) == 2:
|
216
|
+
p[0] = [p[1]]
|
217
|
+
else:
|
218
|
+
p[0] = [p[1]] + p[3]
|
219
|
+
|
220
|
+
|
221
|
+
def p_dict_item(p):
|
222
|
+
'''dict_item : expression COLON expression'''
|
223
|
+
p[0] = Node('DictItem', children=[p[1], p[3]])
|
224
|
+
|
225
|
+
|
200
226
|
def p_loop(p):
|
201
227
|
'''loop : FOR ID IN RANGE LPAREN expression COMMA expression RPAREN DO statements END'''
|
202
228
|
p[0] = Node('ForLoop', [p[6], p[8], p[11]], p[2])
|
@@ -278,13 +304,47 @@ def p_return_statement(p):
|
|
278
304
|
p[0] = Node('Return', [p[2]])
|
279
305
|
|
280
306
|
|
307
|
+
def p_break_statement(p):
|
308
|
+
'''break_statement : BREAK'''
|
309
|
+
p[0] = Node('Break', [])
|
310
|
+
|
311
|
+
|
312
|
+
def p_continue_statement(p):
|
313
|
+
'''continue_statement : CONTINUE'''
|
314
|
+
p[0] = Node('Continue', [])
|
315
|
+
|
316
|
+
|
281
317
|
def p_if_statement(p):
|
282
318
|
'''if_statement : IF expression DO statements END
|
283
|
-
| IF expression DO statements
|
319
|
+
| IF expression DO statements elif_clauses END
|
320
|
+
| IF expression DO statements ELSE statements END
|
321
|
+
| IF expression DO statements elif_clauses ELSE statements END'''
|
284
322
|
if len(p) == 6:
|
323
|
+
# if condition do statements end
|
285
324
|
p[0] = Node('IfStatement', [p[2], p[4]], None)
|
286
|
-
|
325
|
+
elif len(p) == 7:
|
326
|
+
# if condition do statements elif_clauses end
|
327
|
+
p[0] = Node('IfStatement', [p[2], p[4]] + p[5], None)
|
328
|
+
elif len(p) == 8:
|
329
|
+
# if condition do statements else statements end
|
287
330
|
p[0] = Node('IfStatement', [p[2], p[4], p[6]], None)
|
331
|
+
else:
|
332
|
+
# if condition do statements elif_clauses else statements end
|
333
|
+
p[0] = Node('IfStatement', [p[2], p[4]] + p[5] + [p[7]], None)
|
334
|
+
|
335
|
+
|
336
|
+
def p_elif_clauses(p):
|
337
|
+
'''elif_clauses : elif_clause
|
338
|
+
| elif_clause elif_clauses'''
|
339
|
+
if len(p) == 2:
|
340
|
+
p[0] = [p[1]]
|
341
|
+
else:
|
342
|
+
p[0] = [p[1]] + p[2]
|
343
|
+
|
344
|
+
|
345
|
+
def p_elif_clause(p):
|
346
|
+
'''elif_clause : ELIF expression DO statements'''
|
347
|
+
p[0] = Node('ElifClause', [p[2], p[4]], None)
|
288
348
|
|
289
349
|
|
290
350
|
def p_comparison_expr(p):
|
@@ -319,7 +379,8 @@ def p_arithmetic_expr(p):
|
|
319
379
|
'''arithmetic_expr : expression PLUS expression
|
320
380
|
| expression MINUS expression
|
321
381
|
| expression TIMES expression
|
322
|
-
| expression DIVIDE expression
|
382
|
+
| expression DIVIDE expression
|
383
|
+
| expression MODULO expression'''
|
323
384
|
|
324
385
|
# 根据规则索引判断使用的是哪个操作符
|
325
386
|
if p.slice[2].type == 'PLUS':
|
@@ -330,6 +391,8 @@ def p_arithmetic_expr(p):
|
|
330
391
|
operator = '*'
|
331
392
|
elif p.slice[2].type == 'DIVIDE':
|
332
393
|
operator = '/'
|
394
|
+
elif p.slice[2].type == 'MODULO':
|
395
|
+
operator = '%'
|
333
396
|
else:
|
334
397
|
print(f"警告: 无法识别的操作符类型 {p.slice[2].type}")
|
335
398
|
operator = None
|