pytest-dsl 0.4.0__py3-none-any.whl → 0.6.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.
Files changed (36) hide show
  1. pytest_dsl/cli.py +28 -33
  2. pytest_dsl/core/auto_decorator.py +72 -53
  3. pytest_dsl/core/auto_directory.py +8 -5
  4. pytest_dsl/core/dsl_executor.py +211 -53
  5. pytest_dsl/core/http_request.py +272 -221
  6. pytest_dsl/core/lexer.py +14 -13
  7. pytest_dsl/core/parser.py +27 -8
  8. pytest_dsl/core/parsetab.py +71 -66
  9. pytest_dsl/core/plugin_discovery.py +1 -8
  10. pytest_dsl/core/yaml_loader.py +96 -19
  11. pytest_dsl/examples/assert/assertion_example.auto +1 -1
  12. pytest_dsl/examples/assert/boolean_test.auto +2 -2
  13. pytest_dsl/examples/assert/expression_test.auto +1 -1
  14. pytest_dsl/examples/custom/test_advanced_keywords.auto +2 -2
  15. pytest_dsl/examples/custom/test_custom_keywords.auto +2 -2
  16. pytest_dsl/examples/custom/test_default_values.auto +2 -2
  17. pytest_dsl/examples/http/file_reference_test.auto +1 -1
  18. pytest_dsl/examples/http/http_advanced.auto +1 -1
  19. pytest_dsl/examples/http/http_example.auto +1 -1
  20. pytest_dsl/examples/http/http_length_test.auto +1 -1
  21. pytest_dsl/examples/http/http_retry_assertions.auto +1 -1
  22. pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +2 -2
  23. pytest_dsl/examples/http/http_with_yaml.auto +1 -1
  24. pytest_dsl/examples/quickstart/api_basics.auto +1 -1
  25. pytest_dsl/examples/quickstart/assertions.auto +1 -1
  26. pytest_dsl/examples/quickstart/loops.auto +2 -2
  27. pytest_dsl/keywords/assertion_keywords.py +76 -62
  28. pytest_dsl/keywords/global_keywords.py +43 -4
  29. pytest_dsl/keywords/http_keywords.py +141 -139
  30. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/METADATA +266 -15
  31. pytest_dsl-0.6.0.dist-info/RECORD +68 -0
  32. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/WHEEL +1 -1
  33. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/entry_points.txt +1 -0
  34. pytest_dsl-0.4.0.dist-info/RECORD +0 -68
  35. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/licenses/LICENSE +0 -0
  36. {pytest_dsl-0.4.0.dist-info → pytest_dsl-0.6.0.dist-info}/top_level.txt +0 -0
pytest_dsl/cli.py CHANGED
@@ -13,8 +13,9 @@ from pathlib import Path
13
13
  from pytest_dsl.core.lexer import get_lexer
14
14
  from pytest_dsl.core.parser import get_parser
15
15
  from pytest_dsl.core.dsl_executor import DSLExecutor
16
- from pytest_dsl.core.yaml_vars import yaml_vars
16
+ from pytest_dsl.core.yaml_loader import load_yaml_variables_from_args
17
17
  from pytest_dsl.core.auto_directory import SETUP_FILE_NAME, TEARDOWN_FILE_NAME, execute_hook_file
18
+ from pytest_dsl.core.plugin_discovery import load_all_plugins
18
19
 
19
20
 
20
21
  def read_file(filename):
@@ -27,35 +28,26 @@ def parse_args():
27
28
  """解析命令行参数"""
28
29
  parser = argparse.ArgumentParser(description='执行DSL测试文件')
29
30
  parser.add_argument('path', help='要执行的DSL文件路径或包含DSL文件的目录')
