pytest-dsl 0.2.0__py3-none-any.whl → 0.3.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.
pytest_dsl/cli.py CHANGED
@@ -5,12 +5,16 @@ pytest-dsl命令行入口
5
5
  """
6
6
 
7
7
  import sys
8
+ import argparse
8
9
  import pytest
10
+ import os
9
11
  from pathlib import Path
10
12
 
11
13
  from pytest_dsl.core.lexer import get_lexer
12
14
  from pytest_dsl.core.parser import get_parser
13
15
  from pytest_dsl.core.dsl_executor import DSLExecutor
16
+ from pytest_dsl.core.yaml_vars import yaml_vars
17
+ from pytest_dsl.core.auto_directory import SETUP_FILE_NAME, TEARDOWN_FILE_NAME, execute_hook_file
14
18
 
15
19
 
16
20
  def read_file(filename):
@@ -19,26 +23,121 @@ def read_file(filename):
19
23
  return f.read()
20
24
 
21
25
 
26
+ def parse_args():
27
+ """解析命令行参数"""
28
+ parser = argparse.ArgumentParser(description='执行DSL测试文件')
29
+ parser.add_argument('path', help='要执行的DSL文件路径或包含DSL文件的目录')
30
+ parser.add_argument('--yaml-vars', action='append', default=[],
31
+ help='YAML变量文件路径,可以指定多个文件 (例如: --yaml-vars vars1.yaml --yaml-vars vars2.yaml)')
32
+ parser.add_argument('--yaml-vars-dir', default=None,
33
+ help='YAML变量文件目录路径,将加载该目录下所有.yaml文件')
34
+
35
+ return parser.parse_args()
36
+
37
+
38
+ def load_yaml_variables(args):
39
+ """从命令行参数加载YAML变量"""
40
+ # 加载单个YAML文件
41
+ if args.yaml_vars:
42
+ yaml_vars.load_yaml_files(args.yaml_vars)
43
+ print(f"已加载YAML变量文件: {', '.join(args.yaml_vars)}")
44
+
45
+ # 加载目录中的YAML文件
46
+ if args.yaml_vars_dir:
47
+ yaml_vars_dir = args.yaml_vars_dir
48
+ try:
49
+ yaml_vars.load_from_directory(yaml_vars_dir)
50
+ print(f"已加载YAML变量目录: {yaml_vars_dir}")
51
+ loaded_files = yaml_vars.get_loaded_files()
52
+ if loaded_files:
53
+ dir_files = [f for f in loaded_files if Path(f).parent == Path(yaml_vars_dir)]
54
+ if dir_files:
55
+ print(f"目录中加载的文件: {', '.join(dir_files)}")
56
+ except NotADirectoryError:
57
+ print(f"YAML变量目录不存在: {yaml_vars_dir}")
58
+ sys.exit(1)
59
+
60
+
61
+ def execute_dsl_file(file_path, lexer, parser, executor):
62
+ """执行单个DSL文件"""
63
+ try:
64
+ print(f"执行文件: {file_path}")
65
+ dsl_code = read_file(file_path)
66
+ ast = parser.parse(dsl_code, lexer=lexer)
67
+ executor.execute(ast)
68
+ return True
69
+ except Exception as e:
70
+ print(f"执行失败 {file_path}: {e}")
71
+ return False
72
+
73
+
74
+ def find_dsl_files(directory):
75
+ """查找目录中的所有DSL文件"""
76
+ dsl_files = []
77
+ for root, _, files in os.walk(directory):
78
+ for file in files:
79
+ if file.endswith(('.dsl', '.auto')) and file not in [SETUP_FILE_NAME, TEARDOWN_FILE_NAME]:
80
+ dsl_files.append(os.path.join(root, file))
81
+ return dsl_files
82
+
83
+
22
84
  def main():
23
85
  """命令行入口点"""
24
- if len(sys.argv) < 2:
25
- print("用法: python -m pytest_dsl.cli <dsl_file>")
26
- sys.exit(1)
27
-
28
- filename = sys.argv[1]
86
+ args = parse_args()
87
+ path = args.path
88
+
89
+ # 加载YAML变量
90
+ load_yaml_variables(args)
29
91
 
30
92
  lexer = get_lexer()
31
93
  parser = get_parser()
32
94
  executor = DSLExecutor()
33
95
 
34
- try:
35
- dsl_code = read_file(filename)
36
- ast = parser.parse(dsl_code, lexer=lexer)
37
- executor.execute(ast)
38
- except Exception as e:
39
- print(f"执行失败: {e}")
96
+ # 检查路径是文件还是目录
97
+ if os.path.isfile(path):
98
+ # 执行单个文件
99
+ success = execute_dsl_file(path, lexer, parser, executor)
100
+ if not success:
101
+ sys.exit(1)
102
+ elif os.path.isdir(path):
103
+ # 执行目录中的所有DSL文件
104
+ print(f"执行目录: {path}")
105
+
106
+ # 先执行目录的setup文件(如果存在)
107
+ setup_file = os.path.join(path, SETUP_FILE_NAME)
108
+ if os.path.exists(setup_file):
109
+ execute_hook_file(Path(setup_file), True, path)
110
+
111
+ # 查找并执行所有DSL文件
112
+ dsl_files = find_dsl_files(path)
113
+ if not dsl_files:
114
+ print(f"目录中没有找到DSL文件: {path}")
115
+ sys.exit(1)
116
+
117
+ print(f"找到 {len(dsl_files)} 个DSL文件")
118
+
119
+ # 执行所有DSL文件
120
+ failures = 0
121
+ for file_path in dsl_files:
122
+ success = execute_dsl_file(file_path, lexer, parser, executor)
123
+ if not success:
124
+ failures += 1
125
+
126
+ # 最后执行目录的teardown文件(如果存在)
127
+ teardown_file = os.path.join(path, TEARDOWN_FILE_NAME)
128
+ if os.path.exists(teardown_file):
129
+ execute_hook_file(Path(teardown_file), False, path)
130
+
131
+ # 如果有失败的测试,返回非零退出码
132
+ if failures > 0:
133
+ print(f"总计 {failures}/{len(dsl_files)} 个测试失败")
134
+ sys.exit(1)
135
+ else:
136
+ print(f"所有 {len(dsl_files)} 个测试成功完成")
137
+ else:
138
+ print(f"路径不存在: {path}")
40
139
  sys.exit(1)
41
140
 
42
141
 
43
142
  if __name__ == '__main__':
44
- main()
143
+ main()
@@ -93,6 +93,12 @@ class DSLExecutor:
93
93
  elif expr_node.type == 'BooleanExpr':
94
94
  # 处理布尔值表达式
95
95
  return expr_node.value
96
+ elif expr_node.type == 'ComparisonExpr':
97
+ # 处理比较表达式
98
+ return self._eval_comparison_expr(expr_node)
99
+ elif expr_node.type == 'ArithmeticExpr':
100
+ # 处理算术表达式
101
+ return self._eval_arithmetic_expr(expr_node)
96
102
  else:
97
103
  raise Exception(f"无法求值的表达式类型: {expr_node.type}")
98
104
 
@@ -101,6 +107,10 @@ class DSLExecutor:
101
107
  if isinstance(value, Node):
102
108
  return self.eval_expression(value)
103
109
  elif isinstance(value, str):
110
+ # 如果是ID类型的变量名
111
+ if value in self.variable_replacer.local_variables:
112
+ return self.variable_replacer.local_variables[value]
113
+
104
114
  # 定义变量引用模式
105
115
  pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)\}'
106
116
  # 检查整个字符串是否完全匹配单一变量引用模式
@@ -113,6 +123,86 @@ class DSLExecutor:
113
123
  return self.variable_replacer.replace_in_string(value)
114
124
  return value
115
125
 
126
+ def _eval_comparison_expr(self, expr_node):
127
+ """
128
+ 对比较表达式进行求值
129
+
130
+ :param expr_node: 比较表达式节点
131
+ :return: 比较结果(布尔值)
132
+ """
133
+ left_value = self.eval_expression(expr_node.children[0])
134
+ right_value = self.eval_expression(expr_node.children[1])
135
+ operator = expr_node.value # 操作符: >, <, >=, <=, ==, !=
136
+
137
+ # 尝试类型转换
138
+ if isinstance(left_value, str) and str(left_value).isdigit():
139
+ left_value = int(left_value)
140
+ if isinstance(right_value, str) and str(right_value).isdigit():
141
+ right_value = int(right_value)
142
+
143
+ # 根据操作符执行相应的比较操作
144
+ if operator == '>':
145
+ return left_value > right_value
146
+ elif operator == '<':
147
+ return left_value < right_value
148
+ elif operator == '>=':
149
+ return left_value >= right_value
150
+ elif operator == '<=':
151
+ return left_value <= right_value
152
+ elif operator == '==':
153
+ return left_value == right_value
154
+ elif operator == '!=':
155
+ return left_value != right_value
156
+ else:
157
+ raise Exception(f"未知的比较操作符: {operator}")
158
+
159
+ def _eval_arithmetic_expr(self, expr_node):
160
+ """
161
+ 对算术表达式进行求值
162
+
163
+ :param expr_node: 算术表达式节点
164
+ :return: 计算结果
165
+ """
166
+ left_value = self.eval_expression(expr_node.children[0])
167
+ right_value = self.eval_expression(expr_node.children[1])
168
+ operator = expr_node.value # 操作符: +, -, *, /
169
+
170
+ # 尝试类型转换 - 如果是字符串数字则转为数字
171
+ if isinstance(left_value, str) and str(left_value).replace('.', '', 1).isdigit():
172
+ left_value = float(left_value)
173
+ # 如果是整数则转为整数
174
+ if left_value.is_integer():
175
+ left_value = int(left_value)
176
+
177
+ if isinstance(right_value, str) and str(right_value).replace('.', '', 1).isdigit():
178
+ right_value = float(right_value)
179
+ # 如果是整数则转为整数
180
+ if right_value.is_integer():
181
+ right_value = int(right_value)
182
+
183
+ # 进行相应的算术运算
184
+ if operator == '+':
185
+ # 对于字符串,+是连接操作
186
+ if isinstance(left_value, str) or isinstance(right_value, str):
187
+ return str(left_value) + str(right_value)
188
+ return left_value + right_value
189
+ elif operator == '-':
190
+ return left_value - right_value
191
+ elif operator == '*':
192
+ # 如果其中一个是字符串,另一个是数字,则进行字符串重复
193
+ if isinstance(left_value, str) and isinstance(right_value, (int, float)):
194
+ return left_value * int(right_value)
195
+ elif isinstance(right_value, str) and isinstance(left_value, (int, float)):
196
+ return right_value * int(left_value)
197
+ return left_value * right_value
198
+ elif operator == '/':
199
+ # 除法时检查除数是否为0
200
+ if right_value == 0:
201
+ raise Exception("除法错误: 除数不能为0")
202
+ return left_value / right_value
203
+ else:
204
+ raise Exception(f"未知的算术操作符: {operator}")
205
+
116
206
  def _get_variable(self, var_name):
117
207
  """获取变量值,优先从本地变量获取,如果不存在则尝试从全局上下文获取"""
118
208
  return self.variable_replacer.get_variable(var_name)
@@ -120,6 +210,21 @@ class DSLExecutor:
120
210
  def _replace_variables_in_string(self, value):
121
211
  """替换字符串中的变量引用"""
122
212
  return self.variable_replacer.replace_in_string(value)
213
+
214
+ def _handle_custom_keywords_in_file(self, node):
215
+ """处理文件中的自定义关键字定义
216
+
217
+ Args:
218
+ node: Start节点
219
+ """
220
+ if len(node.children) > 1 and node.children[1].type == 'Statements':
221
+ statements_node = node.children[1]
222
+ for stmt in statements_node.children:
223
+ if stmt.type == 'CustomKeyword':
224
+ # 导入自定义关键字管理器
225
+ from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
226
+ # 注册自定义关键字
227
+ custom_keyword_manager._register_custom_keyword(stmt, "current_file")
123
228
 
124
229
  def _handle_start(self, node):
125
230
  """处理开始节点"""
@@ -140,6 +245,8 @@ class DSLExecutor:
140
245
  elif child.type == 'Teardown':
141
246
  teardown_node = child
142
247
 
248
+ # 在_execute_test_iteration之前添加
249
+ self._handle_custom_keywords_in_file(node)
143
250
  # 执行测试
144
251
  self._execute_test_iteration(metadata, node, teardown_node)
145
252
 
@@ -341,6 +448,28 @@ class DSLExecutor:
341
448
  expr_node = node.children[0]
342
449
  return self.eval_expression(expr_node)
343
450
 
451
+ @allure.step("执行条件语句")
452
+ def _handle_if_statement(self, node):
453
+ """处理if-else语句
454
+
455
+ Args:
456
+ node: IfStatement节点,包含条件表达式、if分支和可选的else分支
457
+ """
458
+ condition = self.eval_expression(node.children[0])
459
+
460
+ # 将条件转换为布尔值进行评估
461
+ if condition:
462
+ # 执行if分支
463
+ with allure.step("执行if分支"):
464
+ return self.execute(node.children[1])
465
+ elif len(node.children) > 2:
466
+ # 如果存在else分支且条件为假,则执行else分支
467
+ with allure.step("执行else分支"):
468
+ return self.execute(node.children[2])
469
+
470
+ # 如果条件为假且没有else分支,则不执行任何操作
471
+ return None
472
+
344
473
  def execute(self, node):
345
474
  """执行AST节点"""
346
475
  handlers = {
@@ -352,7 +481,9 @@ class DSLExecutor:
352
481
  'ForLoop': self._handle_for_loop,
353
482
  'KeywordCall': self._execute_keyword_call,
354
483
  'Teardown': self._handle_teardown,
355
- 'Return': self._handle_return
484
+ 'Return': self._handle_return,
485
+ 'IfStatement': self._handle_if_statement,
486
+ 'CustomKeyword': lambda _: None # 添加对CustomKeyword节点的处理,只需注册不需执行
356
487
  }
357
488
 
358
489
  handler = handlers.get(node.type)
@@ -363,4 +494,4 @@ class DSLExecutor:
363
494
  def read_file(filename):
364
495
  """读取 DSL 文件内容"""
365
496
  with open(filename, 'r', encoding='utf-8') as f:
366
- return f.read()
497
+ return f.read()
pytest_dsl/core/lexer.py CHANGED
@@ -10,7 +10,9 @@ reserved = {
10
10
  'using': 'USING', # Add new keyword for data-driven testing
11
11
  'True': 'TRUE', # 添加布尔值支持
12
12
  'False': 'FALSE', # 添加布尔值支持
13
- 'return': 'RETURN' # 添加return关键字支持
13
+ 'return': 'RETURN', # 添加return关键字支持
14
+ 'else': 'ELSE', # 添加else关键字支持
15
+ 'if': 'IF' # 添加if关键字支持
14
16
  }
15
17
 
16
18
  # token 名称列表
@@ -36,6 +38,16 @@ tokens = [
36
38
  'DATA_KEYWORD', # Add new token for @data keyword
37
39
  'KEYWORD_KEYWORD', # 添加@keyword关键字
38
40
  'IMPORT_KEYWORD', # 添加@import关键字
41
+ 'GT', # 大于 >
42
+ 'LT', # 小于 <
43
+ 'GE', # 大于等于 >=
44
+ 'LE', # 小于等于 <=
45
+ 'EQ', # 等于 ==
46
+ 'NE', # 不等于 !=
47
+ 'PLUS', # 加法 +
48
+ 'MINUS', # 减法 -
49
+ 'TIMES', # 乘法 *
50
+ 'DIVIDE', # 除法 /
39
51
  ] + list(reserved.values())
40
52
 
41
53
  # 正则表达式定义 token
@@ -46,6 +58,16 @@ t_RBRACKET = r'\]'
46
58
  t_COLON = r':'
47
59
  t_COMMA = r','
48
60
  t_EQUALS = r'='
61
+ t_GT = r'>'
62
+ t_LT = r'<'
63
+ t_GE = r'>='
64
+ t_LE = r'<='
65
+ t_EQ = r'=='
66
+ t_NE = r'!='
67
+ t_PLUS = r'\+'
68
+ t_MINUS = r'-'
69
+ t_TIMES = r'\*'
70
+ t_DIVIDE = r'/'
49
71
 
50
72
  # 增加PLACEHOLDER规则,匹配 ${变量名} 格式
51
73
  t_PLACEHOLDER = r'\$\{[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*\}'
pytest_dsl/core/parser.py CHANGED
@@ -12,23 +12,44 @@ class Node:
12
12
  # 定义优先级和结合性
13
13
  precedence = (
14
14
  ('left', 'COMMA'),
15
+ ('left', 'GT', 'LT', 'GE', 'LE', 'EQ', 'NE'), # 比较运算符优先级
16
+ ('left', 'PLUS', 'MINUS'), # 加减运算符优先级
17
+ ('left', 'TIMES', 'DIVIDE'), # 乘除运算符优先级
15
18
  ('right', 'EQUALS'),
16
19
  )
17
20
 
18
21
 
19
22
  def p_start(p):
20
23
  '''start : metadata statements teardown
21
- | metadata statements'''
24
+ | metadata statements
25
+ | statements teardown
26
+ | statements'''
22
27
 
23
28
  if len(p) == 4:
24
29
  p[0] = Node('Start', [p[1], p[2], p[3]])
30
+ elif len(p) == 3:
31
+ # 判断第二个元素是teardown还是statements
32
+ if p[2].type == 'Teardown':
33
+ p[0] = Node('Start', [Node('Metadata', []), p[1], p[2]])
34
+ else:
35
+ p[0] = Node('Start', [p[1], p[2]])
25
36
  else:
26
- p[0] = Node('Start', [p[1], p[2]])
37
+ # 没有metadata和teardown
38
+ p[0] = Node('Start', [Node('Metadata', []), p[1]])
27
39
 
28
40
 
29
41
  def p_metadata(p):
30
- '''metadata : metadata_items'''
31
- p[0] = Node('Metadata', p[1])
42
+ '''metadata : metadata_items
43
+ | empty'''
44
+ if p[1]:
45
+ p[0] = Node('Metadata', p[1])
46
+ else:
47
+ p[0] = Node('Metadata', [])
48
+
49
+
50
+ def p_empty(p):
51
+ '''empty :'''
52
+ p[0] = None
32
53
 
33
54
 
34
55
  def p_metadata_items(p):
@@ -95,7 +116,8 @@ def p_statement(p):
95
116
  | keyword_call
96
117
  | loop
97
118
  | custom_keyword
98
- | return_statement'''
119
+ | return_statement
120
+ | if_statement'''
99
121
  p[0] = p[1]
100
122
 
101
123
 
@@ -109,13 +131,31 @@ def p_assignment(p):
109
131
 
110
132
 
111
133
  def p_expression(p):
112
- '''expression : NUMBER
113
- | STRING
114
- | PLACEHOLDER
115
- | ID
116
- | boolean_expr
117
- | list_expr'''
118
- p[0] = Node('Expression', value=p[1])
134
+ '''expression : expr_atom
135
+ | comparison_expr
136
+ | arithmetic_expr'''
137
+ # 如果是比较表达式或其他复合表达式,则已经是一个Node对象
138
+ if isinstance(p[1], Node):
139
+ p[0] = p[1]
140
+ else:
141
+ p[0] = Node('Expression', value=p[1])
142
+
143
+
144
+ def p_expr_atom(p):
145
+ '''expr_atom : NUMBER
146
+ | STRING
147
+ | PLACEHOLDER
148
+ | ID
149
+ | boolean_expr
150
+ | list_expr
151
+ | LPAREN expression RPAREN'''
152
+ if p[1] == '(':
153
+ # 处理括号表达式,直接返回括号内的表达式节点
154
+ p[0] = p[2]
155
+ elif isinstance(p[1], Node):
156
+ p[0] = p[1]
157
+ else:
158
+ p[0] = Node('Expression', value=p[1])
119
159
 
120
160
 
121
161
  def p_boolean_expr(p):
@@ -228,6 +268,65 @@ def p_return_statement(p):
228
268
  p[0] = Node('Return', [p[2]])
229
269
 
230
270
 
271
+ def p_if_statement(p):
272
+ '''if_statement : IF expression DO statements END
273
+ | IF expression DO statements ELSE statements END'''
274
+ if len(p) == 6:
275
+ p[0] = Node('IfStatement', [p[2], p[4]], None)
276
+ else:
277
+ p[0] = Node('IfStatement', [p[2], p[4], p[6]], None)
278
+
279
+
280
+ def p_comparison_expr(p):
281
+ '''comparison_expr : expr_atom GT expr_atom
282
+ | expr_atom LT expr_atom
283
+ | expr_atom GE expr_atom
284
+ | expr_atom LE expr_atom
285
+ | expr_atom EQ expr_atom
286
+ | expr_atom NE expr_atom'''
287
+
288
+ # 根据规则索引判断使用的是哪个操作符
289
+ if p.slice[2].type == 'GT':
290
+ operator = '>'
291
+ elif p.slice[2].type == 'LT':
292
+ operator = '<'
293
+ elif p.slice[2].type == 'GE':
294
+ operator = '>='
295
+ elif p.slice[2].type == 'LE':
296
+ operator = '<='
297
+ elif p.slice[2].type == 'EQ':
298
+ operator = '=='
299
+ elif p.slice[2].type == 'NE':
300
+ operator = '!='
301
+ else:
302
+ print(f"警告: 无法识别的操作符类型 {p.slice[2].type}")
303
+ operator = None
304
+
305
+ p[0] = Node('ComparisonExpr', [p[1], p[3]], operator)
306
+
307
+
308
+ def p_arithmetic_expr(p):
309
+ '''arithmetic_expr : expression PLUS expression
310
+ | expression MINUS expression
311
+ | expression TIMES expression
312
+ | expression DIVIDE expression'''
313
+
314
+ # 根据规则索引判断使用的是哪个操作符
315
+ if p.slice[2].type == 'PLUS':
316
+ operator = '+'
317
+ elif p.slice[2].type == 'MINUS':
318
+ operator = '-'
319
+ elif p.slice[2].type == 'TIMES':
320
+ operator = '*'
321
+ elif p.slice[2].type == 'DIVIDE':
322
+ operator = '/'
323
+ else:
324
+ print(f"警告: 无法识别的操作符类型 {p.slice[2].type}")
325
+ operator = None
326
+
327
+ p[0] = Node('ArithmeticExpr', [p[1], p[3]], operator)
328
+
329
+
231
330
  def p_error(p):
232
331
  if p:
233
332
  print(