iflow-mcp_galaxyxieyu_api-auto-test 0.1.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,60 @@
1
+ # @time: 2024-09-20
2
+ # @author: xiaoqq
3
+
4
+ from atf.core.config_manager import ConfigManager
5
+ from atf.auth import Auth
6
+ from atf.core.globals import Globals
7
+ from atf.core.log_manager import log
8
+
9
+ class LoginHandler:
10
+ """
11
+ 用于处理项目登录的管理类,提供登录判断和登录操作。
12
+ """
13
+
14
+ def __init__(self):
15
+ self.config_manager = ConfigManager()
16
+
17
+ def login_if_needed(self, project_name, project_config, env):
18
+ """
19
+ 判断项目是否需要登录,如果需要则进行登录
20
+ :param project_name: 项目名称
21
+ :param project_config: 项目配置
22
+ :param env: 环境名称
23
+ :return: 登录成功返回 token,失败返回 None
24
+ """
25
+ if project_config and project_config.get('is_need_login'):
26
+ auth_class = Auth()
27
+ # 获取 auth_class 中指定名称的方法,如果找不到该方法(例如 project_name_login),则使用默认的 login 方法。
28
+ login_function = getattr(auth_class, f'{project_name}_login', auth_class.login)
29
+ log.info(f"正在登录 {project_name}, 登录参数为:{project_config['login']}")
30
+ try:
31
+ token = login_function(project_name, project_config['login'], env)
32
+ log.info(f"{project_name} 登录成功并获取到 token: {token}。")
33
+ return token
34
+ except Exception as e:
35
+ log.error(f"{project_name} 登录失败:{e}")
36
+ return None
37
+ return None
38
+
39
+ def check_and_login_project(self, project_name, env):
40
+ """
41
+ 检查并登录指定项目,如果 Globals 中没有 token,则执行登录操作
42
+ :param project_name: 项目名称
43
+ :param env: 环境名称
44
+ """
45
+ project_info = Globals.get(project_name)
46
+ if project_info is None or not project_info.get("token"):
47
+ project_env_config = self.config_manager.get_project_env_config(project_name, env)
48
+ if project_env_config is not None:
49
+ log.info(f"project_name, project_env_config, env:{project_name}, {project_env_config}, {env}")
50
+ token = self.login_if_needed(project_name, project_env_config.get(project_name), env)
51
+ if token:
52
+ # 将 token 存入全局变量
53
+ Globals.update(project_name, "token", token)
54
+ # log.info(f"{project_name} 登录成功并获取 token。")
55
+ else:
56
+ log.error(f"{project_name} 登录失败。")
57
+ else:
58
+ log.warning(f"未找到项目 {project_name} 的配置,跳过登录。")
59
+ else:
60
+ log.info(f"{project_name} 已经登录,跳过登录操作。")
@@ -0,0 +1,189 @@
1
+ # @time: 2024-08-01
2
+ # @author: xiaoqq
3
+
4
+ import json
5
+ import time
6
+ import requests
7
+ from atf.core.log_manager import log
8
+
9
+
10
+ class SSEResponse:
11
+ """SSE 响应封装"""
12
+ def __init__(self, events, raw_lines, status_code):
13
+ self.events = events # 解析后的事件列表
14
+ self.raw_lines = raw_lines # 原始行数据
15
+ self.status_code = status_code
16
+ self.event_count = len(events)
17
+
18
+ def get_event(self, index):
19
+ """获取指定索引的事件"""
20
+ return self.events[index] if 0 <= index < len(self.events) else None
21
+
22
+ def get_all_data(self, field=None):
23
+ """获取所有事件的 data,可选提取特定字段"""
24
+ if field:
25
+ return [e.get('data', {}).get(field) for e in self.events if isinstance(e.get('data'), dict)]
26
+ return [e.get('data') for e in self.events]
27
+
28
+ def find_event(self, **kwargs):
29
+ """查找匹配条件的事件"""
30
+ for event in self.events:
31
+ match = True
32
+ for key, value in kwargs.items():
33
+ if key == 'data_contains':
34
+ if value not in str(event.get('data', '')):
35
+ match = False
36
+ elif key == 'event_type':
37
+ if event.get('event') != value:
38
+ match = False
39
+ else:
40
+ if event.get(key) != value:
41
+ match = False
42
+ if match:
43
+ return event
44
+ return None
45
+
46
+ def contains(self, text):
47
+ """检查是否包含指定文本"""
48
+ return any(text in str(e.get('data', '')) for e in self.events)
49
+
50
+
51
+ class RequestHandler:
52
+ @staticmethod
53
+ def send_request(method, url, headers=None, data=None, params=None, files=None, timeout=10):
54
+ log.info(f"正在发送 {method} 请求至 {url} ,headers={headers}, data={data}, params={params}, files={files}")
55
+
56
+ if method.lower() == 'get':
57
+ _params = params
58
+ if not _params:
59
+ _params = data
60
+ response = requests.get(url, headers=headers, params=_params, timeout=timeout)
61
+ elif method.lower() == 'post':
62
+ if files:
63
+ response = requests.post(url, headers=headers, files=files, timeout=timeout)
64
+ elif headers and 'Content-Type' in headers and headers['Content-Type'] == 'application/x-www-form-urlencoded':
65
+ response = requests.post(url, headers=headers, data=data, timeout=timeout)
66
+ else:
67
+ response = requests.post(url, headers=headers, json=data, timeout=timeout)
68
+ elif method.lower() in ['put', 'delete']:
69
+ response = requests.request(method, url, headers=headers, json=data, timeout=timeout)
70
+ else:
71
+ log.error(f"不支持的方法: {method}")
72
+ raise ValueError(f"不支持的方法: {method}")
73
+
74
+ try:
75
+ response_json = response.json()
76
+ response_json['_status_code'] = response.status_code # 添加状态码到响应中
77
+ log.info("返回参数:{}".format(response_json))
78
+ return response_json
79
+ except ValueError:
80
+ log.error("非JSON响应:{}".format(response.text))
81
+ response.raise_for_status()
82
+
83
+ if not response.ok:
84
+ log.error("请求失败:状态码 {}".format(response.status_code))
85
+ response.raise_for_status()
86
+
87
+ @staticmethod
88
+ def send_sse_request(method, url, headers=None, data=None, params=None,
89
+ timeout=60, max_events=None, stop_on=None):
90
+ """
91
+ 发送 SSE 流式请求
92
+ :param method: 请求方法
93
+ :param url: 请求地址
94
+ :param headers: 请求头
95
+ :param data: 请求体
96
+ :param params: 查询参数
97
+ :param timeout: 超时时间(秒)
98
+ :param max_events: 最大事件数,达到后停止
99
+ :param stop_on: 停止条件,如 {"data_contains": "[DONE]"} 或 {"event_type": "done"}
100
+ :return: SSEResponse 对象
101
+ """
102
+ headers = headers or {}
103
+ headers.setdefault('Accept', 'text/event-stream')
104
+
105
+ log.info(f"正在发送 SSE {method} 请求至 {url}, timeout={timeout}, max_events={max_events}, stop_on={stop_on}")
106
+
107
+ events = []
108
+ raw_lines = []
109
+ current_event = {}
110
+
111
+ try:
112
+ with requests.request(
113
+ method=method,
114
+ url=url,
115
+ headers=headers,
116
+ json=data if method.upper() != 'GET' else None,
117
+ params=params if method.upper() == 'GET' else None,
118
+ stream=True,
119
+ timeout=timeout
120
+ ) as resp:
121
+ status_code = resp.status_code
122
+ start_time = time.time()
123
+
124
+ for line in resp.iter_lines(decode_unicode=True):
125
+ # 超时检查
126
+ if time.time() - start_time > timeout:
127
+ log.info(f"SSE 请求超时,已收集 {len(events)} 个事件")
128
+ break
129
+
130
+ if line:
131
+ raw_lines.append(line)
132
+
133
+ if line.startswith('event:'):
134
+ current_event['event'] = line[6:].strip()
135
+ elif line.startswith('data:'):
136
+ data_str = line[5:].strip()
137
+ # 尝试解析 JSON
138
+ try:
139
+ current_event['data'] = json.loads(data_str)
140
+ except json.JSONDecodeError:
141
+ current_event['data'] = data_str
142
+ elif line.startswith('id:'):
143
+ current_event['id'] = line[3:].strip()
144
+ elif line.startswith('retry:'):
145
+ current_event['retry'] = int(line[6:].strip())
146
+ else:
147
+ # 空行表示事件结束
148
+ if current_event:
149
+ events.append(current_event)
150
+ log.debug(f"收到 SSE 事件: {current_event}")
151
+
152
+ # 检查停止条件
153
+ if stop_on:
154
+ should_stop = True
155
+ for key, value in stop_on.items():
156
+ if key == 'data_contains':
157
+ if value not in str(current_event.get('data', '')):
158
+ should_stop = False
159
+ elif key == 'event_type':
160
+ if current_event.get('event') != value:
161
+ should_stop = False
162
+ else:
163
+ if current_event.get(key) != value:
164
+ should_stop = False
165
+ if should_stop:
166
+ log.info(f"满足停止条件 {stop_on},停止接收")
167
+ break
168
+
169
+ # 检查最大事件数
170
+ if max_events and len(events) >= max_events:
171
+ log.info(f"达到最大事件数 {max_events},停止接收")
172
+ break
173
+
174
+ current_event = {}
175
+
176
+ # 处理最后一个未完成的事件
177
+ if current_event:
178
+ events.append(current_event)
179
+
180
+ except requests.exceptions.Timeout:
181
+ log.warning(f"SSE 请求超时,已收集 {len(events)} 个事件")
182
+ status_code = 0
183
+ except Exception as e:
184
+ log.error(f"SSE 请求异常: {e}")
185
+ raise
186
+
187
+ sse_response = SSEResponse(events, raw_lines, status_code)
188
+ log.info(f"SSE 请求完成,共收到 {sse_response.event_count} 个事件")
189
+ return sse_response
@@ -0,0 +1,212 @@
1
+ # @time: 2024-08-23
2
+ # @author: xiaoqq
3
+
4
+ import re
5
+ import importlib
6
+ from types import ModuleType
7
+ from typing import Any, Dict, Union
8
+ from atf.core.log_manager import log
9
+
10
+ class VariableResolver:
11
+ """
12
+ 解析YAML文件中的变量表达式'{{ *** }}',根据变量表达式调用工具函数、获取全局、会话变量的值。
13
+ 1. 先判断变量中是否有"()",如{{ tools.demo_func(a=2, b=1) }},有则调用 atf.utils.helpers.py 中的函数,并传入参数;
14
+ 2. 再判断是否存在“.”,有则再判断测试用例局部变量session_vars中是否存在“.”前面第一个字符串,如果是则获取
15
+ 3. 如果不是则从全局变量获取
16
+ 4. 如果变量表达式以Global或GLOBAL开头,则直接从全局变量获取
17
+ """
18
+ def __init__(self, session_vars: Dict[str, Any], global_vars: Dict[str, Any]):
19
+ self.session_vars = session_vars
20
+ self.global_vars = global_vars
21
+
22
+ def resolve_variable(self, expression: str) -> Any:
23
+ """解析 {{ }} 表达式并返回其值"""
24
+ expression = expression.strip()
25
+
26
+ # 如果是函数调用形式
27
+ if '(' in expression and ')' in expression:
28
+ return self._resolve_function(expression)
29
+ # 如果是多层次的变量引用形式
30
+ elif '.' in expression or '[' in expression:
31
+ return self._resolve_dot_expression(expression)
32
+ else:
33
+ raise ValueError(f"Unsupported expression format: {expression}")
34
+
35
+ def _resolve_function(self, expression: str) -> Any:
36
+ """处理函数调用形式的表达式"""
37
+ # 提取模块名和函数名
38
+ module_name, func_call = expression.split('.', 1)
39
+ module = self._import_module(module_name)
40
+
41
+ # 提取函数名和参数
42
+ func_name, args_str = re.match(r"(\w+)\((.*)\)", func_call).groups()
43
+ func = getattr(module, func_name)
44
+
45
+ # 解析参数
46
+ args = self._parse_function_args(args_str)
47
+
48
+ # 调用函数并返回结果
49
+ return func(**args)
50
+
51
+ def _import_module(self, module_name: str) -> ModuleType:
52
+ """导入模块"""
53
+ try:
54
+ return importlib.import_module(f'atf.utils.{module_name}')
55
+ except ImportError as e:
56
+ raise ImportError(f"Module {module_name} not found in atf.utils. Error: {e}")
57
+
58
+ def _parse_function_args(self, args_str: str) -> Dict[str, Any]:
59
+ """解析函数参数"""
60
+ return eval(f"dict({args_str})")
61
+
62
+ def _resolve_dot_expression(self, expression: str) -> Any:
63
+ """处理以 '.' 分隔的变量引用形式"""
64
+ keys = re.split(r'\.|\[|\]', expression)
65
+ keys = [key for key in keys if key] # 去掉空字符串
66
+
67
+ root_key = keys[0]
68
+
69
+ # 通过Global 或 GLOBAL 指定从全局变量获取,如{{Global.data.id}}
70
+ use_global = root_key in ['Global', 'GLOBAL']
71
+
72
+ if use_global:
73
+ root_key = keys[1]
74
+ value = self.global_vars[root_key]
75
+ elif root_key in self.session_vars:
76
+ value = self.session_vars[root_key]
77
+ elif root_key in self.global_vars:
78
+ value = self.global_vars[root_key]
79
+ else:
80
+ log.error(f"{root_key} not found in session_vars or global_vars")
81
+ raise ValueError(f"{root_key} not found in session_vars or global_vars")
82
+
83
+ for key in keys[1:]:
84
+ if key.isdigit(): # 检查key是否为索引
85
+ value = value[int(key)]
86
+ else:
87
+ try:
88
+ value = value[key]
89
+ except KeyError:
90
+ log.error(f"Key '{key}' not found in {value}")
91
+ raise ValueError(f"Key '{key}' not found in {value}")
92
+
93
+ return value
94
+
95
+ def process_value(self, value: Union[str, Dict, list]) -> Any:
96
+ """递归处理字典、列表或字符串中的 {{ }} 表达式"""
97
+ if isinstance(value, str) and '{{' in value and '}}' in value:
98
+ return self._process_string(value)
99
+ elif isinstance(value, dict):
100
+ return {k: self.process_value(v) for k, v in value.items()}
101
+ elif isinstance(value, list):
102
+ return [self.process_value(item) for item in value]
103
+ else:
104
+ return value
105
+
106
+ def _process_string(self, value: str) -> any:
107
+ """解析字符串中的 {{ }} 表达式并替换"""
108
+ # pattern = r'\{\{\s*(.*?)\s*\}\}'
109
+ pattern = r'\{\{(.*?)}\}'
110
+ matches = re.findall(pattern, value)
111
+
112
+ for match in matches:
113
+ match_stripped = match.strip() # 去除表达式两侧的空格
114
+ # 检查是否以Global/GLOBAL开头
115
+ if match_stripped.startswith(('Global.', 'GLOBAL.')):
116
+ resolved_value = self._resolve_dot_expression(match_stripped)
117
+ else:
118
+ resolved_value = self.resolve_variable(match_stripped)
119
+
120
+ # resolved_value = self.resolve_variable(match_stripped)
121
+
122
+ # 确定表达式的原始格式,并替换
123
+ original_format = f'{{{{ {match_stripped} }}}}'
124
+ if match.startswith(' ') and match.endswith(' '):
125
+ new_format = original_format
126
+ elif match.startswith(' '):
127
+ new_format = f'{{{{ {match_stripped}}}}}'
128
+ elif match.endswith(' '):
129
+ new_format = f'{{{{{match_stripped} }}}}'
130
+ else:
131
+ new_format = f'{{{{{match_stripped}}}}}'
132
+
133
+ # 如果解析出的值不是字符串,且原始表达式不是整个字符串,则保留原始类型
134
+ if isinstance(resolved_value, (int, float, bool)) and value == new_format:
135
+ return resolved_value
136
+ # 否则,将其转换为字符串并替换掉原始表达式
137
+ value = value.replace(new_format, str(resolved_value))
138
+
139
+ return value
140
+
141
+ def process_data(self, data: Any) -> Any:
142
+ """处理 YAML 数据并替换所有 {{ }} 表达式"""
143
+ data_str = str(data)
144
+ if '{{' in data_str:
145
+ # log.info(f"开始解析并获取变量:{data}")
146
+ log.info(f"开始解析并获取变量")
147
+ return self.process_value(data)
148
+ return data
149
+
150
+ if __name__ == '__main__':
151
+ # 示例用法
152
+ session_vars = {
153
+ 'step1': {
154
+ 'data': {
155
+ 'id': 123,
156
+ 'orderNo': 1020240906142034235893,
157
+ 'items': [
158
+ {'id': 'item_0'},
159
+ {'id': 'item_1'}
160
+ ]
161
+ }
162
+ }
163
+ }
164
+
165
+ global_vars = {
166
+ 'merchant': {
167
+ 'token': 'xyz-token'
168
+ }
169
+ }
170
+
171
+ url = '/api/endpoint1/{{ tools.demo_get_id() }}'
172
+
173
+ headers = {
174
+ 'Content-Type': 'application/json',
175
+ 'Authorization': '{{ merchant.token }}'
176
+ }
177
+
178
+ data = {
179
+ 'param1': '{{ step1.data.id }}',
180
+ 'param2': {
181
+ 'p_param': {
182
+ 'p_p_param1': '{{tools.demo_func(a=3, b=1, c=3) }}',
183
+ 'p_p_param2': '{{ tools.demo_func()}}',
184
+ 'p_p_param3': 333,
185
+ 'p_p_param4': ['/goods/detail/{{tools.demo_get_id()}}', "sss"],
186
+ 'p_p_param5': '{{step1.data.items[1].id}}'
187
+ }
188
+ }
189
+ }
190
+ asserts = [
191
+ {
192
+ "type": "equal",
193
+ "field": "code",
194
+ "expected": 0
195
+ },
196
+ {
197
+ "type": "equal",
198
+ "field": "message",
199
+ "expected": "success"
200
+ },
201
+ {
202
+ "type": "equal",
203
+ "field": "data.orderNo",
204
+ "expected": "{{step1.data.orderNo}}"
205
+ }
206
+ ]
207
+
208
+ resolver = VariableResolver(session_vars, global_vars)
209
+ print(resolver.process_data(url))
210
+ print(resolver.process_data(headers))
211
+ print(resolver.process_data(data))
212
+ print(resolver.process_data(asserts))
@@ -0,0 +1,10 @@
1
+ # 处理器模块
2
+ from .teardown_handler import TeardownHandler
3
+ from .report_generator import ReportGenerator
4
+ from .notification_handler import NotificationHandler
5
+
6
+ __all__ = [
7
+ 'TeardownHandler',
8
+ 'ReportGenerator',
9
+ 'NotificationHandler',
10
+ ]
@@ -0,0 +1,101 @@
1
+ # @time: 2024-08-27
2
+ # @author: xiaoqq
3
+
4
+ from dingtalkchatbot.chatbot import DingtalkChatbot
5
+ from atf.core.log_manager import log
6
+
7
+
8
+ def set_color_and_size(text, color, size='14px'):
9
+ colors = {
10
+ "green": "#00CC00", # 成功
11
+ "blue": "#0000FF", # 信息
12
+ "yellow": "#FFCC00", # 跳过
13
+ "red": "#FF0000", # 失败/错误
14
+ "black": "#000000" # 普通文本
15
+ }
16
+ # 使用 style 属性控制字体大小和加粗
17
+ return f'<font color="{colors.get(color, "#000000")}" style="font-size:{size}; font-weight:bold;">{text}</font>'
18
+
19
+
20
+ def format_field(label, value, color='black', size='14px', icon=None):
21
+ """
22
+ 格式化消息字段,支持颜色、字体大小和图标
23
+ """
24
+ label = set_color_and_size(label, color, size)
25
+ if icon:
26
+ return f"{icon} {label}: {value}"
27
+ return f"{label}: {value}"
28
+
29
+
30
+ class NotificationHandler:
31
+ """
32
+ 钉钉消息发送
33
+ """
34
+
35
+ def __init__(self, webhook, secret, project=None):
36
+ self.ding = DingtalkChatbot(webhook=webhook, secret=secret)
37
+ self.project = project
38
+
39
+ def _build_message(self, conclusion, total, passed, failed, error, skipped, start_time, duration):
40
+ """
41
+ 构建消息文本
42
+ """
43
+ # 消息标题,字体加粗且字号更大
44
+ header = "<strong><font size='24px'>📢 接口自动化测试监控通知</font></strong>"
45
+
46
+ # 信息字段及其样式
47
+ items = [
48
+ ('测试结果', conclusion, 'green' if conclusion == '执行通过' else 'red', '16px'),
49
+ ('开始时间', start_time, 'black', '14px'),
50
+ ('执行时长', f'{duration:.2f} 秒', 'black', '14px'),
51
+ ('用例总数', total, 'blue', '14px'),
52
+ ('成功用例', passed, 'green', '14px'),
53
+ ('失败用例', failed, 'red', '14px'),
54
+ ('错误用例', error, 'red', '14px'),
55
+ ('跳过用例', skipped, 'yellow', '14px')
56
+ ]
57
+
58
+ # 定义 Unicode Emoji 图标
59
+ icons = {
60
+ '测试结果': '📝',
61
+ '开始时间': '⏰',
62
+ '执行时长': '⏱️',
63
+ '用例总数': '📊',
64
+ '成功用例': '✅',
65
+ '失败用例': '❌',
66
+ '错误用例': '💥',
67
+ '跳过用例': '⚠️'
68
+ }
69
+
70
+ # 使用优化后的样式和图标构建消息体
71
+ lines = [format_field(item[0], item[1], item[2], item[3], icon=icons.get(item[0])) for item in items]
72
+ text = "\n\n".join(lines)
73
+ return f"{header}\n\n{text}"
74
+
75
+ def send_markdown_msg(self, conclusion, total, passed, failed, error, skipped, start_time, duration):
76
+ """
77
+ 发送测试用例执行结果
78
+ """
79
+ # 构建钉钉消息内容
80
+ text = self._build_message(conclusion, total, passed, failed, error, skipped, start_time, duration)
81
+ title = "【接口自动化测试通知】"
82
+ try:
83
+ log.info("钉钉机器人正在发送结果......")
84
+ self.ding.send_markdown(title=title, text=text, is_at_all=True)
85
+ log.info("测试结果已发送至钉钉群")
86
+ except Exception as e:
87
+ log.error("钉钉机器人发送消息失败:%s", e)
88
+
89
+ if __name__ == '__main__':
90
+ webhook = "https://oapi.dingtalk.com/robot/send?access_token=your_dingtalk_access_token"
91
+ secret = "your_secret"
92
+ NotificationHandler(webhook, secret).send_markdown_msg(
93
+ conclusion="执行通过",
94
+ total = 100,
95
+ passed = 95,
96
+ failed = 2,
97
+ error = 3,
98
+ skipped = 0,
99
+ start_time = "2024-9-14 11:44:43",
100
+ duration = 1000.22
101
+ )