30
- parser.add_argument('--yaml-vars', action='append', default=[],
31
+ parser.add_argument('--yaml-vars', action='append', default=[],
31
32
  help='YAML变量文件路径,可以指定多个文件 (例如: --yaml-vars vars1.yaml --yaml-vars vars2.yaml)')
32
33
  parser.add_argument('--yaml-vars-dir', default=None,
33
34
  help='YAML变量文件目录路径,将加载该目录下所有.yaml文件')
34
-
35
+
35
36
  return parser.parse_args()
36
37
 
37
38
 
38
39
  def load_yaml_variables(args):
39
40
  """从命令行参数加载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)
41
+ # 使用统一的加载函数,包含远程服务器自动连接功能
42
+ try:
43
+ load_yaml_variables_from_args(
44
+ yaml_files=args.yaml_vars,
45
+ yaml_vars_dir=args.yaml_vars_dir,
46
+ project_root=os.getcwd() # CLI模式下使用当前工作目录作为项目根目录
47
+ )
48
+ except Exception as e:
49
+ print(f"加载YAML变量失败: {str(e)}")
50
+ sys.exit(1)
59
51
 
60
52
 
61
53
  def execute_dsl_file(file_path, lexer, parser, executor):
@@ -85,14 +77,17 @@ def main():
85
77
  """命令行入口点"""
86
78
  args = parse_args()
87
79
  path = args.path
88
-
89
- # 加载YAML变量
80
+
81
+ # 加载内置关键字插件
82
+ load_all_plugins()
83
+
84
+ # 加载YAML变量(包括远程服务器自动连接)
90
85
  load_yaml_variables(args)
91
-
86
+
92
87
  lexer = get_lexer()
93
88
  parser = get_parser()
94
89
  executor = DSLExecutor()
95
-
90
+
96
91
  # 检查路径是文件还是目录
97
92
  if os.path.isfile(path):
98
93
  # 执行单个文件
@@ -102,32 +97,32 @@ def main():
102
97
  elif os.path.isdir(path):
103
98
  # 执行目录中的所有DSL文件
104
99
  print(f"执行目录: {path}")
105
-
100
+
106
101
  # 先执行目录的setup文件(如果存在)
107
102
  setup_file = os.path.join(path, SETUP_FILE_NAME)
108
103
  if os.path.exists(setup_file):
109
104
  execute_hook_file(Path(setup_file), True, path)
110
-
105
+
111
106
  # 查找并执行所有DSL文件
112
107
  dsl_files = find_dsl_files(path)
113
108
  if not dsl_files:
114
109
  print(f"目录中没有找到DSL文件: {path}")
115
110
  sys.exit(1)
116
-
111
+
117
112
  print(f"找到 {len(dsl_files)} 个DSL文件")
118
-
113
+
119
114
  # 执行所有DSL文件
120
115
  failures = 0
121
116
  for file_path in dsl_files:
122
117
  success = execute_dsl_file(file_path, lexer, parser, executor)
123
118
  if not success:
124
119
  failures += 1
125
-
120
+
126
121
  # 最后执行目录的teardown文件(如果存在)
127
122
  teardown_file = os.path.join(path, TEARDOWN_FILE_NAME)
128
123
  if os.path.exists(teardown_file):
129
124
  execute_hook_file(Path(teardown_file), False, path)
130
-
125
+
131
126
  # 如果有失败的测试,返回非零退出码
132
127
  if failures > 0:
133
128
  print(f"总计 {failures}/{len(dsl_files)} 个测试失败")
@@ -140,4 +135,4 @@ def main():
140
135
 
141
136
 
142
137
  if __name__ == '__main__':
143
- main()
138
+ main()
@@ -1,6 +1,6 @@
1
1
  """自动测试装饰器模块
2
2
 
3
- 该模块提供装饰器功能,用于将指定目录下的.auto文件动态添加为测试方法到被装饰的类中。
3
+ 该模块提供装饰器功能,用于将指定目录下的.auto和.dsl文件动态添加为测试方法到被装饰的类中。
4
4
  这种方式更贴合pytest的设计理念,可以充分利用pytest的fixture、参数化等功能。
5
5
  """
6
6
 
@@ -15,7 +15,11 @@ from pytest_dsl.core.dsl_executor import DSLExecutor
15
15
  from pytest_dsl.core.dsl_executor_utils import read_file, execute_dsl_file, extract_metadata_from_ast
16
16
  from pytest_dsl.core.lexer import get_lexer
17
17
  from pytest_dsl.core.parser import get_parser
18
- from pytest_dsl.core.auto_directory import SETUP_FILE_NAME, TEARDOWN_FILE_NAME, execute_hook_file
18
+ from pytest_dsl.core.auto_directory import (
19
+ SETUP_FILE_NAME, TEARDOWN_FILE_NAME,
20
+ SETUP_DSL_FILE_NAME, TEARDOWN_DSL_FILE_NAME,
21
+ execute_hook_file
22
+ )
19
23
 
20
24
  # 获取词法分析器和解析器实例
21
25
  lexer = get_lexer()
@@ -24,12 +28,12 @@ parser = get_parser()
24
28
 
25
29
  def auto_dsl(directory: Union[str, Path], is_file: bool = False):
26
30
  """
27
- 装饰器函数,用于将指定目录下的.auto文件动态添加为测试方法到被装饰的类中。
28
-
31
+ 装饰器函数,用于将指定目录下的.auto和.dsl文件动态添加为测试方法到被装饰的类中。
32
+
29
33
  Args:
30
- directory: 包含.auto文件的目录路径,可以是相对路径或绝对路径
34
+ directory: 包含.auto或.dsl文件的目录路径,可以是相对路径或绝对路径
31
35
  is_file: 是否是文件路径而不是目录路径
32
-
36
+
33
37
  Returns:
34
38
  装饰器函数
35
39
  """
@@ -40,7 +44,7 @@ def auto_dsl(directory: Union[str, Path], is_file: bool = False):
40
44
  caller_file = caller_frame.f_globals['__file__']
41
45
  caller_dir = Path(caller_file).parent
42
46
  path = (caller_dir / path).resolve()
43
-
47
+
44
48
  if is_file:
45
49
  # 路径是文件
46
50
  if not path.exists() or not path.is_file():
@@ -51,109 +55,124 @@ def auto_dsl(directory: Union[str, Path], is_file: bool = False):
51
55
  if not path.exists() or not path.is_dir():
52
56
  raise ValueError(f"目录不存在或不是有效目录: {path}")
53
57
  directory_path = path
54
-
58
+
55
59
  def decorator(cls):
56
60
  if is_file:
57
61
  # 如果是文件路径,只添加这个文件的测试方法
58
62
  _add_test_method(cls, file_path)
59
63
  else:
60
- # 检查setup.auto和teardown.auto文件
61
- setup_file = directory_path / SETUP_FILE_NAME
62
- teardown_file = directory_path / TEARDOWN_FILE_NAME
63
-
64
+ # 检查setup和teardown文件(支持.auto和.dsl扩展名)
65
+ setup_file = None
66
+ teardown_file = None
67
+
68
+ # 优先查找.auto文件,然后查找.dsl文件
69
+ for setup_name in [SETUP_FILE_NAME, SETUP_DSL_FILE_NAME]:
70
+ potential_setup = directory_path / setup_name
71
+ if potential_setup.exists():
72
+ setup_file = potential_setup
73
+ break
74
+
75
+ for teardown_name in [TEARDOWN_FILE_NAME, TEARDOWN_DSL_FILE_NAME]:
76
+ potential_teardown = directory_path / teardown_name
77
+ if potential_teardown.exists():
78
+ teardown_file = potential_teardown
79
+ break
80
+
64
81
  # 添加setup和teardown方法
65
- if setup_file.exists():
82
+ if setup_file:
66
83
  @classmethod
67
84
  @pytest.fixture(scope="class", autouse=True)
68
85
  def setup_class(cls, request):
69
86
  execute_hook_file(setup_file, True, str(directory_path))
70
-
87
+
71
88
  setattr(cls, "setup_class", setup_class)
72
-
73
- if teardown_file.exists():
89
+
90
+ if teardown_file:
74
91
  @classmethod
75
92
  @pytest.fixture(scope="class", autouse=True)
76
93
  def teardown_class(cls, request):
77
94
  request.addfinalizer(lambda: execute_hook_file(teardown_file, False, str(directory_path)))
78
-
95
+
79
96
  setattr(cls, "teardown_class", teardown_class)
80
-
81
- # 处理目录中的测试文件
82
- for auto_file in directory_path.glob("*.auto"):
83
- if auto_file.name not in [SETUP_FILE_NAME, TEARDOWN_FILE_NAME]:
84
- _add_test_method(cls, auto_file)
85
-
97
+
98
+ # 处理目录中的测试文件,支持.auto和.dsl扩展名
99
+ excluded_files = [SETUP_FILE_NAME, TEARDOWN_FILE_NAME, SETUP_DSL_FILE_NAME, TEARDOWN_DSL_FILE_NAME]
100
+ for pattern in ["*.auto", "*.dsl"]:
101
+ for test_file in directory_path.glob(pattern):
102
+ if test_file.name not in excluded_files:
103
+ _add_test_method(cls, test_file)
104
+
86
105
  return cls
87
-
106
+
88
107
  return decorator
89
108
 
90
109
 
91
- def _add_test_method(cls: Type, auto_file: Path) -> None:
110
+ def _add_test_method(cls: Type, test_file: Path) -> None:
92
111
  """
93
- 为.auto文件创建测试方法并添加到类中
94
-
112
+ 为DSL测试文件创建测试方法并添加到类中
113
+
95
114
  Args:
96
115
  cls: 要添加测试方法的类
97
- auto_file: .auto文件路径
116
+ test_file: DSL测试文件路径(.auto或.dsl)
98
117
  """
99
- test_name = f"test_{auto_file.stem}"
100
-
118
+ test_name = f"test_{test_file.stem}"
119
+
101
120
  # 读取DSL文件内容并解析
102
- dsl_code = read_file(str(auto_file))
121
+ dsl_code = read_file(str(test_file))
103
122
  ast = parser.parse(dsl_code, lexer=lexer)
104
-
123
+
105
124
  # 检查是否有数据驱动标记和测试名称
106
125
  data_source, test_title = extract_metadata_from_ast(ast)
107
-
126
+
108
127
  if data_source:
109
- test_method = _create_data_driven_test(auto_file, data_source, test_title)
128
+ test_method = _create_data_driven_test(test_file, data_source, test_title)
110
129
  else:
111
- test_method = _create_simple_test(auto_file)
112
-
130
+ test_method = _create_simple_test(test_file)
131
+
113
132
  # 将测试方法添加到类
114
133
  setattr(cls, test_name, test_method)
115
134
 
116
135
 
117
- def _create_simple_test(auto_file: Path) -> Callable:
136
+ def _create_simple_test(test_file: Path) -> Callable:
118
137
  """
119
138
  创建普通的测试方法
120
-
139
+
121
140
  Args:
122
- auto_file: .auto文件路径
123
-
141
+ test_file: DSL测试文件路径(.auto或.dsl)
142
+
124
143
  Returns:
125
144
  function: 测试方法
126
145
  """
127
146
  def test_method(self):
128
- execute_dsl_file(str(auto_file))
129
-
147
+ execute_dsl_file(str(test_file))
148
+
130
149
  return test_method
131
150
 
132
151
 
133
- def _create_data_driven_test(auto_file: Path, data_source: Dict, test_title: Optional[str]) -> Callable:
152
+ def _create_data_driven_test(test_file: Path, data_source: Dict, test_title: Optional[str]) -> Callable:
134
153
  """
135
154
  创建数据驱动的测试方法
136
-
155
+
137
156
  Args:
138
- auto_file: .auto文件路径
157
+ test_file: DSL测试文件路径(.auto或.dsl)
139
158
  data_source: 数据源
140
159
  test_title: 测试标题
141
-
160
+
142
161
  Returns:
143
162
  function: 装饰后的测试方法
144
163
  """
145
164
  def test_method(self, test_data):
146
165
  executor = DSLExecutor()
147
166
  executor.set_current_data(test_data)
148
- execute_dsl_file(str(auto_file), executor)
149
-
167
+ execute_dsl_file(str(test_file), executor)
168
+
150
169
  # 加载测试数据
151
170
  executor = DSLExecutor()
152
171
  test_data_list = executor._load_test_data(data_source)
153
-
172
+
154
173
  # 为每个数据集创建一个唯一的ID
155
- test_ids = _generate_test_ids(test_data_list, test_title or auto_file.stem)
156
-
174
+ test_ids = _generate_test_ids(test_data_list, test_title or test_file.stem)
175
+
157
176
  # 使用pytest.mark.parametrize装饰测试方法
158
177
  return pytest.mark.parametrize(
159
178
  'test_data',
@@ -165,11 +184,11 @@ def _create_data_driven_test(auto_file: Path, data_source: Dict, test_title: Opt
165
184
  def _generate_test_ids(test_data_list: List[Dict[str, Any]], base_name: str) -> List[str]:
166
185
  """
167
186
  为数据驱动测试生成ID
168
-
187
+
169
188
  Args:
170
189
  test_data_list: 测试数据列表
171
190
  base_name: 基础名称
172
-
191
+
173
192
  Returns:
174
193
  List[str]: 测试ID列表
175
194
  """
@@ -32,6 +32,9 @@ _teardown_executed = set()
32
32
  # 常量定义
33
33
  SETUP_FILE_NAME = "setup.auto"
34
34
  TEARDOWN_FILE_NAME = "teardown.auto"
35
+ # 支持.dsl扩展名的setup和teardown文件
36
+ SETUP_DSL_FILE_NAME = "setup.dsl"
37
+ TEARDOWN_DSL_FILE_NAME = "teardown.dsl"
35
38
  TMP_DIR = "/tmp"
36
39
  LOCK_FILE_SUFFIX = ".lock"
37
40
  EXECUTED_FILE_SUFFIX = ".lock.executed"
@@ -39,11 +42,11 @@ EXECUTED_FILE_SUFFIX = ".lock.executed"
39
42
 
40
43
  def get_lock_file_path(dir_path: str, is_setup: bool) -> str:
41
44
  """获取锁文件路径
42
-
45
+
43
46
  Args:
44
47
  dir_path: 目录路径
45
48
  is_setup: 是否为setup锁文件
46
-
49
+
47
50
  Returns:
48
51
  str: 锁文件路径
49
52
  """
@@ -53,7 +56,7 @@ def get_lock_file_path(dir_path: str, is_setup: bool) -> str:
53
56
 
54
57
  def execute_hook_file(file_path: Path, is_setup: bool, dir_path_str: str) -> None:
55
58
  """执行setup或teardown钩子文件
56
-
59
+
57
60
  Args:
58
61
  file_path: 钩子文件路径
59
62
  is_setup: 是否为setup钩子
@@ -62,12 +65,12 @@ def execute_hook_file(file_path: Path, is_setup: bool, dir_path_str: str) -> Non
62
65
  hook_type = "Setup" if is_setup else "Teardown"
63
66
  executed_set = _setup_executed if is_setup else _teardown_executed
64
67
  lock_file = get_lock_file_path(dir_path_str, is_setup)
65
-
68
+
66
69
  # 检查是否已执行过
67
70
  if dir_path_str in executed_set:
68
71
  logger.info(f"{hook_type} for directory already executed: {dir_path_str}")
69
72
  return
70
-
73
+
71
74
  # 使用filelock获取锁并执行
72
75
  with FileLock(lock_file):
73
76
  if dir_path_str not in executed_set: # 再次检查,防止在获取锁期间被其他进程执行