WebsocketTest 1.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.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.1
2
+ Name: WebsocketTest
3
+ Version: 1.0.0
4
+ Summary: websocket api autotest
5
+ Author: chencheng
6
+ Requires-Python: >=3.7
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: allure-python-commons==2.13.5
9
+ Requires-Dist: numpy==2.2.4
10
+ Requires-Dist: pandas==2.2.3
11
+ Requires-Dist: pytest==8.2.2
12
+ Requires-Dist: PyYAML==6.0.2
13
+ Requires-Dist: websockets==12.0
14
+
15
+ # WebSocket 接口自动化测试工具 (WS_API_TEST)
16
+
17
+ #### 介绍
18
+
19
+ 这是一个基于 WebSocket 协议的接口自动化测试工具。它可以用于自动化测试 WebSocket 接口,确保接口的稳定性和可靠性。
20
+
21
+ #### 系统要求
22
+
23
+ **Python 3.10+**:确保你的系统上已经安装了 Python 3.10(推荐使用最新稳定版)。
24
+ **项目依赖**:项目需要一些第三方库,可以通过 requirements.txt 文件安装。
25
+
26
+ #### 安装步骤
27
+
28
+ **1.安装 Python:**
29
+ 确保你的系统上已经安装了 Python 3.10 或更高版本。你可以从 [Python 官方网站 ](https://www.python.org/downloads/?spm=5176.28103460.0.0.40f75d27PnqPkU)下载并安装。
30
+ **2.克隆项目:**
31
+ 使用 Git 克隆项目到你的本地机器。
32
+
33
+ ```
34
+ git clone https://code.iflytek.com/ZNQC_AUTO_AI/python_scripts.git
35
+ cd python_scripts
36
+ git checkout WS_API_TEST
37
+ ```
38
+
39
+ **3.安装项目依赖:**
40
+ 使用 pip 安装项目所需的依赖库。
41
+
42
+ ```
43
+ pip install -r requirements.txt
44
+ ```
45
+ **4.运行项目:**
46
+ 你可以通过以下两种方式之一来运行项目:
47
+
48
+ * **使用命令行**:
49
+ 在命令行中运行以下命令:
50
+
51
+ ```
52
+ python run_tests.py --env uat --app 3d7d3ea4 --service gateway_5.4 --project vwa
53
+ ```
54
+
55
+ * **使用批处理脚本**:
56
+ 双击 run_tests.bat 文件来运行项目。
57
+
58
+ #### 项目结构
59
+ ├─allure_report
60
+ ├─allure_results
61
+ ├─build
62
+ ├─common
63
+ ├─config
64
+ ├─data
65
+ ├─dist
66
+ ├─logs
67
+ ├─testcase
68
+
69
+ * **文件说明**:
70
+ **allure_report**:存放 Allure 生成的 HTML 报告,用于展示测试结果。
71
+ **allure_results**:存放 Allure 生成的测试结果数据文件,如 result-12345.xml。
72
+ **build**:存放构建过程中生成的临时文件
73
+ **common**:存放通用的工具类
74
+ **config**:存放环境配置文件
75
+ **data**:存放测试数据文件
76
+ **dist**:存放最终发布的测试包
77
+ **logs**:存放运行日志文件
78
+ **testcase**:存放测试用例脚本
@@ -0,0 +1,17 @@
1
+ caseScript/Aqua.py,sha256=zr1mURYGikzzJDsx2lwdH9m_ENlJzH3vDvepsfwhfdg,7405
2
+ caseScript/Gateway.py,sha256=D_oVhhKGFA3FdFblg_YmUj2HhlPNI3kht9LqHnSuNZA,11723
3
+ caseScript/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ common/Assertion.py,sha256=xMesYiZY6FSf7_Zf6BIXGl9a2Prx-blRYGK2zKQy81M,5215
5
+ common/WSBaseApi.py,sha256=-zDpMsaF4ugVKUpzDwbs6l9DBvlh2uw1oJGY64fk6fY,3982
6
+ common/WebSocketApi.py,sha256=JpgX1qAA8skh_t9hLDmgEx6KugewF284u2cjrQkbLnI,2743
7
+ common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ common/assertUtils.py,sha256=xJvjm0QDIxUHIZm96-2sKkDUWzPQHaOJB9x9eSLlIoU,4699
9
+ common/logger.py,sha256=B7jjPd5miAYorveHUEGW8xKxUNlJzKlVug0rKWRK3p0,783
10
+ common/utils.py,sha256=5NrU_cXzpzTYSI38LZe4IjJe3HxNB64rtrXZcf3Vixs,8706
11
+ testcase/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ testcase/test_all.py,sha256=mdfivt5vmfcn_4LZVcQ1KTH0n69gKxd_Id-DCapGlu4,198
13
+ WebsocketTest-1.0.0.dist-info/METADATA,sha256=hvOrTxBDRTSSsRwEBzdiXqf1wOvCMgc1tx-kPUCG5Ik,2378
14
+ WebsocketTest-1.0.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
15
+ WebsocketTest-1.0.0.dist-info/entry_points.txt,sha256=IHzfkbidO-zy6nSbDStRwp2Qa0C6zHgD8M1Tq_ik9rA,195
16
+ WebsocketTest-1.0.0.dist-info/top_level.txt,sha256=KmqByBRxmIHZiIEEGQnX5_C3urP-43-6TCFUhM3aMKk,27
17
+ WebsocketTest-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.45.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ har2case = pastor.cli:main_har2case_alias
3
+ locusts = pastor.ext.locust:main_locusts
4
+ pastor = pastor.cli:main
5
+ pmake = pastor.cli:main_make_alias
6
+ prun = pastor.cli:main_prun_alias
@@ -0,0 +1,3 @@
1
+ caseScript
2
+ common
3
+ testcase
caseScript/Aqua.py ADDED
@@ -0,0 +1,165 @@
1
+
2
+ from common.utils import *
3
+ from urllib.parse import quote_plus
4
+ from common import WSBaseApi
5
+ from common.WSBaseApi import WSBaseApi
6
+
7
+
8
+
9
+
10
+ class ApiTestRunner(WSBaseApi):
11
+ def __init__(self, **kwargs):
12
+ super().__init__(**kwargs)
13
+ self.answer_text = ""
14
+ def build_params(self):
15
+ """准备请求参数"""
16
+ return {
17
+ "header": {
18
+ "appid": self.appId,
19
+ "scene": self.scene,
20
+ "sid": self.sid or generate_random_string(), # 奥迪sid为空时,不会自生成
21
+ "uid": self.uid,
22
+ "usrid": ""
23
+ },
24
+ "parameter": {
25
+ "custom": {
26
+ "custom_data": {
27
+ "SessionParams": {
28
+ "isLog": "true",
29
+ "app_id": "",
30
+ "attachparams": {
31
+ "iat_params": {
32
+ "compress": "raw",
33
+ "da": "0",
34
+ "domain": "aiui-automotiveknife",
35
+ "dwa": "wpgs",
36
+ "encoding": "utf8",
37
+ "eos": "600",
38
+ "format": "json",
39
+ "isFar": "0",
40
+ "opt": "2",
41
+ "ufsa": "1",
42
+ "vgap": "200",
43
+ "accent": self.accent,
44
+ "language": self.language
45
+ },
46
+ "nlp_params": {
47
+ "llmEnv": "test",
48
+ "ovs_cluster":"AUDI",
49
+ "city": "合肥",
50
+ "compress": "raw",
51
+ "encoding": "utf8",
52
+ "format": "json",
53
+ "devid": "",
54
+ "news": {
55
+ "pageNo": 1,
56
+ "pageSize": 20
57
+ },
58
+ "flight": {
59
+ "pageNo": 1,
60
+ "pageSize": 20
61
+ },
62
+ "ovs_version": {
63
+ "weather": "3.5"
64
+ },
65
+ "user_defined_params": {},
66
+ "weather_airquality": "true",
67
+ "mapU": {
68
+ "pageNo": 1,
69
+ "pageSize": 20
70
+ },
71
+ "deviceId": self.deviceId,
72
+ "userId": self.userId,
73
+ "asp_did": self.asp_did,
74
+ "vWtoken": self.token,
75
+ "car_identity": self.car_identity,
76
+ "theme": "standard",
77
+ "vin": self.vin,
78
+ "interactive_mode": "fullDuplex",
79
+ "did": self.did,
80
+ "smarthome": {
81
+ "jd": {
82
+ "newSession": "true",
83
+ "sessionId": "123456789",
84
+ "userId": "9adbd42d-618f-4752-ad6d-9cb382079e25"
85
+ }
86
+ },
87
+ "train": {
88
+ "pageNo": 1,
89
+ "pageSize": 20
90
+ }
91
+ },
92
+ "tts_params": {
93
+ "bit_depth": "16",
94
+ "channels": "1",
95
+ "encoding": "speex-wb",
96
+ "frame_size": "0",
97
+ "sample_rate": "16000"
98
+ }
99
+ },
100
+ "aue": "speex-wb",
101
+ "bit_depth": "16",
102
+ "channels": "1",
103
+ "city_pd": "",
104
+ "client_ip": "112.132.223.243",
105
+ "dtype": "text",
106
+ "frame_size": "0",
107
+ "msc.lat": "31.837463",
108
+ "msc.lng": "117.17",
109
+ "pers_param": self.pers_param,
110
+ "sample_rate": "16000",
111
+ "scene": self.scene,
112
+ "stmid": "0",
113
+ "uid": self.uid,
114
+ "debug": self.debug,
115
+ "debugx": self.debugx,
116
+ "category": self.category
117
+ },
118
+ "UserParams": encode_base64(self.UserParams),
119
+ "UserData": quote_plus(self.UserData)
120
+ }
121
+ }
122
+ },
123
+ "payload": {
124
+ "text": {
125
+ "compress": "raw",
126
+ "encoding": "utf8",
127
+ "format": "plain",
128
+ "plainText": self.plainText,
129
+ "status": 3
130
+ }
131
+ }
132
+ }
133
+ async def handle_v1_chain(self, ws):
134
+ """
135
+ 处理5.0链路逻辑:
136
+ - 根据接收到的消息执行不同的操作
137
+ - 当'action'为'started'时,读取文件并发送数据块
138
+ - 发送结束标识
139
+ - 当'action'为'result'且'sub'为'stream_tpp'时,返回内容
140
+ """
141
+ while True:
142
+ _msg = await ws.recv()
143
+ # print(_msg)
144
+ try:
145
+ msg = json.loads(_msg)
146
+ code = safe_get(msg, ["header","code"])
147
+ if code != 0:
148
+ logging.error(f'请求错误: {code}, {msg}')
149
+ break
150
+ else:
151
+ answer = safe_get(msg, ["payload","results","text","intent","answer"])
152
+ answerText = safe_get(answer, ["text"])
153
+ if answerText:
154
+ self.answer_text += answerText
155
+ if msg['header']['status']=="2" or msg['header']['status']=="3": # 返回结果接收完成
156
+ if self.answer_text:
157
+ answer["text"] = self.answer_text
158
+ self.response = msg
159
+ return
160
+
161
+ except Exception as e:
162
+ logging.error(f"error in handle_v1_chain :{e}")
163
+ break
164
+
165
+
caseScript/Gateway.py ADDED
@@ -0,0 +1,264 @@
1
+
2
+ from wsgiref.handlers import format_date_time
3
+ import asyncio
4
+ from time import mktime
5
+ import hashlib
6
+ import hmac,time
7
+ from urllib.parse import urlencode, urlparse
8
+ import uuid
9
+ from common.utils import *
10
+ from common.WSBaseApi import WSBaseApi
11
+ from pathlib import Path
12
+
13
+ class ApiTestRunner(WSBaseApi):
14
+ def __init__(self, **kwargs):
15
+ super().__init__(**kwargs)
16
+ self.text_path = Path.cwd().resolve().joinpath("data/audio",f"{self.text}.pcm")
17
+ def generate_v2_auth_headers(self):
18
+ """为v2 5.4链路生成授权头"""
19
+ url_host = urlparse(self.url).netloc
20
+ date = format_date_time(mktime(datetime.now().timetuple()))
21
+ authorization_headers = f"host: {url_host}\ndate: {date}\nGET /v2/autoCar HTTP/1.1"
22
+ signature_sha = hmac.new(self.apiSecret.encode('utf-8'), authorization_headers.encode('utf-8'),
23
+ digestmod=hashlib.sha256).digest()
24
+ authorization_signature = encode_base64(signature_sha,input_encoding='bytes')
25
+ authorization = f'api_key="{self.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="{authorization_signature}"'
26
+ return {
27
+ "host": url_host,
28
+ "date": date,
29
+ "authorization": encode_base64(authorization),
30
+ "appid": self.appId
31
+ }
32
+ def generate_v1_auth_headers(self):
33
+
34
+ """为5.0链路生成参数"""
35
+ param_iat = self.build_params()
36
+ cur_time = int(time.time())
37
+ param = json.dumps(param_iat, ensure_ascii=False)
38
+ param_base64 = encode_base64(param)
39
+ check_sum_pre = self.apiKey + str(cur_time) + param_base64
40
+ checksum = hashlib.md5(check_sum_pre.encode("utf-8")).hexdigest()
41
+ return {
42
+ "appid": self.appId,
43
+ "checksum": checksum,
44
+ "param": param_base64,
45
+ "curtime": str(cur_time),
46
+ "signtype": "md5"
47
+ }
48
+ def assemble_ws_auth_url(self, chain=None):
49
+ if chain:
50
+ # v2 5.4链路
51
+ params = self.generate_v2_auth_headers()
52
+ else:
53
+ # 默认5.0链路
54
+ params = self.generate_v1_auth_headers()
55
+
56
+ return f"{self.url}?{urlencode(params)}"
57
+ # @exception_decorator
58
+ async def handle_v2_chain(self, ws):
59
+ """
60
+ 处理v2 5.4链路逻辑:
61
+ - 接收消息并解析
62
+ - 检查返回码是否为0(成功)
63
+ - 如果状态为1,则解码并返回特定的文本内容
64
+ - 如果状态为2,则结束循环
65
+ """
66
+ while True:
67
+ try:
68
+ msg = await ws.recv()
69
+ # print(msg)
70
+ response_data = json.loads(msg)
71
+ # code 返回码,0表示成功,其它表示异常
72
+ if response_data["header"]["code"] == 0:
73
+ # status 整个结果的状态,0-会话起始结果,1-中间结果,2-最终结果
74
+ if "status" in response_data["header"]:
75
+ status = response_data["header"]["status"]
76
+ if status == 1 and "cbm_semantic" in response_data["payload"]:
77
+ semantic_bs64 = response_data["payload"]["cbm_semantic"]["text"]
78
+ semantic_str = base64.b64decode(semantic_bs64.encode('utf-8')).decode('utf-8')
79
+ self.response = json.loads(semantic_str)
80
+ return
81
+ elif status == 2:
82
+ break
83
+ else:
84
+ logging.error(f"返回结果错误:{response_data['header']['message']}")
85
+ break
86
+ except Exception as e:
87
+ logging.error(f"Error in processing message: {e}")
88
+ break
89
+ # @exception_decorator
90
+ async def handle_v1_chain(self, ws):
91
+ """
92
+ 处理5.0链路逻辑:
93
+ - 根据接收到的消息执行不同的操作
94
+ - 当'action'为'started'时,读取文件并发送数据块
95
+ - 发送结束标识
96
+ - 当'action'为'result'且'sub'为'stream_tpp'时,返回内容
97
+ """
98
+
99
+ while True:
100
+ _msg = await ws.recv()
101
+ # print(_msg)
102
+ try:
103
+ msg = json.loads(_msg)
104
+ if msg['action'] == "started":
105
+ with open(self.text_path, 'rb') as file:
106
+ for chunk in iter(lambda: file.read(1280), b''):
107
+ await ws.send(chunk)
108
+ await asyncio.sleep(0.04) # 使用asyncio.sleep避免阻塞事件循环
109
+ await ws.send("--end--".encode("utf-8"))
110
+ elif msg['action'] == "result":
111
+ data = msg['data']
112
+ if data.get('sub') == "stream_tpp" or data.get('sub') == "tpp":
113
+ self.response = json.loads(data['content'])
114
+ return
115
+ except Exception as e:
116
+ logging.error(f"error in handle_v1_chain :{e}")
117
+ break
118
+
119
+ def build_params(self,chain=None):
120
+ if chain: #5.4链路参数
121
+ _textParams = {
122
+ "sparkEnv": self.sparkEnv,
123
+ "userId": self.userId,
124
+ "user_data": "",
125
+ "attachparams": str({"nlp_params": {"vWtoken": self.token, "aiui45_intv_mode": 2, "aqua_route": "vwa"}}), # 不能传递字典,需要传str
126
+ "scene": self.scene,
127
+ "debugx": "true",
128
+ "debug": "true"
129
+ }
130
+ # # 序列化 textParams
131
+ textParams = json.dumps(_textParams, ensure_ascii=False)
132
+ def get_audio():
133
+ with open(self.text_path, "rb") as file:
134
+ content = file.read()
135
+ return encode_base64(content,input_encoding='bytes')
136
+
137
+
138
+ return {
139
+ "header": {
140
+ "app_id": self.appId,
141
+ "uid": "efeafe5e-82d6-4922-9770-f7aaabf97548",
142
+ "stmid": "sid1111",
143
+ "status": 3,
144
+ "scene": self.scene
145
+ },
146
+ "parameter": {
147
+ "nlp": {
148
+ "sub_scene": "cbm_v47",
149
+ "new_session": False,
150
+ "nlp": {
151
+ "encoding": "utf8",
152
+ "compress": "raw",
153
+ "format": "json"
154
+ }
155
+ }
156
+ },
157
+ "payload": {
158
+ "cbm_semantic": {
159
+ "compress": "raw",
160
+ "format": "plain",
161
+ "text": encode_base64(textParams),
162
+ "encoding": "utf8",
163
+ "status": 3
164
+ },
165
+ "audio": {
166
+ "audio": get_audio(),
167
+ "status": 2,
168
+ "encoding": "raw",
169
+ "sample_rate": 16000,
170
+ "channels": 1,
171
+ "bit_depth": 16,
172
+ "frame_size": 0
173
+ }
174
+ }
175
+ }
176
+ else: #5.0链路参数
177
+ def get_auth_id():
178
+ """
179
+ 生成基于系统MAC地址的唯一身份验证ID。
180
+
181
+ 返回:
182
+ str: 唯一的身份验证ID。
183
+ """
184
+ # 获取系统MAC地址的整数表示,并转换为12位的十六进制字符串
185
+ mac = uuid.UUID(int=uuid.getnode()).hex[-12:]
186
+
187
+ # 将MAC地址按照标准格式(使用冒号分隔)重新组合
188
+ formatted_mac = ":".join([mac[e:e + 2] for e in range(0, 11, 2)])
189
+
190
+ # 对格式化后的MAC地址进行MD5哈希处理,并返回其十六进制表示
191
+ auth_id = hashlib.md5(formatted_mac.encode("utf-8")).hexdigest()
192
+ return auth_id
193
+ trs_params = {}
194
+ iat_params = {
195
+ "accent": "mandarin",
196
+ "language": self.language,
197
+ "domain": "aiui-automotiveknife",
198
+ "eos": "600",
199
+ "evl": "0",
200
+ "isFar": "0",
201
+ "svl": "50",
202
+ "vgap": "400"
203
+ }
204
+
205
+ nlp_params = {
206
+ "devid": "LG8-LGA26.08.2420010196",
207
+ "city": "合肥市",
208
+ "user_defined_params": {},
209
+ "weather_airquality": "true",
210
+ "deviceId": "LG8-LGA26.08.2420010196",
211
+ "userId": "69b85a13-1434-408b-872f-1632c587dbc4",
212
+ "asp_did": "LG8-LGA26.08.2420010196",
213
+ "vWtoken": self.token,
214
+ "car_identity": "MP24",
215
+ "theme": "standard",
216
+ "vin": "HVWJA1ER5R1203864",
217
+ "interactive_mode": "fullDuplex",
218
+ "did": "VW_HU_ICAS3_LG8-LGA26.08.2420010196_v2.0.1_v0.0.1"
219
+ }
220
+ attach_params = {
221
+ "trs_params": json.dumps(trs_params, ensure_ascii=False),
222
+ "iat_params": json.dumps(iat_params, ensure_ascii=False),
223
+ "nlp_params": json.dumps(nlp_params, ensure_ascii=False)
224
+ }
225
+ return {
226
+ "auth_id": get_auth_id(),
227
+ "ver_type": "websocket",
228
+ "data_type": "audio",
229
+ "scene": "main",
230
+ "lat": "31.704187",
231
+ "lng": "117.239833",
232
+ "attach_params": json.dumps(attach_params, ensure_ascii=False),
233
+ "userparams": "eyJjbGVhbl9oaXN0b3J5Ijoib2ZmIiwic2tpcCI6Im5vdF9za2lwIn0=",
234
+ "interact_mode": "continuous",
235
+ "text_query": "tpp",
236
+ "sample_rate": "16000",
237
+ "aue": "raw",
238
+ "speex_size": "60",
239
+ "dwa": "wpgs",
240
+ "result_level": "complete",
241
+ "debugx": "True",
242
+ "debug": "true",
243
+ "close_delay": "100"
244
+ }
245
+
246
+ @exception_decorator
247
+ def run(self):
248
+ extra_headers = {'Authorization': self.token}
249
+ # 使用split函数分割URL,并找到包含"v2"的部分
250
+ parts = self.url.split('/')
251
+ # "v2"正好位于倒数第二个位置, v2走5.4链路,v1走5.0链路
252
+ version = "v2" if len(parts) > 2 and parts[-2] == 'v2' else None
253
+ # 根据版本选择正确的assemble_ws_auth_url方法参数
254
+ self.url = self.assemble_ws_auth_url(version)
255
+ # 构建frame,对于v2版本添加额外参数
256
+ if version:
257
+ self.request = self.build_params(version)
258
+ # WebSocket连接与消息发送
259
+ asyncio.run(self.WebSocketApi(
260
+ extra_headers,
261
+ version
262
+ ))
263
+
264
+
caseScript/__init__.py ADDED
File without changes
common/Assertion.py ADDED
@@ -0,0 +1,105 @@
1
+
2
+ from datetime import datetime, timedelta,date
3
+ from common.assertUtils import *
4
+ import pytest
5
+
6
+
7
+ def iAssert(response, keys_values_str):
8
+ """
9
+ 断言JSON响应中的键值对是否符合预期。
10
+
11
+ :param response: JSON响应作为字典。
12
+ :param keys_values_str: 键值对字符串,用逗号或顿号分隔。
13
+ :return: 包含断言结果的字典。
14
+ """
15
+ # 处理输入字符串,提取键和值。
16
+ key_value_map = get_key_value(keys_values_str, delimiter='\n', pair_delimiter='=',opt="not in")
17
+
18
+ for key,value in key_value_map.items():
19
+ expected_value = value
20
+ actual_value = str(get_value(response, key))
21
+ msg = f"【{key}】 expected【{expected_value}】, but got 【{actual_value}】"
22
+ if expected_value == '1no1' or expected_value is None:
23
+ # 对于期望值为 '1no1' 或 None 的情况,仅检查键是否存在。
24
+ assert has_key(response, key), f"key:{key} is not exist, check path!"
25
+ else:
26
+ # 需要校验值的情况。
27
+ if expected_value.startswith('*') and expected_value.endswith('*'):
28
+ assert re.search(expected_value[1:-1], actual_value), msg
29
+ elif expected_value.endswith('*'):
30
+ assert re.match(expected_value[:-1], actual_value), msg
31
+ elif expected_value.startswith('*'):
32
+ assert re.search(expected_value[1:], actual_value), msg
33
+ elif isinstance(expected_value, dict):
34
+ assert actual_value != expected_value, msg
35
+ elif expected_value.startswith('eval_'):
36
+ except_value = expected_value[5:]
37
+ assert str(eval(except_value)) == actual_value, msg
38
+ elif expected_value.startswith('in('):
39
+ # 处理以 in 开头的情况。or:a in(1||2||3)
40
+ expected_value_list = expected_value[3:-1].split('||')
41
+ assert actual_value in expected_value_list, msg
42
+ elif expected_value.startswith('language_'):
43
+ # 检测文本 language
44
+ assert check_text_language(actual_value) == expected_value[9:], msg
45
+ else:
46
+ assert actual_value == expected_value, msg
47
+
48
+
49
+ def urlAssert(req, res, query_info):
50
+ """
51
+ 比较请求和响应参数。
52
+ :param req: 请求数据
53
+ :param res: 响应数据
54
+ :param query_info: 查询信息字符串
55
+ :return: 响应数据对象
56
+ """
57
+ query_info_dic = get_key_value(query_info, delimiter='\n', pair_delimiter=':')
58
+ url_path = query_info_dic["queryPath"]
59
+ res_url = get_Urlparam_dic(get_value(res, url_path), json_fields = ['pers_param', 'user_defined_params'])
60
+ req_res_keys_str = query_info_dic["queryCondit"]
61
+ req_res_keys = get_key_value(req_res_keys_str)
62
+ for req_key,res_key in req_res_keys.items():
63
+ req_val = get_value(req,req_key.strip(), decode_funcs={"parameter.custom.custom_data.UserParams": decode_base64,"parameter.custom.custom_data.UserData": url_decode})
64
+ res_val = get_value(res_url,res_key.strip())
65
+ assert req_val == res_val, f"【请求参数: {req_key}】与【响应参数: {res_key}】 的值对比不一致,\n请求值为: 【{req_val}】\n响应值为:【{res_val}】"
66
+ return res_url
67
+
68
+ def arrayAssert(response_json, query_info):
69
+ """
70
+ path对应数据是数组, 根据多个条件查找数组中匹配的元素。
71
+ """
72
+ query_info_dic = get_key_value(query_info, delimiter='\n', pair_delimiter=':')
73
+ query_condit = query_info_dic["queryCondit"]
74
+ query_path = query_info_dic["queryPath"]
75
+
76
+ # 解析 查询条件,将其转换为字典形式
77
+ condit_dit = get_key_value(query_condit, delimiter=',', pair_delimiter='=')
78
+
79
+ try:
80
+ # 获取返回消息中path对应的JSON 数据
81
+ path_json = get_value(response_json, query_path)
82
+ except KeyError as e:
83
+ raise ValueError(f'{e}')
84
+
85
+ # 查找所有匹配项
86
+ index_list = list(find_matching_index(path_json, condit_dit))
87
+
88
+ if not index_list:
89
+ raise ValueError(f'未找到符合条件的元素:【{query_condit}】')
90
+ else:
91
+ # 假设我们只关心第一个匹配项
92
+ return path_json[int(index_list[0])]
93
+
94
+ def Assert(request,response, case_suite):
95
+ """执行断言"""
96
+ if case_suite["UniversalAssert"]: #通用断言,key=value
97
+ iAssert(response, case_suite['UniversalAssert'])
98
+
99
+ if case_suite["ArrayAssert"]: #数组断言,value=[{...},{...},{...},...],给出条件,找到匹配的{...},再在{...}中执行通用断言 key=value
100
+ iAssert(arrayAssert(response, case_suite['ArrayAssert']), case_suite['ArrayAssert'])
101
+
102
+ if case_suite["URLAssert"]: #url查询参数断言,某些resquest key(a.b.c=xx)会传给res url,作为res_url的查询参数(http://...?c=xx),断言value(a.b.c)=value(cc)
103
+ _res = urlAssert(request, response, case_suite['URLAssert'])
104
+ if get_key_value(case_suite['URLAssert'], delimiter='\n', pair_delimiter='=',opt="not in"):
105
+ iAssert(_res, case_suite['URLAssert'])
common/WSBaseApi.py ADDED
@@ -0,0 +1,109 @@
1
+ import websockets
2
+ from functools import cached_property
3
+ import asyncio
4
+ from common.utils import *
5
+ import allure
6
+ from pathlib import Path
7
+ from common.Assertion import Assert
8
+ import pytest
9
+ import traceback
10
+ class WSBaseApi:
11
+ def __init__(self, **kwargs):
12
+ self.request = {}
13
+ self.response = {}
14
+ for key, value in kwargs.items():
15
+ setattr(self, key, value)
16
+ async def WebSocketApi(self, extra_headers=None, chain=None):
17
+ """
18
+ 建立WebSocket连接,根据提供的参数发送初始帧,并根据链路版本处理后续消息
19
+ - 如果chain为v2,则调用handle_v2_chain处理消息
20
+ - 否则,默认调用handle_v1_chain处理消息
21
+ """
22
+ async with websockets.connect(self.url, extra_headers=extra_headers) as ws:
23
+ await ws.send(json.dumps(self.request, ensure_ascii=False))
24
+ await self.handle_v2_chain(ws) if chain == "v2" else await self.handle_v1_chain(ws)
25
+ @exception_decorator
26
+ def run(self):
27
+ self.request = self.build_params()
28
+ # WebSocket连接与消息发送
29
+ asyncio.run(self.WebSocketApi())
30
+
31
+
32
+
33
+ class BaseApiTest:
34
+ # 定义需要子类实现的属性
35
+ TEST_TYPE = "API自动化测试" # 默认值,可被子类覆盖
36
+ CASE_PATH = Path.cwd().resolve().joinpath("data/case_data.xlsx")
37
+
38
+ @cached_property
39
+ def API_TEST_RUNNER_CLASS(self):
40
+ """动态加载对应的测试运行器类(自动缓存)"""
41
+ class_prefix = self.__class__.__name__[4:] # TestGateway -> Gateway
42
+ module_path = f"caseScript.{class_prefix}"
43
+
44
+ try:
45
+ module = __import__(module_path, fromlist=['ApiTestRunner'])
46
+ return module.ApiTestRunner
47
+ except ImportError as e:
48
+ raise ImportError(
49
+ f"无法加载 {module_path}.ApiTestRunner,"
50
+ "请检查模块路径和类命名是否符合规范"
51
+ ) from e
52
+
53
+ @pytest.mark.parametrize('case_suite', gen_case_suite(CASE_PATH))
54
+ def test_api(self, case_suite, setup_env):
55
+ """测试用例执行模板"""
56
+ try:
57
+ # 1. 合并参数
58
+ params = merge_dicts(case_suite, setup_env)
59
+
60
+ # 2. 执行测试
61
+ runner = self.API_TEST_RUNNER_CLASS(**params)
62
+ runner.run()
63
+
64
+ # 3. 记录报告
65
+ self._record_allure_report(runner)
66
+
67
+ # 4. 执行断言
68
+ self._execute_assertions(runner, case_suite)
69
+
70
+ except Exception as e:
71
+ self._record_error(e)
72
+ raise
73
+
74
+ def _record_allure_report(self, runner):
75
+ """记录Allure测试报告"""
76
+ allure.dynamic.epic(
77
+ f"【{runner.env_name}】"
78
+ f"【{runner.project}】"
79
+ f"【{runner.service}】"
80
+ f"{self.TEST_TYPE}"
81
+ )
82
+ allure.dynamic.story(runner.appId)
83
+ allure.attach(runner.url, 'URL', allure.attachment_type.URI_LIST)
84
+ allure.attach(
85
+ f"{convert_to_json(runner.request)}",
86
+ 'API请求',
87
+ allure.attachment_type.JSON
88
+ )
89
+ allure.attach(
90
+ f"{convert_to_json(runner.response)}",
91
+ 'API响应',
92
+ allure.attachment_type.JSON
93
+ )
94
+ def _execute_assertions(self, runner, case_suite):
95
+ """执行断言逻辑"""
96
+ with allure.step(f'【断言】{runner.number}_{runner.casename}'):
97
+ Assert(runner.request, runner.response, case_suite)
98
+ def _record_error(self, error):
99
+ """记录测试错误信息"""
100
+ allure.attach(
101
+ str(error),
102
+ '异常信息',
103
+ allure.attachment_type.TEXT
104
+ )
105
+ allure.attach(
106
+ traceback.format_exc(),
107
+ '异常堆栈',
108
+ allure.attachment_type.TEXT
109
+ )
common/WebSocketApi.py ADDED
@@ -0,0 +1,73 @@
1
+ import _thread as thread
2
+ import logging
3
+ import websocket # 使用websocket_client
4
+ from common.utils import *
5
+
6
+ class WebSocketApi:
7
+ def __init__(self, url, request):
8
+ self.url = url
9
+ self.request = request
10
+ self.answer_text = ""
11
+ self.errcode = ""
12
+ self.response = {}
13
+ self.ws = None
14
+
15
+ def on_error(self, ws, error):
16
+ """处理WebSocket错误"""
17
+ logging.error(f"### error: {error}")
18
+ self.errcode = str(error)
19
+ self.ws.close()
20
+
21
+ def on_close(self, ws, close_status_code, close_msg):
22
+ """处理WebSocket关闭"""
23
+ logging.info(f"### ws closed with status {close_status_code} and message {close_msg}")
24
+
25
+ def on_open(self, ws):
26
+ """处理WebSocket连接建立"""
27
+ thread.start_new_thread(self.run, (ws,))
28
+
29
+ def run(self, ws, *args):
30
+ """发送请求"""
31
+ ws.send(self.request)
32
+
33
+ def on_message(self, ws, message: str):
34
+ """处理WebSocket消息"""
35
+ try:
36
+ msg = json.loads(message)
37
+ code = safe_get(msg, ["header","code"])
38
+ if code != 0:
39
+ logging.error(f'请求错误: {code}, {msg}')
40
+ err_msg = safe_get(msg, ["header","message"])
41
+ self.errcode = f"{code}, error msg: {err_msg}"
42
+ self.ws.close()
43
+ else:
44
+ answer = safe_get(msg, ["payload","results","text","intent","answer"])
45
+ answerText = safe_get(answer, ["text"])
46
+ if answerText:
47
+ self.answer_text += answerText
48
+ if msg['header']['status']=="2" or msg['header']['status']=="3": # 返回结果接收完成
49
+ if self.answer_text:
50
+ answer["text"] = self.answer_text
51
+ self.response = msg
52
+ self.ws.close()
53
+ except json.JSONDecodeError as e:
54
+ logging.error(f"JSON解码错误: {e}")
55
+ self.errcode = f"JSON解码错误: {e}"
56
+ self.ws.close()
57
+
58
+ def start(self):
59
+ """启动WebSocket客户端"""
60
+ websocket.enableTrace(False)
61
+ self.ws = websocket.WebSocketApp(self.url,
62
+ on_open=self.on_open,
63
+ on_error=self.on_error,
64
+ on_message=self.on_message,
65
+ on_close=self.on_close)
66
+ self.ws.run_forever()
67
+
68
+
69
+ def main(Spark_url, request: str):
70
+ """主函数,启动WebSocket客户端并返回响应"""
71
+ client = WebSocketApi(Spark_url, request)
72
+ client.start()
73
+ return client.response
common/__init__.py ADDED
File without changes
common/assertUtils.py ADDED
@@ -0,0 +1,132 @@
1
+ import re
2
+ from common.utils import *
3
+ from typing import Literal
4
+
5
+ def get_value(data, path, decode_funcs=None):
6
+ """
7
+ 获取嵌套字典中的值。
8
+
9
+ :param data: 嵌套字典
10
+ :param path: 字典路径,用点号分隔
11
+ :param decode_funcs: 一个字典,键是路径,值是解码函数
12
+ :return: 路径对应的值, 如果路径不存在则返回None
13
+ """
14
+ keys = path.split('.')
15
+ current = data
16
+ for key in keys:
17
+ if isinstance(current, dict) and key in current:
18
+ current = current[key]
19
+ else:
20
+ return None
21
+ # 特殊处理某些路径的值
22
+ if decode_funcs and path in decode_funcs:
23
+ return decode_funcs[path](current)
24
+ else:
25
+ return current
26
+
27
+ def get_key_value(input_str, delimiter=',,', pair_delimiter='=',opt: Literal["in", "not in"]="in"):
28
+ """
29
+ 解析键值对字符串,返回指定数据格式。
30
+
31
+ :param input_str: 键值对字符串
32
+ :param delimiter: 键值对之间的分隔符,默认为逗号
33
+ :param pair_delimiter: 键和值之间的分隔符,默认为等号
34
+ :return: 键和值
35
+ """
36
+ def should_include(part):
37
+ """根据 opt 判断是否包含该部分"""
38
+ if opt == "in":
39
+ return pair_delimiter in part
40
+ elif opt == "not in":
41
+ return ":" not in part
42
+ else:
43
+ raise ValueError(f"Invalid operation: {opt}")
44
+
45
+ # 解析字符串并根据 opt 进行过滤
46
+ key_value_map = {part.split(pair_delimiter)[0].strip():part.strip().split(pair_delimiter)[1].strip() for part in re.split(rf'[{delimiter}]', input_str) if should_include(part)}
47
+ return key_value_map
48
+
49
+
50
+ def get_Urlparam_dic(urlstr, json_fields=None):
51
+ """
52
+ 将URL查询字符串解析为字典,并处理特定字段的JSON解码。
53
+
54
+ :param urlstr: 完整的URL字符串
55
+ :param json_fields: 需要进行JSON解码的字段列表
56
+ :return: 解析后的字典
57
+ """
58
+ if json_fields is None:
59
+ json_fields = []
60
+ # 如果URL中没有查询字符串,直接返回空字典
61
+ if '?' not in urlstr:
62
+ return {}
63
+
64
+ # 分离URL和查询字符串
65
+ _, _params = urlstr.split('?', 1)
66
+
67
+ # 解析查询字符串为键值对
68
+ params = urllib.parse.parse_qs(_params)
69
+ # 解码值并构建字典,values[0]取第一个值(假设每个键只有一个值)
70
+ obj = {key: url_decode(values[0]) if values else None for key, values in params.items()}
71
+ def decode_json_field(field, value):
72
+ """
73
+ 尝试将字段值解析为JSON对象。
74
+
75
+ :param field: 字段名
76
+ :param value: 字段值
77
+ :return: 解析后的JSON对象或原始值
78
+ """
79
+ if value is None:
80
+ return None
81
+
82
+ try:
83
+ return json.loads(value)
84
+ except json.JSONDecodeError as e:
85
+ print(f"JSON解码错误: {e}")
86
+ print(f"字段 '{field}' 的值为: {value}")
87
+ return None
88
+ # 解析特定字段为JSON对象
89
+ for field in json_fields:
90
+ if field in obj:
91
+ obj[field] = decode_json_field(field, obj[field])
92
+
93
+ return obj
94
+
95
+
96
+ def has_key(obj, key_path):
97
+ """
98
+ 检查一个对象中是否存在指定的键路径。
99
+
100
+ :param obj: 要检查的对象。
101
+ :param key_path: 键路径,用点号分隔。
102
+ :return: 如果路径存在则返回True, 否则返回False。
103
+ """
104
+ current_obj = obj
105
+ keys = key_path.split('.')
106
+ for key in keys:
107
+ if key in current_obj:
108
+ current_obj = current_obj[key]
109
+ else:
110
+ return False
111
+ return True
112
+
113
+
114
+ def find_matching_index(data, conditions, current_path=""):
115
+ """
116
+ 递归查找匹配所有条件的数据index。
117
+ 只有当项是字典或列表时才进一步递归
118
+ """
119
+ if isinstance(data, list):
120
+ for index, item in enumerate(data):
121
+ if isinstance(item, (dict, list)):
122
+ new_path = f"{current_path}[{index}]"
123
+ yield from find_matching_index(item, conditions, new_path)
124
+ elif isinstance(data, dict):
125
+ if all([str(get_value(data,key)) == value for key, value in conditions.items()]):
126
+ index = re.search(r'\[(\d+)\]', current_path).group(1)
127
+ yield index
128
+ else:
129
+ for key, value in data.items():
130
+ new_path = f"{current_path}.{key}" if current_path else key
131
+ if isinstance(value, (dict, list)):
132
+ yield from find_matching_index(value, conditions, new_path)
common/logger.py ADDED
@@ -0,0 +1,27 @@
1
+ import os
2
+ import logging
3
+ from datetime import datetime
4
+
5
+ def configure_logging():
6
+ # 指定日志文件夹路径
7
+ log_dir = 'logs'
8
+
9
+ # 创建日志文件夹(如果它不存在)
10
+ os.makedirs(log_dir, exist_ok=True)
11
+
12
+ # 获取今天的日期,用于日志文件名
13
+ today_date = datetime.now().date()
14
+
15
+ # 配置日志记录
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format='%(asctime)s.%(msecs)03d - %(levelname)s - %(name)s - %(message)s',
19
+ datefmt='%H:%M:%S',
20
+ handlers=[
21
+ logging.StreamHandler(), # 输出到控制台
22
+ logging.FileHandler(f'{log_dir}/{today_date}.log') # 输出到文件
23
+ ]
24
+ )
25
+
26
+ # 调用函数以配置日志记录
27
+ configure_logging()
common/utils.py ADDED
@@ -0,0 +1,244 @@
1
+ import base64, json,os
2
+ import pandas as pd
3
+ import inspect,urllib
4
+ import random
5
+ import string
6
+ from datetime import datetime
7
+ import email.utils
8
+ from functools import wraps
9
+ from common.logger import *
10
+ from typing import Optional, Union, Literal
11
+ import numpy as np
12
+ def get_rfc1123_time():
13
+ # 获取当前UTC时间
14
+ now = datetime.utcnow()
15
+ # 将时间格式化为RFC1123格式
16
+ rfc1123_time = email.utils.format_datetime(now)
17
+ return rfc1123_time
18
+
19
+ def generate_random_string(length=32):
20
+ """
21
+ 生成指定长度的随机字符串,该字符串可以从包含小写字母、数字以及'@'符号的字符集中随机选择。
22
+
23
+ Args:
24
+ length (int): 生成字符串的长度,默认为32。
25
+
26
+ Returns:
27
+ str: 生成的随机字符串。
28
+ """
29
+ if length < 1:
30
+ raise ValueError("Length must be at least 1.")
31
+
32
+ # 定义字符池:包含小写字母、数字和 '@' 符号
33
+ chars = string.ascii_lowercase + string.digits + "@"
34
+
35
+ # 从字符池中随机选择指定数量的字符
36
+ random_chars = random.choices(chars, k=length)
37
+
38
+ return ''.join(random_chars)
39
+
40
+ def decode_unicode_escape(data):
41
+ """
42
+ 递归地遍历字典或列表,将所有字符串值中的Unicode转义序列解码为原始字符。
43
+
44
+ Args:
45
+ data (dict or list): 包含Unicode转义序列的JSON对象(字典或列表)。
46
+
47
+ Returns:
48
+ dict or list: 解码后的JSON对象,其中所有字符串值已被正确解码。
49
+ """
50
+ if isinstance(data, dict):
51
+ return {key: decode_unicode_escape(value) for key, value in data.items()}
52
+ elif isinstance(data, list):
53
+ return [decode_unicode_escape(element) for element in data]
54
+ elif isinstance(data, str):
55
+ try:
56
+ # 使用 json.loads 来解析包含转义字符的字符串
57
+ return json.loads(f'"{data}"')
58
+ except json.JSONDecodeError:
59
+ # 如果字符串不是有效的JSON字符串,则直接返回原字符串
60
+ return data
61
+ else:
62
+ # 对于其他类型的数据,直接返回它们
63
+ return data
64
+
65
+ def convert_to_json(data):
66
+ """
67
+ 将包含Unicode转义序列的JSON数据解码,并返回JSON格式的字符串。
68
+
69
+ Args:
70
+ data (dict or list): 包含Unicode转义序列的JSON对象(字典或列表)。
71
+
72
+ Returns:
73
+ str: 解码并格式化后的JSON字符串。
74
+ """
75
+ decoded_data = decode_unicode_escape(data)
76
+ return json.dumps(decoded_data, ensure_ascii=False, indent=4)
77
+
78
+ def encode_base64(
79
+ s: Optional[Union[str, bytes]],
80
+ *,
81
+ input_encoding: Literal['auto', 'str', 'bytes'] = 'auto',
82
+ output_encoding: str = 'utf-8'
83
+ ) -> Optional[str]:
84
+ """将字符串或字节数据编码为 Base64 格式
85
+
86
+ Args:
87
+ s: 要编码的输入数据,可以是字符串或字节,None 直接返回 None
88
+ input_encoding: 输入处理模式:
89
+ 'auto' - 自动检测类型(默认)
90
+ 'str' - 强制作为字符串处理
91
+ 'bytes' - 强制作为字节处理
92
+ output_encoding: 输出结果的编码格式(默认utf-8)
93
+
94
+ Returns:
95
+ Base64 编码后的字符串,如果输入为 None 则返回 None
96
+
97
+ Raises:
98
+ TypeError: 输入类型不符合要求
99
+ ValueError: 编码失败或模式参数无效
100
+ """
101
+ if s is None:
102
+ return None
103
+
104
+ # 输入类型处理
105
+ try:
106
+ if input_encoding == 'str':
107
+ data = s.encode('utf-8') if isinstance(s, str) else s
108
+ elif input_encoding == 'bytes':
109
+ data = s if isinstance(s, bytes) else str(s).encode('utf-8')
110
+ elif input_encoding == 'auto':
111
+ data = s.encode('utf-8') if isinstance(s, str) else s
112
+ else:
113
+ raise ValueError(f"Invalid input_encoding: {input_encoding}")
114
+ except (AttributeError, UnicodeError) as e:
115
+ raise ValueError(f"Input encoding failed: {str(e)}") from e
116
+
117
+ if not isinstance(data, bytes):
118
+ raise TypeError(f"Expected bytes or string, got {type(s).__name__}")
119
+
120
+ # Base64 编码
121
+ try:
122
+ return base64.b64encode(data).decode(output_encoding)
123
+ except UnicodeError as e:
124
+ raise ValueError(f"Output decoding failed: {str(e)}") from e
125
+ def decode_base64(s):
126
+ """将 Base64 编码的字符串解码回原始字符串"""
127
+ return base64.b64decode(s.encode('utf-8')).decode('utf-8') if s is not None else None
128
+
129
+ def url_decode(str_):
130
+ """
131
+ 解码URL编码的字符串。
132
+
133
+ :param str_: URL编码的字符串
134
+ :return: 解码后的字符串
135
+ """
136
+ return urllib.parse.unquote_plus(str_) if str_ else None
137
+
138
+ def check_text_language(text):
139
+ """
140
+ 检查文本是纯中文、纯英文还是中文加英文。
141
+
142
+ :param text: 需要检查的文本
143
+ :return: 文本的语言类型 ('纯中文', '纯英文', '中文+英文', '其他')
144
+ """
145
+ has_chinese = any('\u4e00' <= char <= '\u9fff' for char in text)
146
+ has_english = any('a' <= char <= 'z' or 'A' <= char <= 'Z' for char in text)
147
+
148
+ if has_chinese and has_english:
149
+ return "中文+英文"
150
+ elif has_chinese:
151
+ return "纯中文"
152
+ elif has_english:
153
+ return "纯英文"
154
+ else:
155
+ return "其他"
156
+
157
+ def safe_get(data, keys, default=""):
158
+ """ 安全地从嵌套字典中获取值 """
159
+ for key in keys:
160
+ try:
161
+ data = data[key]
162
+ except (KeyError, TypeError):
163
+ return default
164
+ return data
165
+
166
+ def append_msg_to_excel(file_path, **kwargs):
167
+ try:
168
+ # 检查文件是否存在
169
+ if not os.path.exists(file_path):
170
+ # 如果文件不存在,创建一个新的 DataFrame 并保存为 Excel 文件
171
+ existing_df = pd.DataFrame()
172
+ else:
173
+ # 读取现有数据
174
+ existing_df = pd.read_excel(file_path)
175
+
176
+ # 动态构建 new_data 字典
177
+ new_data = {**kwargs}
178
+
179
+ # 将新数据转换为 DataFrame
180
+ new_df = pd.DataFrame([new_data])
181
+
182
+ # 追加新数据到现有数据
183
+ updated_df = pd.concat([existing_df, new_df], ignore_index=True)
184
+
185
+ # 写回 Excel 文件
186
+ updated_df.to_excel(file_path, index=False)
187
+ except Exception as e:
188
+ # 获取当前文件名和方法名
189
+ current_file = inspect.getfile(inspect.currentframe())
190
+ current_function = inspect.currentframe().f_code.co_name
191
+ print(f"An error occurred in file '{current_file}' and function '{current_function}': {e}")
192
+
193
+ def read_excel(file_path):
194
+ df = pd.read_excel(file_path, header=0, dtype={'debug': str,'debugX': str})
195
+ # 如果确实需要空字符串,先转换类型
196
+ pd.set_option('future.no_silent_downcasting', True)
197
+ df = df.astype(object).fillna("") # 转换为object类型
198
+ data_dict = df.to_dict(orient='records')
199
+ return data_dict
200
+
201
+ def exception_decorator(func):
202
+ @wraps(func)
203
+ def wrapper(*args, **kwargs):
204
+ try:
205
+ return func(*args, **kwargs)
206
+ except Exception as e:
207
+ # 直接使用被装饰函数的名字
208
+ function_name = func.__name__
209
+ file_name = os.path.basename(func.__code__.co_filename) # 提取文件名
210
+ # 打印异常和函数名
211
+ logging.error( f"异常发生在文件 [{file_name}] 的函数 [{function_name}] 中: {str(e)}",
212
+ exc_info=True # 自动追加堆栈信息
213
+ )
214
+ # 可选择再次抛出异常或进行其他处理
215
+ raise
216
+ return wrapper
217
+ def merge_dicts(a: dict, b: dict) -> dict:
218
+ """合并两个字典,重复key时优先取a中非空的值
219
+
220
+ Args:
221
+ a: 优先字典(保留其非空值)
222
+ b: 后备字典(当a中的值为空时使用)
223
+
224
+ Returns:
225
+ 合并后的新字典
226
+ """
227
+ return {
228
+ # 遍历所有key(a和b的key取并集)
229
+ k:
230
+ # 如果key在a中 且 a的value不是空值 → 取a的值
231
+ a.get(k) if k in a and a.get(k) not in (None, "")
232
+ # 否则 → 取b的值(可能为None)
233
+ else b.get(k)
234
+ # 获取a和b的所有key的并集(避免用a|b需要Python 3.9+)
235
+ for k in set(a) | set(b)
236
+ }
237
+
238
+ def gen_case_suite(file_path):
239
+ # 生成测试用例套件
240
+ data_dict = read_excel(file_path)
241
+ case_suite = []
242
+ for case in data_dict:
243
+ case_suite.append(case)
244
+ return case_suite
testcase/__init__.py ADDED
File without changes
testcase/test_all.py ADDED
@@ -0,0 +1,10 @@
1
+ # 具体测试类实现
2
+ from common.WSBaseApi import BaseApiTest
3
+ import pytest
4
+
5
+ @pytest.mark.gateway
6
+ class TestGateway(BaseApiTest):pass
7
+
8
+
9
+ @pytest.mark.aqua
10
+ class TestAqua(BaseApiTest):pass