kdtest-pw 2.0.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.
- kdtest_pw/__init__.py +50 -0
- kdtest_pw/action/__init__.py +7 -0
- kdtest_pw/action/base_keyword.py +292 -0
- kdtest_pw/action/element_plus/__init__.py +23 -0
- kdtest_pw/action/element_plus/el_cascader.py +263 -0
- kdtest_pw/action/element_plus/el_datepicker.py +324 -0
- kdtest_pw/action/element_plus/el_dialog.py +317 -0
- kdtest_pw/action/element_plus/el_form.py +443 -0
- kdtest_pw/action/element_plus/el_menu.py +456 -0
- kdtest_pw/action/element_plus/el_select.py +268 -0
- kdtest_pw/action/element_plus/el_table.py +442 -0
- kdtest_pw/action/element_plus/el_tree.py +364 -0
- kdtest_pw/action/element_plus/el_upload.py +313 -0
- kdtest_pw/action/key_retrieval.py +311 -0
- kdtest_pw/action/page_action.py +1129 -0
- kdtest_pw/api/__init__.py +6 -0
- kdtest_pw/api/api_keyword.py +251 -0
- kdtest_pw/api/request_handler.py +232 -0
- kdtest_pw/cases/__init__.py +6 -0
- kdtest_pw/cases/case_collector.py +182 -0
- kdtest_pw/cases/case_executor.py +359 -0
- kdtest_pw/cases/read/__init__.py +6 -0
- kdtest_pw/cases/read/cell_handler.py +305 -0
- kdtest_pw/cases/read/excel_reader.py +223 -0
- kdtest_pw/cli/__init__.py +5 -0
- kdtest_pw/cli/run.py +318 -0
- kdtest_pw/common.py +106 -0
- kdtest_pw/core/__init__.py +7 -0
- kdtest_pw/core/browser_manager.py +196 -0
- kdtest_pw/core/config_loader.py +235 -0
- kdtest_pw/core/page_context.py +228 -0
- kdtest_pw/data/__init__.py +5 -0
- kdtest_pw/data/init_data.py +105 -0
- kdtest_pw/data/static/elementData.yaml +59 -0
- kdtest_pw/data/static/parameters.json +24 -0
- kdtest_pw/plugins/__init__.py +6 -0
- kdtest_pw/plugins/element_plus_plugin/__init__.py +5 -0
- kdtest_pw/plugins/element_plus_plugin/elementData/elementData.yaml +144 -0
- kdtest_pw/plugins/element_plus_plugin/element_plus_plugin.py +237 -0
- kdtest_pw/plugins/element_plus_plugin/my.ini +23 -0
- kdtest_pw/plugins/plugin_base.py +180 -0
- kdtest_pw/plugins/plugin_loader.py +260 -0
- kdtest_pw/product.py +5 -0
- kdtest_pw/reference.py +99 -0
- kdtest_pw/utils/__init__.py +13 -0
- kdtest_pw/utils/built_in_function.py +376 -0
- kdtest_pw/utils/decorator.py +211 -0
- kdtest_pw/utils/log/__init__.py +6 -0
- kdtest_pw/utils/log/html_report.py +336 -0
- kdtest_pw/utils/log/logger.py +123 -0
- kdtest_pw/utils/public_script.py +366 -0
- kdtest_pw-2.0.0.dist-info/METADATA +169 -0
- kdtest_pw-2.0.0.dist-info/RECORD +57 -0
- kdtest_pw-2.0.0.dist-info/WHEEL +5 -0
- kdtest_pw-2.0.0.dist-info/entry_points.txt +2 -0
- kdtest_pw-2.0.0.dist-info/licenses/LICENSE +21 -0
- kdtest_pw-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""API 测试关键字"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from .request_handler import RequestHandler
|
|
7
|
+
from ..reference import INFO, set_element_value, get_element_value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiKeyword:
|
|
11
|
+
"""API 测试关键字
|
|
12
|
+
|
|
13
|
+
提供 API 测试的关键字方法。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, request_handler: RequestHandler = None):
|
|
17
|
+
"""初始化 API 关键字
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
request_handler: 请求处理器
|
|
21
|
+
"""
|
|
22
|
+
self._handler = request_handler or RequestHandler()
|
|
23
|
+
|
|
24
|
+
def api_set_base_url(self, *, content: str) -> None:
|
|
25
|
+
"""设置基础 URL
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
content: 基础 URL
|
|
29
|
+
"""
|
|
30
|
+
self._handler._base_url = content
|
|
31
|
+
INFO(f"设置基础 URL: {content}")
|
|
32
|
+
|
|
33
|
+
def api_set_header(self, *, content: str) -> None:
|
|
34
|
+
"""设置请求头
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
content: JSON 格式的请求头 {"key": "value"}
|
|
38
|
+
"""
|
|
39
|
+
headers = json.loads(content)
|
|
40
|
+
self._handler.set_default_headers(headers)
|
|
41
|
+
INFO(f"设置请求头: {list(headers.keys())}")
|
|
42
|
+
|
|
43
|
+
def api_get(self, *, content: str, key: str = None) -> Dict[str, Any]:
|
|
44
|
+
"""GET 请求
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
content: URL 或 JSON 配置 {"url": "...", "params": {...}}
|
|
48
|
+
key: 响应缓存键名
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict: 响应数据
|
|
52
|
+
"""
|
|
53
|
+
if content.startswith('{'):
|
|
54
|
+
config = json.loads(content)
|
|
55
|
+
url = config.get('url', '')
|
|
56
|
+
params = config.get('params')
|
|
57
|
+
headers = config.get('headers')
|
|
58
|
+
response = self._handler.get(url, params=params, headers=headers)
|
|
59
|
+
else:
|
|
60
|
+
response = self._handler.get(content)
|
|
61
|
+
|
|
62
|
+
if key:
|
|
63
|
+
set_element_value(key, response)
|
|
64
|
+
|
|
65
|
+
return response
|
|
66
|
+
|
|
67
|
+
def api_post(self, *, content: str, key: str = None) -> Dict[str, Any]:
|
|
68
|
+
"""POST 请求
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
content: JSON 配置 {"url": "...", "data": {...}}
|
|
72
|
+
key: 响应缓存键名
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dict: 响应数据
|
|
76
|
+
"""
|
|
77
|
+
config = json.loads(content)
|
|
78
|
+
url = config.get('url', '')
|
|
79
|
+
data = config.get('data')
|
|
80
|
+
json_data = config.get('json')
|
|
81
|
+
headers = config.get('headers')
|
|
82
|
+
|
|
83
|
+
response = self._handler.post(url, data=data, json_data=json_data, headers=headers)
|
|
84
|
+
|
|
85
|
+
if key:
|
|
86
|
+
set_element_value(key, response)
|
|
87
|
+
|
|
88
|
+
return response
|
|
89
|
+
|
|
90
|
+
def api_put(self, *, content: str, key: str = None) -> Dict[str, Any]:
|
|
91
|
+
"""PUT 请求
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
content: JSON 配置
|
|
95
|
+
key: 响应缓存键名
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Dict: 响应数据
|
|
99
|
+
"""
|
|
100
|
+
config = json.loads(content)
|
|
101
|
+
url = config.get('url', '')
|
|
102
|
+
data = config.get('data')
|
|
103
|
+
json_data = config.get('json')
|
|
104
|
+
headers = config.get('headers')
|
|
105
|
+
|
|
106
|
+
response = self._handler.put(url, data=data, json_data=json_data, headers=headers)
|
|
107
|
+
|
|
108
|
+
if key:
|
|
109
|
+
set_element_value(key, response)
|
|
110
|
+
|
|
111
|
+
return response
|
|
112
|
+
|
|
113
|
+
def api_delete(self, *, content: str, key: str = None) -> Dict[str, Any]:
|
|
114
|
+
"""DELETE 请求
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
content: URL 或 JSON 配置
|
|
118
|
+
key: 响应缓存键名
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dict: 响应数据
|
|
122
|
+
"""
|
|
123
|
+
if content.startswith('{'):
|
|
124
|
+
config = json.loads(content)
|
|
125
|
+
url = config.get('url', '')
|
|
126
|
+
headers = config.get('headers')
|
|
127
|
+
response = self._handler.delete(url, headers=headers)
|
|
128
|
+
else:
|
|
129
|
+
response = self._handler.delete(content)
|
|
130
|
+
|
|
131
|
+
if key:
|
|
132
|
+
set_element_value(key, response)
|
|
133
|
+
|
|
134
|
+
return response
|
|
135
|
+
|
|
136
|
+
def api_status_assert(self, *, content: str) -> bool:
|
|
137
|
+
"""断言响应状态码
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
content: 期望的状态码
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
bool: 断言结果
|
|
144
|
+
"""
|
|
145
|
+
response = self._handler.last_response
|
|
146
|
+
if not response:
|
|
147
|
+
INFO("没有响应数据", "ERROR")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
expected = int(content)
|
|
151
|
+
actual = response.get('status')
|
|
152
|
+
result = actual == expected
|
|
153
|
+
|
|
154
|
+
INFO(f"状态码断言: {actual} == {expected} -> {result}")
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
def api_json_assert(self, *, content: str, path: str, mode: str = 'equals') -> bool:
|
|
158
|
+
"""断言响应 JSON 字段
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
content: 期望值
|
|
162
|
+
path: JSON 路径 (如 "data.user.name")
|
|
163
|
+
mode: 断言模式
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
bool: 断言结果
|
|
167
|
+
"""
|
|
168
|
+
response = self._handler.last_response
|
|
169
|
+
if not response or 'json' not in response:
|
|
170
|
+
INFO("没有 JSON 响应数据", "ERROR")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# 获取实际值
|
|
174
|
+
actual = self._get_json_value(response['json'], path)
|
|
175
|
+
|
|
176
|
+
# 断言
|
|
177
|
+
if mode == 'equals':
|
|
178
|
+
result = str(actual) == content
|
|
179
|
+
elif mode == 'contains':
|
|
180
|
+
result = content in str(actual)
|
|
181
|
+
elif mode == 'exists':
|
|
182
|
+
result = actual is not None
|
|
183
|
+
elif mode == 'not_exists':
|
|
184
|
+
result = actual is None
|
|
185
|
+
else:
|
|
186
|
+
result = str(actual) == content
|
|
187
|
+
|
|
188
|
+
INFO(f"JSON 断言: {path}='{actual}' {mode} '{content}' -> {result}")
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
def api_extract_value(self, *, content: str, path: str) -> Any:
|
|
192
|
+
"""提取响应值并缓存
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
content: 缓存键名
|
|
196
|
+
path: JSON 路径
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
提取的值
|
|
200
|
+
"""
|
|
201
|
+
response = self._handler.last_response
|
|
202
|
+
if not response or 'json' not in response:
|
|
203
|
+
INFO("没有 JSON 响应数据", "ERROR")
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
value = self._get_json_value(response['json'], path)
|
|
207
|
+
set_element_value(content, value)
|
|
208
|
+
|
|
209
|
+
INFO(f"提取值: {content} = {path} -> {value}")
|
|
210
|
+
return value
|
|
211
|
+
|
|
212
|
+
def api_get_response(self, *, content: str) -> Optional[Dict[str, Any]]:
|
|
213
|
+
"""获取缓存的响应
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
content: 缓存键名
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
响应数据
|
|
220
|
+
"""
|
|
221
|
+
return get_element_value(content)
|
|
222
|
+
|
|
223
|
+
def _get_json_value(self, data: Any, path: str) -> Any:
|
|
224
|
+
"""从 JSON 获取值
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
data: JSON 数据
|
|
228
|
+
path: 路径 (支持点号和数组索引)
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
值
|
|
232
|
+
"""
|
|
233
|
+
keys = path.replace('[', '.').replace(']', '').split('.')
|
|
234
|
+
value = data
|
|
235
|
+
|
|
236
|
+
for key in keys:
|
|
237
|
+
if not key:
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
if isinstance(value, dict):
|
|
241
|
+
value = value.get(key)
|
|
242
|
+
elif isinstance(value, list) and key.isdigit():
|
|
243
|
+
index = int(key)
|
|
244
|
+
value = value[index] if 0 <= index < len(value) else None
|
|
245
|
+
else:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
if value is None:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
return value
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""API 请求处理器"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
from playwright.sync_api import APIRequestContext, BrowserContext
|
|
5
|
+
|
|
6
|
+
from ..reference import GSTORE, INFO, set_element_value
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RequestHandler:
|
|
10
|
+
"""API 请求处理器
|
|
11
|
+
|
|
12
|
+
使用 Playwright 的 APIRequestContext 发送 HTTP 请求。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, context: Optional[BrowserContext] = None, base_url: str = ''):
|
|
16
|
+
"""初始化请求处理器
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
context: BrowserContext 实例
|
|
20
|
+
base_url: 基础 URL
|
|
21
|
+
"""
|
|
22
|
+
self._context = context or GSTORE.get('context')
|
|
23
|
+
self._base_url = base_url
|
|
24
|
+
self._request: Optional[APIRequestContext] = None
|
|
25
|
+
self._default_headers: Dict[str, str] = {}
|
|
26
|
+
self._last_response: Optional[Dict[str, Any]] = None
|
|
27
|
+
|
|
28
|
+
def create_request_context(
|
|
29
|
+
self,
|
|
30
|
+
base_url: str = None,
|
|
31
|
+
extra_headers: Dict[str, str] = None
|
|
32
|
+
) -> None:
|
|
33
|
+
"""创建请求上下文
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
base_url: 基础 URL
|
|
37
|
+
extra_headers: 额外请求头
|
|
38
|
+
"""
|
|
39
|
+
if self._context:
|
|
40
|
+
options = {}
|
|
41
|
+
if base_url:
|
|
42
|
+
options['base_url'] = base_url
|
|
43
|
+
if extra_headers:
|
|
44
|
+
options['extra_http_headers'] = extra_headers
|
|
45
|
+
|
|
46
|
+
self._request = self._context.request
|
|
47
|
+
|
|
48
|
+
def set_default_headers(self, headers: Dict[str, str]) -> None:
|
|
49
|
+
"""设置默认请求头
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
headers: 请求头字典
|
|
53
|
+
"""
|
|
54
|
+
self._default_headers.update(headers)
|
|
55
|
+
|
|
56
|
+
def get(
|
|
57
|
+
self,
|
|
58
|
+
url: str,
|
|
59
|
+
params: Dict[str, Any] = None,
|
|
60
|
+
headers: Dict[str, str] = None
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
"""GET 请求
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
url: 请求 URL
|
|
66
|
+
params: 查询参数
|
|
67
|
+
headers: 请求头
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Dict: 响应数据
|
|
71
|
+
"""
|
|
72
|
+
return self._request_method('get', url, params=params, headers=headers)
|
|
73
|
+
|
|
74
|
+
def post(
|
|
75
|
+
self,
|
|
76
|
+
url: str,
|
|
77
|
+
data: Any = None,
|
|
78
|
+
json_data: Dict = None,
|
|
79
|
+
headers: Dict[str, str] = None
|
|
80
|
+
) -> Dict[str, Any]:
|
|
81
|
+
"""POST 请求
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
url: 请求 URL
|
|
85
|
+
data: 表单数据
|
|
86
|
+
json_data: JSON 数据
|
|
87
|
+
headers: 请求头
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dict: 响应数据
|
|
91
|
+
"""
|
|
92
|
+
return self._request_method('post', url, data=data, json_data=json_data, headers=headers)
|
|
93
|
+
|
|
94
|
+
def put(
|
|
95
|
+
self,
|
|
96
|
+
url: str,
|
|
97
|
+
data: Any = None,
|
|
98
|
+
json_data: Dict = None,
|
|
99
|
+
headers: Dict[str, str] = None
|
|
100
|
+
) -> Dict[str, Any]:
|
|
101
|
+
"""PUT 请求
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
url: 请求 URL
|
|
105
|
+
data: 表单数据
|
|
106
|
+
json_data: JSON 数据
|
|
107
|
+
headers: 请求头
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dict: 响应数据
|
|
111
|
+
"""
|
|
112
|
+
return self._request_method('put', url, data=data, json_data=json_data, headers=headers)
|
|
113
|
+
|
|
114
|
+
def delete(
|
|
115
|
+
self,
|
|
116
|
+
url: str,
|
|
117
|
+
headers: Dict[str, str] = None
|
|
118
|
+
) -> Dict[str, Any]:
|
|
119
|
+
"""DELETE 请求
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
url: 请求 URL
|
|
123
|
+
headers: 请求头
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dict: 响应数据
|
|
127
|
+
"""
|
|
128
|
+
return self._request_method('delete', url, headers=headers)
|
|
129
|
+
|
|
130
|
+
def patch(
|
|
131
|
+
self,
|
|
132
|
+
url: str,
|
|
133
|
+
data: Any = None,
|
|
134
|
+
json_data: Dict = None,
|
|
135
|
+
headers: Dict[str, str] = None
|
|
136
|
+
) -> Dict[str, Any]:
|
|
137
|
+
"""PATCH 请求
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
url: 请求 URL
|
|
141
|
+
data: 表单数据
|
|
142
|
+
json_data: JSON 数据
|
|
143
|
+
headers: 请求头
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Dict: 响应数据
|
|
147
|
+
"""
|
|
148
|
+
return self._request_method('patch', url, data=data, json_data=json_data, headers=headers)
|
|
149
|
+
|
|
150
|
+
def _request_method(
|
|
151
|
+
self,
|
|
152
|
+
method: str,
|
|
153
|
+
url: str,
|
|
154
|
+
params: Dict[str, Any] = None,
|
|
155
|
+
data: Any = None,
|
|
156
|
+
json_data: Dict = None,
|
|
157
|
+
headers: Dict[str, str] = None
|
|
158
|
+
) -> Dict[str, Any]:
|
|
159
|
+
"""发送请求
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
method: 请求方法
|
|
163
|
+
url: 请求 URL
|
|
164
|
+
params: 查询参数
|
|
165
|
+
data: 表单数据
|
|
166
|
+
json_data: JSON 数据
|
|
167
|
+
headers: 请求头
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Dict: 响应数据
|
|
171
|
+
"""
|
|
172
|
+
if not self._request and self._context:
|
|
173
|
+
self._request = self._context.request
|
|
174
|
+
|
|
175
|
+
if not self._request:
|
|
176
|
+
raise RuntimeError("请求上下文未初始化")
|
|
177
|
+
|
|
178
|
+
# 合并请求头
|
|
179
|
+
final_headers = {**self._default_headers}
|
|
180
|
+
if headers:
|
|
181
|
+
final_headers.update(headers)
|
|
182
|
+
|
|
183
|
+
# 构建完整 URL
|
|
184
|
+
full_url = f"{self._base_url}{url}" if self._base_url and not url.startswith('http') else url
|
|
185
|
+
|
|
186
|
+
# 构建请求参数
|
|
187
|
+
request_kwargs = {
|
|
188
|
+
'headers': final_headers if final_headers else None,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if params:
|
|
192
|
+
request_kwargs['params'] = params
|
|
193
|
+
if data:
|
|
194
|
+
request_kwargs['data'] = data
|
|
195
|
+
if json_data:
|
|
196
|
+
request_kwargs['data'] = json_data
|
|
197
|
+
|
|
198
|
+
INFO(f"API {method.upper()}: {full_url}")
|
|
199
|
+
|
|
200
|
+
# 发送请求
|
|
201
|
+
request_method = getattr(self._request, method)
|
|
202
|
+
response = request_method(full_url, **{k: v for k, v in request_kwargs.items() if v is not None})
|
|
203
|
+
|
|
204
|
+
# 解析响应
|
|
205
|
+
result = {
|
|
206
|
+
'status': response.status,
|
|
207
|
+
'status_text': response.status_text,
|
|
208
|
+
'headers': dict(response.headers),
|
|
209
|
+
'url': response.url,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# 尝试解析 JSON
|
|
213
|
+
try:
|
|
214
|
+
result['json'] = response.json()
|
|
215
|
+
except Exception:
|
|
216
|
+
result['text'] = response.text()
|
|
217
|
+
|
|
218
|
+
self._last_response = result
|
|
219
|
+
INFO(f"API 响应: {response.status} {response.status_text}")
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def last_response(self) -> Optional[Dict[str, Any]]:
|
|
225
|
+
"""获取最后一次响应"""
|
|
226
|
+
return self._last_response
|
|
227
|
+
|
|
228
|
+
def dispose(self) -> None:
|
|
229
|
+
"""释放资源"""
|
|
230
|
+
if self._request:
|
|
231
|
+
self._request.dispose()
|
|
232
|
+
self._request = None
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""测试用例收集器"""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .read.excel_reader import ExcelReader, TestCase, TestStep
|
|
7
|
+
from ..reference import INFO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CaseCollector:
|
|
11
|
+
"""测试用例收集器
|
|
12
|
+
|
|
13
|
+
从配置和 Excel 文件收集测试用例。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: Dict[str, Any] = None):
|
|
17
|
+
"""初始化用例收集器
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
config: 配置字典
|
|
21
|
+
"""
|
|
22
|
+
self._config = config or {}
|
|
23
|
+
self._cases: List[TestCase] = []
|
|
24
|
+
self._case_files: List[Dict[str, Any]] = []
|
|
25
|
+
|
|
26
|
+
def add_case_file(
|
|
27
|
+
self,
|
|
28
|
+
file_path: str,
|
|
29
|
+
sheets: List[str] = None,
|
|
30
|
+
interface_switch: bool = False
|
|
31
|
+
) -> None:
|
|
32
|
+
"""添加测试用例文件
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
file_path: Excel 文件路径
|
|
36
|
+
sheets: 要执行的 Sheet 列表,None 表示全部
|
|
37
|
+
interface_switch: 是否接口测试
|
|
38
|
+
"""
|
|
39
|
+
self._case_files.append({
|
|
40
|
+
'path': file_path,
|
|
41
|
+
'sheets': sheets,
|
|
42
|
+
'interface': interface_switch
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
def collect(self) -> List[TestCase]:
|
|
46
|
+
"""收集所有测试用例
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List[TestCase]: 测试用例列表
|
|
50
|
+
"""
|
|
51
|
+
self._cases.clear()
|
|
52
|
+
|
|
53
|
+
# 从配置收集
|
|
54
|
+
if 'testCaseFile' in self._config:
|
|
55
|
+
for file_config in self._config['testCaseFile']:
|
|
56
|
+
self.add_case_file(
|
|
57
|
+
file_path=file_config.get('caseFilePath', ''),
|
|
58
|
+
sheets=file_config.get('caseItem'),
|
|
59
|
+
interface_switch=file_config.get('interfaceSwitch', False)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 收集用例
|
|
63
|
+
for file_info in self._case_files:
|
|
64
|
+
self._collect_from_file(file_info)
|
|
65
|
+
|
|
66
|
+
INFO(f"共收集 {len(self._cases)} 个测试用例")
|
|
67
|
+
return self._cases
|
|
68
|
+
|
|
69
|
+
def _collect_from_file(self, file_info: Dict[str, Any]) -> None:
|
|
70
|
+
"""从文件收集用例
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
file_info: 文件信息
|
|
74
|
+
"""
|
|
75
|
+
file_path = file_info['path']
|
|
76
|
+
sheets = file_info['sheets']
|
|
77
|
+
|
|
78
|
+
if not Path(file_path).exists():
|
|
79
|
+
INFO(f"用例文件不存在: {file_path}", "WARNING")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
with ExcelReader(file_path) as reader:
|
|
84
|
+
if sheets:
|
|
85
|
+
# 只读取指定 Sheet
|
|
86
|
+
all_cases = reader.read_all_sheets(sheets)
|
|
87
|
+
else:
|
|
88
|
+
# 读取所有 Sheet
|
|
89
|
+
all_cases = reader.read_all_sheets()
|
|
90
|
+
|
|
91
|
+
for sheet_name, cases in all_cases.items():
|
|
92
|
+
for case_data in cases:
|
|
93
|
+
case_data['file'] = file_path
|
|
94
|
+
case_data['interface'] = file_info['interface']
|
|
95
|
+
self._cases.append(TestCase(case_data))
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
INFO(f"读取用例文件失败: {file_path} - {e}", "ERROR")
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def cases(self) -> List[TestCase]:
|
|
102
|
+
"""获取所有测试用例"""
|
|
103
|
+
return self._cases.copy()
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def case_count(self) -> int:
|
|
107
|
+
"""获取用例数量"""
|
|
108
|
+
return len(self._cases)
|
|
109
|
+
|
|
110
|
+
def get_cases_by_sheet(self, sheet_name: str) -> List[TestCase]:
|
|
111
|
+
"""按 Sheet 获取用例
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
sheet_name: Sheet 名称
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List[TestCase]: 匹配的用例列表
|
|
118
|
+
"""
|
|
119
|
+
return [c for c in self._cases if c.sheet == sheet_name]
|
|
120
|
+
|
|
121
|
+
def get_cases_by_name(self, name_pattern: str) -> List[TestCase]:
|
|
122
|
+
"""按名称模式获取用例
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
name_pattern: 名称模式(支持通配符 *)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List[TestCase]: 匹配的用例列表
|
|
129
|
+
"""
|
|
130
|
+
import fnmatch
|
|
131
|
+
return [c for c in self._cases if fnmatch.fnmatch(c.name, name_pattern)]
|
|
132
|
+
|
|
133
|
+
def get_cases_by_id(self, case_ids: List[str]) -> List[TestCase]:
|
|
134
|
+
"""按 ID 获取用例
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
case_ids: 用例 ID 列表
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List[TestCase]: 匹配的用例列表
|
|
141
|
+
"""
|
|
142
|
+
return [c for c in self._cases if c.id in case_ids]
|
|
143
|
+
|
|
144
|
+
def filter_cases(
|
|
145
|
+
self,
|
|
146
|
+
include_sheets: List[str] = None,
|
|
147
|
+
exclude_sheets: List[str] = None,
|
|
148
|
+
include_ids: List[str] = None,
|
|
149
|
+
exclude_ids: List[str] = None,
|
|
150
|
+
name_pattern: str = None
|
|
151
|
+
) -> List[TestCase]:
|
|
152
|
+
"""过滤用例
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
include_sheets: 包含的 Sheet
|
|
156
|
+
exclude_sheets: 排除的 Sheet
|
|
157
|
+
include_ids: 包含的 ID
|
|
158
|
+
exclude_ids: 排除的 ID
|
|
159
|
+
name_pattern: 名称模式
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List[TestCase]: 过滤后的用例列表
|
|
163
|
+
"""
|
|
164
|
+
result = self._cases.copy()
|
|
165
|
+
|
|
166
|
+
if include_sheets:
|
|
167
|
+
result = [c for c in result if c.sheet in include_sheets]
|
|
168
|
+
|
|
169
|
+
if exclude_sheets:
|
|
170
|
+
result = [c for c in result if c.sheet not in exclude_sheets]
|
|
171
|
+
|
|
172
|
+
if include_ids:
|
|
173
|
+
result = [c for c in result if c.id in include_ids]
|
|
174
|
+
|
|
175
|
+
if exclude_ids:
|
|
176
|
+
result = [c for c in result if c.id not in exclude_ids]
|
|
177
|
+
|
|
178
|
+
if name_pattern:
|
|
179
|
+
import fnmatch
|
|
180
|
+
result = [c for c in result if fnmatch.fnmatch(c.name, name_pattern)]
|
|
181
|
+
|
|
182
|
+
return result
|