pytest-dsl 0.7.0__tar.gz → 0.9.0__tar.gz
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-0.7.0/pytest_dsl.egg-info → pytest_dsl-0.9.0}/PKG-INFO +49 -1
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/README.md +48 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pyproject.toml +2 -1
- pytest_dsl-0.9.0/pytest_dsl/cli.py +457 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/custom_keyword_manager.py +49 -48
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/dsl_executor.py +102 -14
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/lexer.py +8 -2
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/parser.py +18 -3
- pytest_dsl-0.9.0/pytest_dsl/core/parsetab.py +130 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0/pytest_dsl.egg-info}/PKG-INFO +49 -1
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl.egg-info/entry_points.txt +1 -0
- pytest_dsl-0.7.0/pytest_dsl/cli.py +0 -138
- pytest_dsl-0.7.0/pytest_dsl/core/parsetab.py +0 -125
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/LICENSE +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/MANIFEST.in +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/__init__.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/conftest_adapter.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/__init__.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/auth_provider.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/auto_decorator.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/auto_directory.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/context.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/dsl_executor_utils.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/global_context.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/http_client.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/http_request.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/keyword_manager.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/plugin_discovery.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/utils.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/variable_utils.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/yaml_loader.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/core/yaml_vars.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/docs/custom_keywords.md +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/__init__.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/assert/assertion_example.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/assert/boolean_test.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/assert/expression_test.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/custom/test_advanced_keywords.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/custom/test_custom_keywords.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/custom/test_default_values.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/__init__.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/builtin_auth_test.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/file_reference_test.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/http_advanced.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/http_example.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/http_length_test.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/http_retry_assertions.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/http_with_yaml.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/new_retry_test.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/retry_assertions_only.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/retry_config_only.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/retry_debug.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/retry_with_fix.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/simple_retry.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/http/vars.yaml +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/quickstart/api_basics.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/quickstart/assertions.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/quickstart/loops.auto +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/test_assert.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/test_custom_keyword.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/test_http.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/examples/test_quickstart.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/keywords/__init__.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/keywords/assertion_keywords.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/keywords/global_keywords.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/keywords/http_keywords.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/keywords/system_keywords.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/main_adapter.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl/plugin.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl.egg-info/SOURCES.txt +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl.egg-info/dependency_links.txt +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl.egg-info/requires.txt +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/pytest_dsl.egg-info/top_level.txt +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/setup.cfg +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/setup.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/tests/test_end_to_end_seamless.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/tests/test_enhanced_variable_access.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/tests/test_http_assertions_extractors.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/tests/test_seamless_variable_sync.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/tests/test_variable_sync.py +0 -0
- {pytest_dsl-0.7.0 → pytest_dsl-0.9.0}/tests/test_variable_sync_demo.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pytest-dsl
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.9.0
|
4
4
|
Summary: A DSL testing framework based on pytest
|
5
5
|
Author: Chen Shuanglin
|
6
6
|
License: MIT
|
@@ -114,8 +114,32 @@ version = "1.0.0"
|
|
114
114
|
# 数字变量
|
115
115
|
port = 8080
|
116
116
|
|
117
|
+
# 布尔值变量
|
118
|
+
is_enabled = True
|
119
|
+
is_disabled = False
|
120
|
+
|
117
121
|
# 列表
|
118
122
|
users = ["alice", "bob", "charlie"]
|
123
|
+
|
124
|
+
# 字典
|
125
|
+
user_info = {"name": "张三", "age": 30, "city": "北京"}
|
126
|
+
|
127
|
+
# 嵌套字典
|
128
|
+
config = {
|
129
|
+
"database": {
|
130
|
+
"host": "localhost",
|
131
|
+
"port": 3306,
|
132
|
+
"name": "test_db"
|
133
|
+
},
|
134
|
+
"api": {
|
135
|
+
"base_url": "https://api.example.com",
|
136
|
+
"timeout": 30
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
# 访问字典值
|
141
|
+
username = ${user_info["name"]}
|
142
|
+
db_host = ${config["database"]["host"]}
|
119
143
|
```
|
120
144
|
|
121
145
|
#### 流程控制
|
@@ -129,12 +153,33 @@ else
|
|
129
153
|
[打印], 内容: "测试失败"
|
130
154
|
end
|
131
155
|
|
156
|
+
# 使用布尔值的条件判断
|
157
|
+
is_ready = True
|
158
|
+
if ${is_ready} do
|
159
|
+
[打印], 内容: "系统就绪"
|
160
|
+
end
|
161
|
+
|
132
162
|
# 循环结构
|
133
163
|
num = 4
|
134
164
|
for i in range(1, num) do
|
135
165
|
[打印], 内容: "执行第 ${i} 次"
|
136
166
|
end
|
137
167
|
|
168
|
+
# 循环中的break和continue
|
169
|
+
for j in range(1, 11) do
|
170
|
+
# 跳过偶数
|
171
|
+
if ${j} % 2 == 0 do
|
172
|
+
continue
|
173
|
+
end
|
174
|
+
|
175
|
+
# 当达到7时退出循环
|
176
|
+
if ${j} == 7 do
|
177
|
+
[打印], 内容: "达到7,退出循环"
|
178
|
+
break
|
179
|
+
end
|
180
|
+
|
181
|
+
[打印], 内容: "奇数: ${j}"
|
182
|
+
end
|
138
183
|
```
|
139
184
|
|
140
185
|
### 2. 内置关键字详解
|
@@ -1034,6 +1079,9 @@ pytest-dsl api_basic.dsl
|
|
1034
1079
|
- ✅ 断言重试机制
|
1035
1080
|
- ✅ 认证功能示例
|
1036
1081
|
- ✅ 数据驱动测试(pytest集成)
|
1082
|
+
- ✅ 布尔值支持和条件判断
|
1083
|
+
- ✅ 字典定义和嵌套访问
|
1084
|
+
- ✅ 循环控制语句(break/continue)
|
1037
1085
|
|
1038
1086
|
---
|
1039
1087
|
|
@@ -83,8 +83,32 @@ version = "1.0.0"
|
|
83
83
|
# 数字变量
|
84
84
|
port = 8080
|
85
85
|
|
86
|
+
# 布尔值变量
|
87
|
+
is_enabled = True
|
88
|
+
is_disabled = False
|
89
|
+
|
86
90
|
# 列表
|
87
91
|
users = ["alice", "bob", "charlie"]
|
92
|
+
|
93
|
+
# 字典
|
94
|
+
user_info = {"name": "张三", "age": 30, "city": "北京"}
|
95
|
+
|
96
|
+
# 嵌套字典
|
97
|
+
config = {
|
98
|
+
"database": {
|
99
|
+
"host": "localhost",
|
100
|
+
"port": 3306,
|
101
|
+
"name": "test_db"
|
102
|
+
},
|
103
|
+
"api": {
|
104
|
+
"base_url": "https://api.example.com",
|
105
|
+
"timeout": 30
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
# 访问字典值
|
110
|
+
username = ${user_info["name"]}
|
111
|
+
db_host = ${config["database"]["host"]}
|
88
112
|
```
|
89
113
|
|
90
114
|
#### 流程控制
|
@@ -98,12 +122,33 @@ else
|
|
98
122
|
[打印], 内容: "测试失败"
|
99
123
|
end
|
100
124
|
|
125
|
+
# 使用布尔值的条件判断
|
126
|
+
is_ready = True
|
127
|
+
if ${is_ready} do
|
128
|
+
[打印], 内容: "系统就绪"
|
129
|
+
end
|
130
|
+
|
101
131
|
# 循环结构
|
102
132
|
num = 4
|
103
133
|
for i in range(1, num) do
|
104
134
|
[打印], 内容: "执行第 ${i} 次"
|
105
135
|
end
|
106
136
|
|
137
|
+
# 循环中的break和continue
|
138
|
+
for j in range(1, 11) do
|
139
|
+
# 跳过偶数
|
140
|
+
if ${j} % 2 == 0 do
|
141
|
+
continue
|
142
|
+
end
|
143
|
+
|
144
|
+
# 当达到7时退出循环
|
145
|
+
if ${j} == 7 do
|
146
|
+
[打印], 内容: "达到7,退出循环"
|
147
|
+
break
|
148
|
+
end
|
149
|
+
|
150
|
+
[打印], 内容: "奇数: ${j}"
|
151
|
+
end
|
107
152
|
```
|
108
153
|
|
109
154
|
### 2. 内置关键字详解
|
@@ -1003,6 +1048,9 @@ pytest-dsl api_basic.dsl
|
|
1003
1048
|
- ✅ 断言重试机制
|
1004
1049
|
- ✅ 认证功能示例
|
1005
1050
|
- ✅ 数据驱动测试(pytest集成)
|
1051
|
+
- ✅ 布尔值支持和条件判断
|
1052
|
+
- ✅ 字典定义和嵌套访问
|
1053
|
+
- ✅ 循环控制语句(break/continue)
|
1006
1054
|
|
1007
1055
|
---
|
1008
1056
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "pytest-dsl"
|
7
|
-
version = "0.
|
7
|
+
version = "0.9.0"
|
8
8
|
description = "A DSL testing framework based on pytest"
|
9
9
|
readme = "README.md"
|
10
10
|
requires-python = ">=3.9"
|
@@ -41,6 +41,7 @@ pytest_dsl = "pytest_dsl.plugin"
|
|
41
41
|
[project.scripts]
|
42
42
|
pytest-dsl = "pytest_dsl.cli:main"
|
43
43
|
pytest-dsl-server = "pytest_dsl.remote.keyword_server:main"
|
44
|
+
pytest-dsl-list = "pytest_dsl.cli:main"
|
44
45
|
|
45
46
|
[project.urls]
|
46
47
|
"Homepage" = "https://github.com/felix-1991/pytest-dsl"
|
@@ -0,0 +1,457 @@
|
|
1
|
+
"""
|
2
|
+
pytest-dsl命令行入口
|
3
|
+
|
4
|
+
提供独立的命令行工具,用于执行DSL文件。
|
5
|
+
"""
|
6
|
+
|
7
|
+
import sys
|
8
|
+
import argparse
|
9
|
+
import os
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
from pytest_dsl.core.lexer import get_lexer
|
13
|
+
from pytest_dsl.core.parser import get_parser
|
14
|
+
from pytest_dsl.core.dsl_executor import DSLExecutor
|
15
|
+
from pytest_dsl.core.yaml_loader import load_yaml_variables_from_args
|
16
|
+
from pytest_dsl.core.auto_directory import (
|
17
|
+
SETUP_FILE_NAME, TEARDOWN_FILE_NAME, execute_hook_file
|
18
|
+
)
|
19
|
+
from pytest_dsl.core.plugin_discovery import (
|
20
|
+
load_all_plugins, scan_local_keywords
|
21
|
+
)
|
22
|
+
from pytest_dsl.core.keyword_manager import keyword_manager
|
23
|
+
|
24
|
+
|
25
|
+
def read_file(filename):
|
26
|
+
"""读取 DSL 文件内容"""
|
27
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
28
|
+
return f.read()
|
29
|
+
|
30
|
+
|
31
|
+
def parse_args():
|
32
|
+
"""解析命令行参数"""
|
33
|
+
import sys
|
34
|
+
argv = sys.argv[1:] # 去掉脚本名
|
35
|
+
|
36
|
+
# 检查是否使用了子命令格式
|
37
|
+
if argv and argv[0] in ['run', 'list-keywords']:
|
38
|
+
# 使用新的子命令格式
|
39
|
+
parser = argparse.ArgumentParser(description='执行DSL测试文件')
|
40
|
+
subparsers = parser.add_subparsers(dest='command', help='可用命令')
|
41
|
+
|
42
|
+
# 执行命令
|
43
|
+
run_parser = subparsers.add_parser('run', help='执行DSL文件')
|
44
|
+
run_parser.add_argument(
|
45
|
+
'path',
|
46
|
+
help='要执行的DSL文件路径或包含DSL文件的目录'
|
47
|
+
)
|
48
|
+
run_parser.add_argument(
|
49
|
+
'--yaml-vars', action='append', default=[],
|
50
|
+
help='YAML变量文件路径,可以指定多个文件 '
|
51
|
+
'(例如: --yaml-vars vars1.yaml '
|
52
|
+
'--yaml-vars vars2.yaml)'
|
53
|
+
)
|
54
|
+
run_parser.add_argument(
|
55
|
+
'--yaml-vars-dir', default=None,
|
56
|
+
help='YAML变量文件目录路径,'
|
57
|
+
'将加载该目录下所有.yaml文件'
|
58
|
+
)
|
59
|
+
|
60
|
+
# 关键字列表命令
|
61
|
+
list_parser = subparsers.add_parser(
|
62
|
+
'list-keywords',
|
63
|
+
help='罗列所有可用关键字和参数信息'
|
64
|
+
)
|
65
|
+
list_parser.add_argument(
|
66
|
+
'--format', choices=['text', 'json'],
|
67
|
+
default='text',
|
68
|
+
help='输出格式:text(默认) 或 json'
|
69
|
+
)
|
70
|
+
list_parser.add_argument(
|
71
|
+
'--filter', type=str, default=None,
|
72
|
+
help='过滤关键字名称(支持部分匹配)'
|
73
|
+
)
|
74
|
+
list_parser.add_argument(
|
75
|
+
'--category',
|
76
|
+
choices=['builtin', 'custom', 'remote', 'all'],
|
77
|
+
default='all',
|
78
|
+
help='关键字类别:builtin(内置)、custom(自定义)、'
|
79
|
+
'remote(远程)、all(全部,默认)'
|
80
|
+
)
|
81
|
+
|
82
|
+
return parser.parse_args(argv)
|
83
|
+
else:
|
84
|
+
# 向后兼容模式
|
85
|
+
parser = argparse.ArgumentParser(description='执行DSL测试文件')
|
86
|
+
|
87
|
+
# 检查是否是list-keywords的旧格式
|
88
|
+
if '--list-keywords' in argv:
|
89
|
+
parser.add_argument('--list-keywords', action='store_true')
|
90
|
+
parser.add_argument(
|
91
|
+
'--format', choices=['text', 'json'], default='text'
|
92
|
+
)
|
93
|
+
parser.add_argument('--filter', type=str, default=None)
|
94
|
+
parser.add_argument(
|
95
|
+
'--category',
|
96
|
+
choices=['builtin', 'custom', 'remote', 'all'],
|
97
|
+
default='all'
|
98
|
+
)
|
99
|
+
parser.add_argument('path', nargs='?') # 可选的路径参数
|
100
|
+
parser.add_argument(
|
101
|
+
'--yaml-vars', action='append', default=[]
|
102
|
+
)
|
103
|
+
parser.add_argument('--yaml-vars-dir', default=None)
|
104
|
+
|
105
|
+
args = parser.parse_args(argv)
|
106
|
+
args.command = 'list-keywords-compat' # 标记为兼容模式
|
107
|
+
else:
|
108
|
+
# 默认为run命令的向后兼容模式
|
109
|
+
parser.add_argument('path', nargs='?')
|
110
|
+
parser.add_argument(
|
111
|
+
'--yaml-vars', action='append', default=[]
|
112
|
+
)
|
113
|
+
parser.add_argument('--yaml-vars-dir', default=None)
|
114
|
+
|
115
|
+
args = parser.parse_args(argv)
|
116
|
+
args.command = 'run-compat' # 标记为兼容模式
|
117
|
+
|
118
|
+
return args
|
119
|
+
|
120
|
+
|
121
|
+
def load_all_keywords():
|
122
|
+
"""加载所有可用的关键字"""
|
123
|
+
# 首先导入内置关键字模块,确保内置关键字被注册
|
124
|
+
try:
|
125
|
+
import pytest_dsl.keywords # noqa: F401
|
126
|
+
print("内置关键字模块加载完成")
|
127
|
+
except ImportError as e:
|
128
|
+
print(f"加载内置关键字模块失败: {e}")
|
129
|
+
|
130
|
+
# 加载已安装的关键字插件
|
131
|
+
load_all_plugins()
|
132
|
+
|
133
|
+
# 扫描本地关键字
|
134
|
+
scan_local_keywords()
|
135
|
+
|
136
|
+
|
137
|
+
def categorize_keyword(keyword_name, keyword_info):
|
138
|
+
"""判断关键字的类别"""
|
139
|
+
if keyword_info.get('remote', False):
|
140
|
+
return 'remote'
|
141
|
+
|
142
|
+
# 检查是否是内置关键字(通过检查函数所在模块)
|
143
|
+
func = keyword_info.get('func')
|
144
|
+
if func and hasattr(func, '__module__'):
|
145
|
+
module_name = func.__module__
|
146
|
+
if module_name and module_name.startswith('pytest_dsl.keywords'):
|
147
|
+
return 'builtin'
|
148
|
+
|
149
|
+
return 'custom'
|
150
|
+
|
151
|
+
|
152
|
+
def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
|
153
|
+
"""格式化关键字信息为文本格式"""
|
154
|
+
lines = []
|
155
|
+
|
156
|
+
# 关键字名称和类别
|
157
|
+
category = categorize_keyword(keyword_name, keyword_info)
|
158
|
+
category_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
|
159
|
+
|
160
|
+
if show_category:
|
161
|
+
category_display = category_names.get(category, '未知')
|
162
|
+
lines.append(f"关键字: {keyword_name} [{category_display}]")
|
163
|
+
else:
|
164
|
+
lines.append(f"关键字: {keyword_name}")
|
165
|
+
|
166
|
+
# 远程关键字特殊标识
|
167
|
+
if keyword_info.get('remote', False):
|
168
|
+
alias = keyword_info.get('alias', '未知')
|
169
|
+
original_name = keyword_info.get('original_name', keyword_name)
|
170
|
+
lines.append(f" 远程服务器: {alias}")
|
171
|
+
lines.append(f" 原始名称: {original_name}")
|
172
|
+
|
173
|
+
# 参数信息
|
174
|
+
parameters = keyword_info.get('parameters', [])
|
175
|
+
if parameters:
|
176
|
+
lines.append(" 参数:")
|
177
|
+
for param in parameters:
|
178
|
+
param_name = getattr(param, 'name', str(param))
|
179
|
+
param_mapping = getattr(param, 'mapping', '')
|
180
|
+
param_desc = getattr(param, 'description', '')
|
181
|
+
|
182
|
+
if param_mapping and param_mapping != param_name:
|
183
|
+
lines.append(
|
184
|
+
f" {param_name} ({param_mapping}): {param_desc}"
|
185
|
+
)
|
186
|
+
else:
|
187
|
+
lines.append(f" {param_name}: {param_desc}")
|
188
|
+
else:
|
189
|
+
lines.append(" 参数: 无")
|
190
|
+
|
191
|
+
# 函数文档
|
192
|
+
func = keyword_info.get('func')
|
193
|
+
if func and hasattr(func, '__doc__') and func.__doc__:
|
194
|
+
lines.append(f" 说明: {func.__doc__.strip()}")
|
195
|
+
|
196
|
+
return '\n'.join(lines)
|
197
|
+
|
198
|
+
|
199
|
+
def format_keyword_info_json(keyword_name, keyword_info):
|
200
|
+
"""格式化关键字信息为JSON格式"""
|
201
|
+
category = categorize_keyword(keyword_name, keyword_info)
|
202
|
+
|
203
|
+
keyword_data = {
|
204
|
+
'name': keyword_name,
|
205
|
+
'category': category,
|
206
|
+
'parameters': []
|
207
|
+
}
|
208
|
+
|
209
|
+
# 远程关键字特殊信息
|
210
|
+
if keyword_info.get('remote', False):
|
211
|
+
keyword_data['remote'] = {
|
212
|
+
'alias': keyword_info.get('alias', ''),
|
213
|
+
'original_name': keyword_info.get('original_name', keyword_name)
|
214
|
+
}
|
215
|
+
|
216
|
+
# 参数信息
|
217
|
+
parameters = keyword_info.get('parameters', [])
|
218
|
+
for param in parameters:
|
219
|
+
param_data = {
|
220
|
+
'name': getattr(param, 'name', str(param)),
|
221
|
+
'mapping': getattr(param, 'mapping', ''),
|
222
|
+
'description': getattr(param, 'description', '')
|
223
|
+
}
|
224
|
+
keyword_data['parameters'].append(param_data)
|
225
|
+
|
226
|
+
# 函数文档
|
227
|
+
func = keyword_info.get('func')
|
228
|
+
if func and hasattr(func, '__doc__') and func.__doc__:
|
229
|
+
keyword_data['documentation'] = func.__doc__.strip()
|
230
|
+
|
231
|
+
return keyword_data
|
232
|
+
|
233
|
+
|
234
|
+
def list_keywords(output_format='text', name_filter=None,
|
235
|
+
category_filter='all'):
|
236
|
+
"""罗列所有关键字信息"""
|
237
|
+
import json
|
238
|
+
|
239
|
+
print("正在加载关键字...")
|
240
|
+
load_all_keywords()
|
241
|
+
|
242
|
+
# 获取所有注册的关键字
|
243
|
+
all_keywords = keyword_manager._keywords
|
244
|
+
|
245
|
+
if not all_keywords:
|
246
|
+
print("未发现任何关键字")
|
247
|
+
return
|
248
|
+
|
249
|
+
# 过滤关键字
|
250
|
+
filtered_keywords = {}
|
251
|
+
|
252
|
+
for name, info in all_keywords.items():
|
253
|
+
# 名称过滤
|
254
|
+
if name_filter and name_filter.lower() not in name.lower():
|
255
|
+
continue
|
256
|
+
|
257
|
+
# 类别过滤
|
258
|
+
if category_filter != 'all':
|
259
|
+
keyword_category = categorize_keyword(name, info)
|
260
|
+
if keyword_category != category_filter:
|
261
|
+
continue
|
262
|
+
|
263
|
+
filtered_keywords[name] = info
|
264
|
+
|
265
|
+
if not filtered_keywords:
|
266
|
+
if name_filter:
|
267
|
+
print(f"未找到包含 '{name_filter}' 的关键字")
|
268
|
+
else:
|
269
|
+
print(f"未找到 {category_filter} 类别的关键字")
|
270
|
+
return
|
271
|
+
|
272
|
+
# 输出统计信息
|
273
|
+
total_count = len(filtered_keywords)
|
274
|
+
category_counts = {}
|
275
|
+
for name, info in filtered_keywords.items():
|
276
|
+
cat = categorize_keyword(name, info)
|
277
|
+
category_counts[cat] = category_counts.get(cat, 0) + 1
|
278
|
+
|
279
|
+
if output_format == 'text':
|
280
|
+
print(f"\n找到 {total_count} 个关键字:")
|
281
|
+
for cat, count in category_counts.items():
|
282
|
+
cat_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
|
283
|
+
print(f" {cat_names.get(cat, cat)}: {count} 个")
|
284
|
+
print("-" * 60)
|
285
|
+
|
286
|
+
# 按类别分组显示
|
287
|
+
for category in ['builtin', 'custom', 'remote']:
|
288
|
+
cat_keywords = {
|
289
|
+
name: info for name, info in filtered_keywords.items()
|
290
|
+
if categorize_keyword(name, info) == category
|
291
|
+
}
|
292
|
+
|
293
|
+
if cat_keywords:
|
294
|
+
cat_names = {
|
295
|
+
'builtin': '内置关键字',
|
296
|
+
'custom': '自定义关键字',
|
297
|
+
'remote': '远程关键字'
|
298
|
+
}
|
299
|
+
print(f"\n=== {cat_names[category]} ===")
|
300
|
+
|
301
|
+
for name in sorted(cat_keywords.keys()):
|
302
|
+
info = cat_keywords[name]
|
303
|
+
print()
|
304
|
+
print(format_keyword_info_text(
|
305
|
+
name, info, show_category=False
|
306
|
+
))
|
307
|
+
|
308
|
+
elif output_format == 'json':
|
309
|
+
keywords_data = {
|
310
|
+
'summary': {
|
311
|
+
'total_count': total_count,
|
312
|
+
'category_counts': category_counts
|
313
|
+
},
|
314
|
+
'keywords': []
|
315
|
+
}
|
316
|
+
|
317
|
+
for name in sorted(filtered_keywords.keys()):
|
318
|
+
info = filtered_keywords[name]
|
319
|
+
keyword_data = format_keyword_info_json(name, info)
|
320
|
+
keywords_data['keywords'].append(keyword_data)
|
321
|
+
|
322
|
+
print(json.dumps(keywords_data, ensure_ascii=False, indent=2))
|
323
|
+
|
324
|
+
|
325
|
+
def load_yaml_variables(args):
|
326
|
+
"""从命令行参数加载YAML变量"""
|
327
|
+
# 使用统一的加载函数,包含远程服务器自动连接功能
|
328
|
+
try:
|
329
|
+
load_yaml_variables_from_args(
|
330
|
+
yaml_files=args.yaml_vars,
|
331
|
+
yaml_vars_dir=args.yaml_vars_dir,
|
332
|
+
project_root=os.getcwd() # CLI模式下使用当前工作目录作为项目根目录
|
333
|
+
)
|
334
|
+
except Exception as e:
|
335
|
+
print(f"加载YAML变量失败: {str(e)}")
|
336
|
+
sys.exit(1)
|
337
|
+
|
338
|
+
|
339
|
+
def execute_dsl_file(file_path, lexer, parser, executor):
|
340
|
+
"""执行单个DSL文件"""
|
341
|
+
try:
|
342
|
+
print(f"执行文件: {file_path}")
|
343
|
+
dsl_code = read_file(file_path)
|
344
|
+
ast = parser.parse(dsl_code, lexer=lexer)
|
345
|
+
executor.execute(ast)
|
346
|
+
return True
|
347
|
+
except Exception as e:
|
348
|
+
print(f"执行失败 {file_path}: {e}")
|
349
|
+
return False
|
350
|
+
|
351
|
+
|
352
|
+
def find_dsl_files(directory):
|
353
|
+
"""查找目录中的所有DSL文件"""
|
354
|
+
dsl_files = []
|
355
|
+
for root, _, files in os.walk(directory):
|
356
|
+
for file in files:
|
357
|
+
if (file.endswith(('.dsl', '.auto')) and
|
358
|
+
file not in [SETUP_FILE_NAME, TEARDOWN_FILE_NAME]):
|
359
|
+
dsl_files.append(os.path.join(root, file))
|
360
|
+
return dsl_files
|
361
|
+
|
362
|
+
|
363
|
+
def run_dsl_tests(args):
|
364
|
+
"""执行DSL测试的主函数"""
|
365
|
+
path = args.path
|
366
|
+
|
367
|
+
if not path:
|
368
|
+
print("错误: 必须指定要执行的DSL文件路径或目录")
|
369
|
+
sys.exit(1)
|
370
|
+
|
371
|
+
# 加载内置关键字插件
|
372
|
+
load_all_keywords()
|
373
|
+
|
374
|
+
# 加载YAML变量(包括远程服务器自动连接)
|
375
|
+
load_yaml_variables(args)
|
376
|
+
|
377
|
+
lexer = get_lexer()
|
378
|
+
parser = get_parser()
|
379
|
+
executor = DSLExecutor()
|
380
|
+
|
381
|
+
# 检查路径是文件还是目录
|
382
|
+
if os.path.isfile(path):
|
383
|
+
# 执行单个文件
|
384
|
+
success = execute_dsl_file(path, lexer, parser, executor)
|
385
|
+
if not success:
|
386
|
+
sys.exit(1)
|
387
|
+
elif os.path.isdir(path):
|
388
|
+
# 执行目录中的所有DSL文件
|
389
|
+
print(f"执行目录: {path}")
|
390
|
+
|
391
|
+
# 先执行目录的setup文件(如果存在)
|
392
|
+
setup_file = os.path.join(path, SETUP_FILE_NAME)
|
393
|
+
if os.path.exists(setup_file):
|
394
|
+
execute_hook_file(Path(setup_file), True, path)
|
395
|
+
|
396
|
+
# 查找并执行所有DSL文件
|
397
|
+
dsl_files = find_dsl_files(path)
|
398
|
+
if not dsl_files:
|
399
|
+
print(f"目录中没有找到DSL文件: {path}")
|
400
|
+
sys.exit(1)
|
401
|
+
|
402
|
+
print(f"找到 {len(dsl_files)} 个DSL文件")
|
403
|
+
|
404
|
+
# 执行所有DSL文件
|
405
|
+
failures = 0
|
406
|
+
for file_path in dsl_files:
|
407
|
+
success = execute_dsl_file(file_path, lexer, parser, executor)
|
408
|
+
if not success:
|
409
|
+
failures += 1
|
410
|
+
|
411
|
+
# 最后执行目录的teardown文件(如果存在)
|
412
|
+
teardown_file = os.path.join(path, TEARDOWN_FILE_NAME)
|
413
|
+
if os.path.exists(teardown_file):
|
414
|
+
execute_hook_file(Path(teardown_file), False, path)
|
415
|
+
|
416
|
+
# 如果有失败的测试,返回非零退出码
|
417
|
+
if failures > 0:
|
418
|
+
print(f"总计 {failures}/{len(dsl_files)} 个测试失败")
|
419
|
+
sys.exit(1)
|
420
|
+
else:
|
421
|
+
print(f"所有 {len(dsl_files)} 个测试成功完成")
|
422
|
+
else:
|
423
|
+
print(f"路径不存在: {path}")
|
424
|
+
sys.exit(1)
|
425
|
+
|
426
|
+
|
427
|
+
def main():
|
428
|
+
"""命令行入口点"""
|
429
|
+
args = parse_args()
|
430
|
+
|
431
|
+
# 处理子命令
|
432
|
+
if args.command == 'list-keywords':
|
433
|
+
list_keywords(
|
434
|
+
output_format=args.format,
|
435
|
+
name_filter=args.filter,
|
436
|
+
category_filter=args.category
|
437
|
+
)
|
438
|
+
elif args.command == 'run':
|
439
|
+
run_dsl_tests(args)
|
440
|
+
elif args.command == 'list-keywords-compat':
|
441
|
+
# 向后兼容:旧的--list-keywords格式
|
442
|
+
list_keywords(
|
443
|
+
output_format=args.format,
|
444
|
+
name_filter=args.filter,
|
445
|
+
category_filter=args.category
|
446
|
+
)
|
447
|
+
elif args.command == 'run-compat':
|
448
|
+
# 向后兼容:默认执行DSL测试
|
449
|
+
run_dsl_tests(args)
|
450
|
+
else:
|
451
|
+
# 如果没有匹配的命令,显示帮助
|
452
|
+
print("错误: 未知命令")
|
453
|
+
sys.exit(1)
|
454
|
+
|
455
|
+
|
456
|
+
if __name__ == '__main__':
|
457
|
+
main()
|