├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── browser
└── readme.md
├── config
└── config.ini
├── core
├── api
│ ├── collector.py
│ ├── testcase.py
│ └── teststep.py
├── app
│ ├── collector.py
│ ├── device
│ │ ├── __init__.py
│ │ ├── assertionOpt.py
│ │ ├── conditionOpt.py
│ │ ├── relationOpt.py
│ │ ├── scenarioOpt.py
│ │ ├── systemOpt.py
│ │ └── viewOpt.py
│ ├── find_opt.py
│ ├── testcase.py
│ └── teststep.py
├── assertion.py
├── template.py
└── web
│ ├── collector.py
│ ├── driver
│ ├── __init__.py
│ ├── assertionOpt.py
│ ├── browserOpt.py
│ ├── conditionOpt.py
│ ├── pageOpt.py
│ ├── relationOpt.py
│ └── scenarioOpt.py
│ ├── find_opt.py
│ ├── testcase.py
│ └── teststep.py
├── lm
├── lm_api.py
├── lm_case.py
├── lm_config.py
├── lm_log.py
├── lm_report.py
├── lm_result.py
├── lm_run.py
├── lm_setting.py
├── lm_start.py
├── lm_upload.py
└── lm_ws.py
├── requirements.txt
├── startup.py
└── tools
├── funclib
├── __init__.py
├── load_faker.py
├── params_enum.py
└── provider
│ └── lm_provider.py
└── utils
├── sql.py
└── utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | **/__pycache__/
2 |
3 | .idea
4 |
5 | image
6 |
7 | log
8 |
9 | data
10 |
11 | file
12 |
13 | browser/**.exe
14 |
15 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8
2 |
3 | MAINTAINER "liuma"
4 |
5 | COPY browser /liuma/browser
6 |
7 | COPY core /liuma/core
8 |
9 | COPY requirements.txt /liuma/
10 |
11 | COPY tools/ /liuma/tools
12 |
13 | COPY lm/ /liuma/lm
14 |
15 | COPY startup.py /liuma/
16 |
17 | WORKDIR /liuma
18 |
19 | RUN pip install -r requirements.txt -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
20 |
21 | CMD ["python", "startup.py"]
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 流马-低代码测试平台
2 | ## 一、项目概述
3 |
4 | 流马是一款低代码自动化测试平台,旨在采用最简单的架构统一支持API/WebUI/AppUI的自动化测试。平台采用低代码设计模式,将传统测试脚本以配置化实现,从而让代码能力稍弱的用户快速上手自动化测试。同时平台也支持通过简单的代码编写实现自定义组件,使用户可以灵活实现自己的需求。
5 |
6 | 本项目分为平台端和引擎端,采用分布式执行设计,可以将测试执行的节点(即引擎)注册在任意环境的任意一台机器上,从而突破资源及网络限制。同时,通过将引擎启动在本地PC上,方便用户快速调试测试用例,实时查看执行过程,带来传统脚本编写一致的便捷。
7 |
8 | 在线体验: [演示平台](http://demo-ee.liumatest.cn)
9 |
10 | 官网地址: [流马官网](http://www.liumatest.cn)
11 |
12 | 社区地址: [流马社区](http://community.liumatest.cn)
13 |
14 | 配套开发教程: [B站课堂](https://www.bilibili.com/cheese/play/ss7009)
15 |
16 | 如果本项目对您有帮助,请给我们一个Star,您的支持是我们前进的动力。
17 |
18 | 如果您需要二次开发,请务必遵循AGPL开源协议,并保留版权信息。我们保留一切对于侵权行为追责的权利。
19 |
20 | ## 二、功能介绍
21 |
22 | 
23 |
24 | 1. API测试
25 | ```
26 | (1) 支持单接口测试和链路测试。
27 | (2) 支持接口统一管理,支持postman/swagger导入。
28 | (3) 支持一键生成字段校验的接口健壮性用例。
29 | (4) 支持全局变量、关联、断言、内置函数、自定义函数。
30 | (5) 支持前后置脚本、失败继续、超时时间、等待/条件/循环等逻辑控制器。
31 | (6) 支持环境与用例解耦,多种方式匹配域名,让一套用例可以在多个环境上执行。
32 | ```
33 |
34 | 2. WebUI测试
35 | ```
36 | (1) 支持关键字驱动,零代码编写用例。
37 | (2) 支持UI元素统一管理,Excel模板批量导入。
38 | (3) 支持自定义关键字,封装公共的操作步骤,提升用例可读性。
39 | (4) 支持本地引擎执行,实时查看执行过程。
40 | (5) 支持与API用例在同一用例集合顺序执行。
41 | ```
42 |
43 | 3. AppUI测试
44 | ```
45 | (1) 支持WebUI同等用例编写和执行能力
46 | (2) 支持安卓和苹果系统
47 | (3) 支持持真机管理、投屏和在线操作
48 | (4) 支持控件元素在线获取,一键保存元素
49 | (5) 支持实时查看执行过程
50 | ```
51 |
52 | 更多功能及详细请参考: [用户手册](http://www.liumatest.cn/productDoc)
53 |
54 |
55 | ## 三、开发环境
56 |
57 | 环境依赖: Python3.8、Chrome、ChromeDriver(参考:[驱动说明](./browser/readme.md))
58 |
59 | IDE推荐: python使用pyCharm
60 |
61 | 1. 引擎启动
62 | ```
63 | Step1: 安装依赖包 pip3 install -r requirements.txt
64 |
65 | Step2: 流马测试平台->引擎管理->注册引擎 保存engine-code和engine-secret
66 |
67 | Step3: engine-code和engine-secret填写在/config/config.ini文件中对应位置
68 |
69 | Step4: 修改/config/config.ini文件中Platform->url为后端地址
70 |
71 | Step5: 如linux启动,修改/config/config.ini文件中WebDriver->options为headless
72 |
73 | Step6: 如linux/mac启动,修改/config/config.ini文件中WebDriver->path为chromedriver
74 |
75 | Step7: 启动引擎 python3 startup.py
76 | ```
77 |
78 | 2. 验证启动
79 |
80 | 平台引擎管理查看自己的引擎,显示在线,证明启动成功。再编写一个简单的接口用例并执行,执行成功并返回报告,引擎注册完成。
81 |
82 | ## 四、容器部署
83 |
84 | 容器部署请参考: [部署手册](http://www.liumatest.cn/deployDoc)
85 |
86 |
87 | ## 五、关于我们
88 |
89 | 流马秉持着帮助中小企业的测试团队快速建立自动化体系的目标,将会不断迭代并吸取用户的建议,欢迎大家给我们提出宝贵的意见。
90 |
91 | 如需学习平台开发相关内容或在线交流,可关注个人微信公众号【流马测试】
92 |
93 | 
94 |
95 |
96 |
--------------------------------------------------------------------------------
/browser/readme.md:
--------------------------------------------------------------------------------
1 | ### Chromedriver
2 |
3 | selenium驱动谷歌浏览器依赖于chromedriver, 因此启动引擎前需要下载驱动。
4 |
5 | + 下载地址
6 |
7 | 推荐使用淘宝下载源[下载链接](http://npm.taobao.org/mirrors/chromedriver/)
8 |
9 |
10 | + 使用说明
11 |
12 | chromedriver版本需要与引擎所在机器安装的chrome浏览器相对应,且浏览器尽量不要使用最新版本。
13 |
14 | 将chromedriver下载后,复制到当前目录下即可。
15 |
--------------------------------------------------------------------------------
/config/config.ini:
--------------------------------------------------------------------------------
1 | [Platform]
2 | url = http://127.0.0.1:8080
3 | enable-stderr = true
4 |
5 | [Engine]
6 | engine-code = ******
7 | engine-secret = ******
8 |
9 | [Header]
10 | content-type = application/json;charset=utf-8
11 | token = ******
12 |
13 | [WebDriver]
14 | options = normal
15 | path = chromedriver.exe
16 |
17 | [RunSetting]
18 | max-run = 2
19 |
20 |
--------------------------------------------------------------------------------
/core/api/collector.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 |
4 | from tools.utils.utils import proxies_join, handle_form_data, handle_files
5 |
6 |
7 | class ApiRequestCollector:
8 |
9 | def __init__(self):
10 | self.apiId = None
11 | self.apiName = None
12 | self.method = None
13 | self.url = None
14 | self.path = None
15 | self.protocol = None
16 | self.body_type = None
17 | self.others = {}
18 | self.controller = {}
19 | self.looper = {}
20 | self.conditions = []
21 | self.assertions = []
22 | self.relations = []
23 |
24 | def collect_flag(self, api_data, arg_name):
25 | if arg_name not in api_data or api_data[arg_name] is None:
26 | raise NotExistedFieldError('接口数据{}字段不存在或为空'.format(arg_name))
27 | elif type(api_data[arg_name]) is str and len(api_data[arg_name]) == 0:
28 | raise NotExistedFieldError('接口数据{}字段长度为0'.format(arg_name))
29 | else:
30 | setattr(self, arg_name, api_data[arg_name])
31 |
32 | def collect_other(self, api_data, arg_name, func=lambda x: x):
33 | if arg_name not in api_data or api_data[arg_name] is None or len(api_data[arg_name]) == 0:
34 | self.others[arg_name] = None
35 | else:
36 | self.others[arg_name] = func(api_data[arg_name])
37 |
38 | def collect_context(self, api_data, arg_name):
39 | if arg_name not in api_data or api_data[arg_name] is None or len(api_data[arg_name]) == 0:
40 | setattr(self, arg_name, None)
41 | else:
42 | setattr(self, arg_name, api_data[arg_name])
43 |
44 | def collect_id(self, api_data):
45 | self.collect_flag(api_data, "apiId")
46 |
47 | def collect_name(self, api_data):
48 | self.collect_flag(api_data, "apiName")
49 |
50 | def collect_protocol(self, api_data):
51 | self.collect_flag(api_data, "protocol")
52 |
53 | def collect_method(self, api_data):
54 | if 'method' not in api_data or api_data['method'] is None or len(api_data['method']) == 0:
55 | raise UnDefinableMethodError("接口{}未定义请求方法".format(api_data['apiId']))
56 | method = api_data['method'].upper()
57 |
58 | self.method = method
59 |
60 | def collect_url(self, api_data):
61 | if 'url' not in api_data:
62 | raise UnDefinablePathError("接口{}未设置域名".format(api_data['apiId']))
63 | else:
64 | self.url = api_data['url']
65 |
66 | def collect_path(self, api_data):
67 | if 'path' not in api_data:
68 | raise UnDefinablePathError("接口{}未设置路径".format(api_data['apiId']))
69 | else:
70 | fields = re.findall(r'\{(.*?)\}', api_data['path'])
71 | path = api_data['path']
72 | for field in fields:
73 | result = "{%s}" % field
74 | if field in api_data['rest']:
75 | result = api_data["rest"][field] # 将path中的参数替换成rest
76 | if "#{%s}" % field in path: # 兼容老版本#{name}
77 | path = path.replace("#{%s}" % field, result)
78 | else:
79 | path = path.replace("{%s}" % field, result)
80 | self.path = path
81 |
82 | def collect_controller(self, api_data):
83 | if "sleepBeforeRun" not in api_data["controller"]:
84 | api_data["controller"]["sleepBeforeRun"] = 0 # 默认执行前不等待
85 | if "sleepAfterRun" not in api_data["controller"]:
86 | api_data["controller"]["sleepAfterRun"] = 0 # 默认执行完成不等待
87 | if "useSession" not in api_data["controller"]:
88 | api_data["controller"]["useSession"] = "false" # 默认不使用session
89 | if "saveSession" not in api_data["controller"]:
90 | api_data["controller"]["saveSession"] = "false" # 默认不保存session
91 | if "pre" not in api_data["controller"]:
92 | api_data["controller"]["pre"] = None # 默认没有前置脚本和sql
93 | if "post" not in api_data["controller"]:
94 | api_data["controller"]["post"] = None # 默认没有后置脚本和sql
95 | if "errorContinue" not in api_data["controller"]:
96 | api_data["controller"]["errorContinue"] = "false" # 默认错误后不再执行
97 | self.controller = api_data["controller"]
98 |
99 | def collect_conditions(self, api_data):
100 | if "whetherExec" in api_data["controller"]:
101 | self.conditions = json.loads(api_data["controller"]["whetherExec"])
102 |
103 | def collect_looper(self, api_data):
104 | if "loopExec" in api_data["controller"]:
105 | self.looper = json.loads(api_data["controller"]["loopExec"])
106 |
107 | def collect_query(self, api_data):
108 | if len(api_data["query"]) > 0:
109 | self.others["params"] = api_data["query"]
110 | else:
111 | self.others["params"] = None
112 |
113 | def collect_headers(self, api_data):
114 | self.collect_other(api_data, 'headers')
115 |
116 | def collect_cookies(self, api_data):
117 | if self.others['headers'] is not None:
118 | pop_key = None
119 | for key in self.others['headers']:
120 | if key.strip().lower() in ['cookie', 'cookies']:
121 | pop_key = key
122 | break
123 | if pop_key is not None:
124 | value = self.others['headers'].pop(pop_key)
125 | self.others['headers']['cookie'] = value
126 |
127 | def collect_proxies(self, api_data):
128 | self.collect_other(api_data, 'proxies', proxies_join)
129 |
130 | def collect_body(self, api_data):
131 | body = api_data["body"]
132 | if body is None:
133 | return
134 | self.body_type = body["type"]
135 | if body["type"] == "json":
136 | if body["json"] != '':
137 | body_json = json.loads(body["json"])
138 | if len(body_json) > 0:
139 | self.others["json"] = body_json
140 | elif body["type"] in ("form-urlencoded", "form-data"):
141 | body_data, body_file = handle_form_data(body["form"])
142 | if len(body_data) > 0:
143 | self.others["data"] = body_data
144 | if len(body_file) > 0:
145 | self.others["files"] = body_file
146 | elif body["type"] in ("text", "xml", "html"):
147 | if body["raw"] != "":
148 | self.others["data"] = body["raw"]
149 | elif body["type"] == "file":
150 | files = handle_files(body["file"])
151 | if len(files) > 0:
152 | self.others["files"] = files
153 |
154 | def collect_stream(self, api_data):
155 | if "requireStream" in api_data["controller"]:
156 | if api_data["controller"]["requireStream"].lower() == "true":
157 | self.others["stream"] = True
158 | else:
159 | self.others["stream"] = False
160 | else:
161 | self.others["stream"] = None
162 |
163 | def collect_verify(self, api_data):
164 | if "requireVerify" in api_data["controller"]:
165 | if api_data["controller"]["requireVerify"].lower() == "true":
166 | self.others["verify"] = True
167 | else:
168 | self.others["verify"] = False
169 | else:
170 | self.others["verify"] = None
171 |
172 | def collect_auth(self, api_data):
173 | pass
174 |
175 | def collect_timeout(self, api_data):
176 | if "timeout" in api_data["controller"]:
177 | self.others["timeout"] = int(api_data["controller"]["timeout"])
178 | else:
179 | self.others["timeout"] = None
180 |
181 | def collect_allow_redirects(self, api_data):
182 | pass
183 |
184 | def collect_hooks(self, api_data):
185 | pass
186 |
187 | def collect_cert(self, api_data):
188 | pass
189 |
190 | def collect_assertions(self, api_data):
191 | self.collect_context(api_data, 'assertions')
192 |
193 | def collect_relations(self, api_data):
194 | self.collect_context(api_data, 'relations')
195 |
196 | def collect(self, api_data):
197 | self.collect_id(api_data)
198 | self.collect_name(api_data)
199 | self.collect_method(api_data)
200 | self.collect_url(api_data)
201 | self.collect_path(api_data)
202 |
203 | self.collect_controller(api_data)
204 |
205 | self.collect_headers(api_data)
206 | self.collect_cookies(api_data)
207 | self.collect_proxies(api_data)
208 |
209 | self.collect_query(api_data)
210 | self.collect_body(api_data)
211 |
212 | self.collect_verify(api_data)
213 | self.collect_stream(api_data)
214 | self.collect_auth(api_data)
215 | self.collect_timeout(api_data)
216 | self.collect_allow_redirects(api_data)
217 | self.collect_hooks(api_data)
218 | self.collect_cert(api_data)
219 |
220 | self.collect_assertions(api_data)
221 | self.collect_relations(api_data)
222 |
223 |
224 | class UnDefinableMethodError(Exception):
225 | """未定义请求方法"""
226 |
227 |
228 | class UnDefinablePathError(Exception):
229 | """未定义请求路径"""
230 |
231 |
232 | class NotExistedFieldError(Exception):
233 | """未定义必须字段"""
234 |
235 |
236 | class NotExistedFileUploadType(Exception):
237 | """未定义的文件上传方式"""
238 |
--------------------------------------------------------------------------------
/core/api/testcase.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sys
3 |
4 | from core.api.collector import ApiRequestCollector
5 | from core.template import Template
6 | from core.api.teststep import ApiTestStep, dict2str
7 | from jsonpath_ng.parser import JsonPathParser
8 |
9 | from tools.utils.utils import get_case_message, get_json_relation, handle_params_data
10 |
11 |
12 | class ApiTestCase:
13 |
14 | def __init__(self, test):
15 | self.test = test
16 | self.case_message = get_case_message(test.test_data)
17 | self.session = test.session
18 | self.context = test.context
19 | self.id = self.case_message['caseId']
20 | self.name = self.case_message['caseName']
21 | setattr(test, 'test_case_name', self.case_message['caseName'])
22 | setattr(test, 'test_case_desc', self.case_message['comment'])
23 | self.functions = self.case_message['functions']
24 | self.params = handle_params_data(self.case_message['params'])
25 | self.template = Template(self.test, self.context, self.functions, self.params)
26 | self.json_path_parser = JsonPathParser()
27 | self.comp = re.compile(r"\{\{.*?\}\}")
28 |
29 | def execute(self):
30 | """用例执行入口函数"""
31 | if self.case_message['apiList'] is None:
32 | raise RuntimeError("无法获取API相关数据, 请重试!!!")
33 | self.loop_execute(self.case_message['apiList'], "root")
34 |
35 | def loop_execute(self, api_list, loop_id, step_n=0):
36 | """循环执行"""
37 | while step_n < len(api_list):
38 | api_data = api_list[step_n]
39 | # 定义收集器
40 | collector = ApiRequestCollector()
41 | step = ApiTestStep(self.test, self.session, collector, self.context, self.params)
42 | # 循环控制器
43 | step.collector.collect_looper(api_data)
44 | if len(step.collector.looper) > 0 and not (loop_id != "root" and step_n == 0):
45 | # 非根循环 且并非循环第一个接口时才执行循环 从而避免循环套循环情况下的死循环
46 | step.looper_controller(self, api_list, step_n)
47 | step_n = step_n + step.collector.looper["num"] # 跳过本次循环中执行的接口
48 | continue # 母循环最后一个接口索引必须超过子循环的最后一个接口索引 否则超过母循环的接口无法执行
49 | step_n += 1
50 | # 定义事务
51 | self.test.defineTrans(api_data['apiId'], api_data['apiName'], api_data['path'], api_data['apiDesc'])
52 | # 条件控制器
53 | step.collector.collect_conditions(api_data)
54 | if len(step.collector.conditions) > 0:
55 | result = step.condition_controller(self)
56 | if result is not True:
57 | self.test.updateTransStatus(3) # 任意条件不满足 跳过执行
58 | self.test.debugLog('[{}]接口条件控制器判断为否: {}'.format(api_data['apiName'], result))
59 | continue
60 | # 收集请求主体并执行
61 | step.collector.collect(api_data)
62 | try:
63 | # 执行前置脚本和sql
64 | if step.collector.controller["pre"] is not None:
65 | for pre in step.collector.controller["pre"]:
66 | if pre['name'] == 'preScript':
67 | step.exec_script(pre["value"])
68 | else:
69 | step.exec_sql(pre["value"], self)
70 | # 渲染主体
71 | self.render_content(step)
72 | # 执行step, 接口参数移除,接口请求,接口响应,断言操作,依赖参数提取
73 | step.execute()
74 | # 执行后置脚本和sql
75 | if step.collector.controller["post"] is not None:
76 | for post in step.collector.controller["post"]:
77 | if post['name'] == 'postScript':
78 | step.exec_script(post["value"])
79 | else:
80 | step.exec_sql(post["value"], self)
81 | # 检查step的断言结果
82 | if step.assert_result['result']:
83 | self.test.debugLog('[{}]接口断言成功: {}'.format(step.collector.apiName,
84 | dict2str(step.assert_result['checkMessages'])))
85 | else:
86 | self.test.errorLog('[{}]接口断言失败: {}'.format(step.collector.apiName,
87 | dict2str(step.assert_result['checkMessages'])))
88 | raise AssertionError(dict2str(step.assert_result['checkMessages']))
89 | except Exception as e:
90 | error_info = sys.exc_info()
91 | if collector.controller["errorContinue"].lower() == "true":
92 | # 失败后继续执行
93 | if issubclass(error_info[0], AssertionError):
94 | self.test.recordFailStatus(error_info)
95 | else:
96 | self.test.recordErrorStatus(error_info)
97 | else:
98 | raise e
99 |
100 | def render_looper(self, looper):
101 | self.template.init(looper)
102 | _looper = self.template.render()
103 | if "times" in _looper:
104 | try:
105 | times = int(_looper["times"])
106 | except:
107 | times = 1
108 | _looper["times"] = times
109 | return _looper
110 |
111 | def render_conditions(self, conditions):
112 | self.template.init(conditions)
113 | return self.template.render()
114 |
115 | def render_sql(self, sql):
116 | self.template.init(sql)
117 | return self.template.render()
118 |
119 | def render_content(self, step):
120 | self.template.init(step.collector.path)
121 | step.collector.path = self.template.render()
122 | if step.collector.others.get('headers') is not None:
123 | headers = step.collector.others.pop('headers')
124 | else:
125 | headers = None
126 | if step.collector.others.get('params') is not None:
127 | query = step.collector.others.pop('params')
128 | else:
129 | query = None
130 | if step.collector.others.get('data') is not None:
131 | body = step.collector.others.pop('data')
132 | pop_key = 'data'
133 | elif step.collector.others.get('json') is not None:
134 | body = step.collector.others.pop('json')
135 | pop_key = 'json'
136 | else:
137 | body = None
138 | pop_key = None
139 | self.template.init(step.collector.others)
140 | step.collector.others = self.template.render()
141 | self.template.set_help_data(step.collector.url, step.collector.path, headers, query, body)
142 | if "#{_request_query" in str(headers).lower() or "#{_request_body" in str(headers).lower():
143 | if "#{_request_body" in str(query).lower():
144 | self.render_json(step, body, "body", pop_key)
145 | self.render_json(step, query, "query")
146 | self.render_json(step, headers, "headers")
147 | else:
148 | self.render_json(step, query, "query")
149 | self.render_json(step, body, "body", pop_key)
150 | self.render_json(step, headers, "headers")
151 | else:
152 | if "#{_request_body" in str(query).lower():
153 | self.render_json(step, headers, "headers")
154 | self.render_json(step, body, "body", pop_key)
155 | self.render_json(step, query, "query")
156 | else:
157 | self.render_json(step, headers, "headers")
158 | self.render_json(step, query, "query")
159 | self.render_json(step, body, "body", pop_key)
160 | if step.collector.assertions is not None:
161 | self.template.init(step.collector.assertions)
162 | step.collector.assertions = self.template.render()
163 | if step.collector.relations is not None:
164 | self.template.init(step.collector.relations)
165 | step.collector.relations = self.template.render()
166 |
167 | def render_json(self, step, data, name, pop_key=None):
168 | if data is None:
169 | return
170 | if name == "body" and step.collector.body_type not in ("json", "form-urlencoded", "form-data"):
171 | self.template.init(data)
172 | render_value = self.template.render()
173 | self.template.request_body = render_value
174 | else:
175 | for expr, value in get_json_relation(data, name):
176 | if isinstance(value, str) and self.comp.search(value) is not None:
177 | self.template.init(value)
178 | render_value = self.template.render()
179 | if name == "headers":
180 | render_value = str(render_value)
181 | expression = self.json_path_parser.parse(expr)
182 | expression.update(data, render_value)
183 | if name == "body":
184 | self.template.request_body = data
185 | elif name == "query":
186 | self.template.request_query = data
187 | else:
188 | self.template.request_headers = data
189 | if name == "body":
190 | step.collector.others.setdefault(pop_key, self.template.request_body)
191 | elif name == "query":
192 | step.collector.others.setdefault("params", self.template.request_query)
193 | else:
194 | step.collector.others.setdefault("headers", self.template.request_headers)
195 |
196 |
--------------------------------------------------------------------------------
/core/app/collector.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 |
4 | class AppOperationCollector:
5 |
6 | def __init__(self):
7 | self.id = None
8 | self.opt_type = None
9 | self.opt_system = None
10 | self.opt_name = None
11 | self.opt_trans = None
12 | self.opt_element = None
13 | self.opt_data = None
14 | self.opt_code = None
15 |
16 | @staticmethod
17 | def __parse(ui_data: dict, name):
18 | if name not in ui_data:
19 | return None
20 | return ui_data.get(name)
21 |
22 | def collect_id(self, ui_data):
23 | self.id = AppOperationCollector.__parse(ui_data, "operationId")
24 |
25 | def collect_opt_type(self, ui_data):
26 | self.opt_type = AppOperationCollector.__parse(ui_data, "operationType")
27 |
28 | def collect_opt_system(self, ui_data):
29 | self.opt_system = AppOperationCollector.__parse(ui_data, "operationSystem")
30 |
31 | def collect_opt_name(self, ui_data):
32 | self.opt_name = AppOperationCollector.__parse(ui_data, "operationName")
33 |
34 | def collect_opt_trans(self, ui_data):
35 | self.opt_trans = AppOperationCollector.__parse(ui_data, "operationTrans")
36 |
37 | def collect_opt_code(self, ui_data):
38 | self.opt_code = AppOperationCollector.__parse(ui_data, "operationCode")
39 |
40 | def collect_opt_element(self, ui_data):
41 | opt_element = AppOperationCollector.__parse(ui_data, "operationElement")
42 | if opt_element is None or len(opt_element) == 0:
43 | self.opt_element = None
44 | else:
45 | elements = {}
46 | for name, element in opt_element.items():
47 | props = {}
48 | if element["by"].lower() == "prop":
49 | for prop in json.loads(element["expression"]):
50 | props[prop["propName"]] = prop["propValue"]
51 | elif element["by"].lower() == "pred":
52 | props["predicate"] = element["expression"]
53 | elif element["by"].lower() == "class":
54 | props["classChain"] = element["expression"]
55 | else:
56 | props[element["by"].lower()] = element["expression"]
57 | elements[name] = props
58 | self.opt_element = elements
59 |
60 | def collect_opt_data(self, ui_data):
61 | opt_data = AppOperationCollector.__parse(ui_data, "operationData")
62 | if opt_data is None or len(opt_data) == 0:
63 | self.opt_data = None
64 | else:
65 | self.opt_data = opt_data
66 |
67 | def collect(self, ui_data):
68 | self.collect_id(ui_data)
69 | self.collect_opt_type(ui_data)
70 | self.collect_opt_system(ui_data)
71 | self.collect_opt_name(ui_data)
72 | self.collect_opt_trans(ui_data)
73 | self.collect_opt_element(ui_data)
74 | self.collect_opt_data(ui_data)
75 | self.collect_opt_code(ui_data)
76 |
77 |
--------------------------------------------------------------------------------
/core/app/device/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from uiautomator2 import Device
3 | from wda import Client, AlertAction, BaseClient
4 |
5 |
6 | class AndroidDriver(Device):
7 | """安卓设备"""
8 | def __call__(self, **kwargs):
9 | if len(kwargs) == 1 and "xpath" in kwargs:
10 | return self.xpath(kwargs["xpath"])
11 | else:
12 | return Device.__call__(self, **kwargs)
13 |
14 | def find_element(self, **kwargs):
15 | if len(kwargs) == 1 and "xpath" in kwargs:
16 | return self.xpath(kwargs["xpath"])
17 | else:
18 | return Device.__call__(self, **kwargs)
19 |
20 |
21 | class AppleDevice(Client):
22 | """苹果设备"""
23 | def session(self,
24 | bundle_id=None,
25 | arguments: Optional[list] = None,
26 | environment: Optional[dict] = None,
27 | alert_action: Optional[AlertAction] = None):
28 | setattr(Client, 'find_element', AppleDevice.find_element)
29 | client = BaseClient.session(self, bundle_id, arguments, environment, alert_action)
30 | return client
31 |
32 | def find_element(self, **kwargs):
33 | return BaseClient.__call__(self, **kwargs)
34 |
35 |
36 | def connect_device(system: str, url: str):
37 | if system.lower() == "android":
38 | return AndroidDriver(url)
39 | else:
40 | return AppleDevice(url)
41 |
42 |
43 | class Operation(object):
44 | def __init__(self, test, device):
45 | self.device = device
46 | self.test = test
47 | self.print = print
48 |
49 | def find_element(self, ele):
50 | """查找单个元素"""
51 | try:
52 | element = self.device.find_element(**ele)
53 | self.test.debugLog("定位元素: %s" % str(ele))
54 | return element
55 | except Exception as e:
56 | self.test.errorLog("定位元素出错: %s" % str(ele))
57 | raise e
58 |
59 |
60 | class ElementNotFoundError(Exception):
61 | """元素获取失败"""
62 |
63 |
64 | class ElementNotDisappearError(Exception):
65 | """元素消失失败"""
66 |
67 |
--------------------------------------------------------------------------------
/core/app/device/assertionOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from uiautomator2 import UiObjectNotFoundError
3 | from core.assertion import LMAssert
4 | from core.app.device import Operation
5 |
6 |
7 | class Assertion(Operation):
8 | """断言类操作"""
9 |
10 | def assert_ele_exists(self, element, assertion, expect):
11 | """断言元素存在"""
12 | try:
13 | actual = self.find_element(element).exists
14 | self.test.debugLog("成功获取元素exists:%s" % str(actual))
15 | except Exception as e:
16 | self.test.errorLog("无法获取元素exists")
17 | raise e
18 | else:
19 | result, msg = LMAssert(assertion, actual, expect).compare()
20 | return result, msg
21 |
22 | def assert_ele_text(self, system, element, assertion, expect):
23 | """断言元素文本"""
24 | try:
25 | if system == "android":
26 | actual = self.find_element(element).get_text()
27 | else:
28 | actual = self.find_element(element).text
29 | self.test.debugLog("成功获取元素text:%s" % str(actual))
30 | except Exception as e:
31 | self.test.errorLog("无法获取元素text")
32 | raise e
33 | else:
34 | result, msg = LMAssert(assertion, actual, expect).compare()
35 | return result, msg
36 |
37 | def assert_ele_attribute(self, element, attribute, assertion, expect):
38 | """断言元素属性"""
39 | try:
40 | actual = self.find_element(element).info[attribute]
41 | self.test.debugLog("成功获取元素%s属性:%s" % (attribute, str(actual)))
42 | except Exception as e:
43 | self.test.errorLog("无法获取元素%s属性" % attribute)
44 | raise e
45 | else:
46 | result, msg = LMAssert(assertion, actual, expect).compare()
47 | return result, msg
48 |
49 | def assert_ele_center(self, system, element, assertion, expect):
50 | """断言元素位置"""
51 | try:
52 | if system == "android":
53 | x, y = self.find_element(element).center()
54 | actual = (x, y)
55 | else:
56 | x, y = self.find_element(element).bounds.center
57 | actual = (x, y)
58 | self.test.debugLog("成功获取元素位置:%s" % str(actual))
59 | except Exception as e:
60 | self.test.errorLog("无法获取元素位置")
61 | raise e
62 | else:
63 | result, msg = LMAssert(assertion, str(actual), expect).compare()
64 | return result, msg
65 |
66 | def assert_ele_x(self, system, element, assertion, expect):
67 | """断言元素X坐标"""
68 | try:
69 | if system == "android":
70 | x, y = self.find_element(element).center()
71 | actual = x
72 | else:
73 | x, y = self.find_element(element).bounds.center
74 | actual = x
75 | self.test.debugLog("成功获取元素X坐标:%s" % str(actual))
76 | except Exception as e:
77 | self.test.errorLog("无法获取元素X坐标")
78 | raise e
79 | else:
80 | result, msg = LMAssert(assertion, actual, expect).compare()
81 | return result, msg
82 |
83 | def assert_ele_y(self, system, element, assertion, expect):
84 | """断言元素Y坐标"""
85 | try:
86 | if system == "android":
87 | x, y = self.find_element(element).center()
88 | actual = y
89 | else:
90 | x, y = self.find_element(element).bounds.center
91 | actual = y
92 | self.test.debugLog("成功获取元素Y坐标:%s" % str(actual))
93 | except Exception as e:
94 | self.test.errorLog("无法获取元素Y坐标")
95 | raise e
96 | else:
97 | result, msg = LMAssert(assertion, actual, expect).compare()
98 | return result, msg
99 |
100 | def assert_alert_exists(self, assertion, expect):
101 | """断言弹框存在 IOS专属"""
102 | try:
103 | actual = self.device.alert.exists
104 | self.test.debugLog("成功获取弹框exists:%s" % str(actual))
105 | except Exception as e:
106 | self.test.errorLog("无法获取弹框exists")
107 | raise e
108 | else:
109 | result, msg = LMAssert(assertion, actual, expect).compare()
110 | return result, msg
111 |
112 | def assert_alert_text(self, assertion, expect):
113 | """断言弹框文本 IOS专属"""
114 | try:
115 | actual = self.device.alert.text
116 | self.test.debugLog("成功获取弹框文本:%s" % str(actual))
117 | except Exception as e:
118 | self.test.errorLog("无法获取弹框文本")
119 | raise e
120 | else:
121 | result, msg = LMAssert(assertion, actual, expect).compare()
122 | return result, msg
123 |
124 | def custom(self, **kwargs):
125 | """自定义"""
126 | code = kwargs["code"]
127 | names = locals()
128 | names["element"] = kwargs["element"]
129 | names["data"] = kwargs["data"]
130 | names["device"] = self.device
131 | names["test"] = self.test
132 | try:
133 | """断言操作需要返回被断言的值 以sys_return(value)返回"""
134 | def print(*args, sep=' ', end='\n', file=None, flush=False):
135 | if file is None or file in (sys.stdout, sys.stderr):
136 | file = names["test"].stdout_buffer
137 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
138 |
139 | def sys_return(res):
140 | names["_exec_result"] = res
141 |
142 | def sys_get(name):
143 | if name in names["test"].context:
144 | return names["test"].context[name]
145 | elif name in names["test"].common_params:
146 | return names["test"].common_params[name]
147 | else:
148 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
149 |
150 | def sys_put(name, val, ps=False):
151 | if ps:
152 | names["test"].common_params[name] = val
153 | else:
154 | names["test"].context[name] = val
155 |
156 | exec(code)
157 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
158 | except UiObjectNotFoundError as e:
159 | raise e
160 | except Exception as e:
161 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
162 | raise e
163 | else:
164 | result, msg = LMAssert(kwargs["data"]["assertion"], names["_exec_result"], kwargs["data"]["expect"]).compare()
165 | return result, msg
166 |
167 |
--------------------------------------------------------------------------------
/core/app/device/conditionOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from uiautomator2 import UiObjectNotFoundError
4 | from core.assertion import LMAssert
5 | from core.app.device import Operation
6 |
7 |
8 | class Condition(Operation):
9 | """条件类操作"""
10 |
11 | def condition_ele_exists(self, element, assertion, expect):
12 | """判断元素存在"""
13 | try:
14 | actual = self.find_element(element).exists
15 | self.test.debugLog("成功获取元素exists:%s" % str(actual))
16 | except Exception as e:
17 | self.test.errorLog("无法获取元素exists")
18 | raise e
19 | else:
20 | result, msg = LMAssert(assertion, actual, expect).compare()
21 | return result, msg
22 |
23 | def condition_ele_text(self, system, element, assertion, expect):
24 | """判断元素文本"""
25 | try:
26 | if system == "android":
27 | actual = self.find_element(element).get_text()
28 | else:
29 | actual = self.find_element(element).text
30 | self.test.debugLog("成功获取元素text:%s" % str(actual))
31 | except Exception as e:
32 | self.test.errorLog("无法获取元素text")
33 | raise e
34 | else:
35 | result, msg = LMAssert(assertion, actual, expect).compare()
36 | return result, msg
37 |
38 | def condition_ele_attribute(self, element, attribute, assertion, expect):
39 | """判断元素属性"""
40 | try:
41 | actual = self.find_element(element).info[attribute]
42 | self.test.debugLog("成功获取元素%s属性:%s" % (attribute, str(actual)))
43 | except Exception as e:
44 | self.test.errorLog("无法获取元素%s属性" % attribute)
45 | raise e
46 | else:
47 | result, msg = LMAssert(assertion, actual, expect).compare()
48 | return result, msg
49 |
50 | def condition_ele_center(self, system, element, assertion, expect):
51 | """判断元素位置"""
52 | try:
53 | if system == "android":
54 | x, y = self.find_element(element).center()
55 | actual = (x, y)
56 | else:
57 | size = self.find_element(element).bounds
58 | actual = (size.x, size.y)
59 | self.test.debugLog("成功获取元素位置:%s" % str(actual))
60 | except Exception as e:
61 | self.test.errorLog("无法获取元素位置")
62 | raise e
63 | else:
64 | result, msg = LMAssert(assertion, str(actual), expect).compare()
65 | return result, msg
66 |
67 | def condition_ele_x(self, system, element, assertion, expect):
68 | """判断元素X坐标"""
69 | try:
70 | if system == "android":
71 | x, y = self.find_element(element).center()
72 | actual = x
73 | else:
74 | x, y = self.find_element(element).bounds.center
75 | actual = x
76 | self.test.debugLog("成功获取元素X坐标:%s" % str(actual))
77 | except Exception as e:
78 | self.test.errorLog("无法获取元素X坐标")
79 | raise e
80 | else:
81 | result, msg = LMAssert(assertion, actual, expect).compare()
82 | return result, msg
83 |
84 | def condition_ele_y(self, system, element, assertion, expect):
85 | """判断元素Y坐标"""
86 | try:
87 | if system == "android":
88 | x, y = self.find_element(element).center()
89 | actual = y
90 | else:
91 | x, y = self.find_element(element).bounds.center
92 | actual = y
93 | self.test.debugLog("成功获取元素Y坐标:%s" % str(actual))
94 | except Exception as e:
95 | self.test.errorLog("无法获取元素Y坐标")
96 | raise e
97 | else:
98 | result, msg = LMAssert(assertion, actual, expect).compare()
99 | return result, msg
100 |
101 | def condition_alert_exists(self, assertion, expect):
102 | """判断弹框存在 IOS专属"""
103 | try:
104 | actual = self.device.alert.exists
105 | self.test.debugLog("成功获取弹框exists:%s" % str(actual))
106 | except Exception as e:
107 | self.test.errorLog("无法获取弹框exists")
108 | raise e
109 | else:
110 | result, msg = LMAssert(assertion, actual, expect).compare()
111 | return result, msg
112 |
113 | def condition_alert_text(self, assertion, expect):
114 | """判断弹框文本 IOS专属"""
115 | try:
116 | actual = self.device.alert.text
117 | self.test.debugLog("成功获取弹框文本:%s" % str(actual))
118 | except Exception as e:
119 | self.test.errorLog("无法获取弹框文本")
120 | raise e
121 | else:
122 | result, msg = LMAssert(assertion, actual, expect).compare()
123 | return result, msg
124 |
125 | def custom(self, **kwargs):
126 | """自定义"""
127 | code = kwargs["code"]
128 | names = locals()
129 | names["element"] = kwargs["element"]
130 | names["data"] = kwargs["data"]
131 | names["device"] = self.device
132 | names["test"] = self.test
133 | try:
134 | """条件操作需要返回被判断的值 以sys_return(value)返回"""
135 | def print(*args, sep=' ', end='\n', file=None, flush=False):
136 | if file is None or file in (sys.stdout, sys.stderr):
137 | file = names["test"].stdout_buffer
138 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
139 |
140 | def sys_return(res):
141 | names["_exec_result"] = res
142 |
143 | def sys_get(name):
144 | if name in names["test"].context:
145 | return names["test"].context[name]
146 | elif name in names["test"].common_params:
147 | return names["test"].common_params[name]
148 | else:
149 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
150 |
151 | def sys_put(name, val, ps=False):
152 | if ps:
153 | names["test"].common_params[name] = val
154 | else:
155 | names["test"].context[name] = val
156 |
157 | exec(code)
158 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
159 | except UiObjectNotFoundError as e:
160 | raise e
161 | except Exception as e:
162 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
163 | raise e
164 | else:
165 | result, msg = LMAssert(kwargs["data"]["assertion"], names["_exec_result"], kwargs["data"]["expect"]).compare()
166 | return result, msg
167 |
168 |
--------------------------------------------------------------------------------
/core/app/device/relationOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from uiautomator2 import UiObjectNotFoundError
4 | from core.app.device import Operation
5 |
6 |
7 | class Relation(Operation):
8 | """关联类操作"""
9 | def get_window_size(self, system, save_name):
10 | """提取屏幕尺寸"""
11 | try:
12 | if system == "android":
13 | w, h = self.device.window_size()
14 | actual = (w, h)
15 | else:
16 | size = self.device.window_size()
17 | actual = (size.width, size.height)
18 | self.test.debugLog("成功获取屏幕尺寸:%s" % str(actual))
19 | except Exception as e:
20 | self.test.errorLog("无法获取屏幕尺寸")
21 | raise e
22 | else:
23 | self.test.context[save_name] = actual
24 |
25 | def get_window_width(self, system, save_name):
26 | """提取屏幕宽度"""
27 | try:
28 | if system == "android":
29 | w, h = self.device.window_size()
30 | actual = w
31 | else:
32 | size = self.device.window_size()
33 | actual = size.width
34 | self.test.debugLog("成功获取屏幕宽度:%s" % str(actual))
35 | except Exception as e:
36 | self.test.errorLog("无法获取屏幕宽度")
37 | raise e
38 | else:
39 | self.test.context[save_name] = actual
40 |
41 | def get_window_height(self, system, save_name):
42 | """提取屏幕高度"""
43 | try:
44 | if system == "android":
45 | w, h = self.device.window_size()
46 | actual = h
47 | else:
48 | size = self.device.window_size()
49 | actual = size.height
50 | self.test.debugLog("成功获取屏幕高度:%s" % str(actual))
51 | except Exception as e:
52 | self.test.errorLog("无法获取屏幕高度")
53 | raise e
54 | else:
55 | self.test.context[save_name] = actual
56 |
57 | def get_ele_text(self, system, element, save_name):
58 | """提取元素文本"""
59 | try:
60 | if system == "android":
61 | actual = self.find_element(element).get_text()
62 | else:
63 | actual = self.find_element(element).text
64 | self.test.debugLog("成功获取元素文本:%s" % str(actual))
65 | except Exception as e:
66 | self.test.errorLog("无法获取元素文本")
67 | raise e
68 | else:
69 | self.test.context[save_name] = actual
70 |
71 | def get_ele_center(self, system, element, save_name):
72 | """提取元素位置"""
73 | try:
74 | if system == "android":
75 | x, y = self.find_element(element).center()
76 | actual = (x, y)
77 | else:
78 | x, y = self.find_element(element).bounds.center
79 | actual = (x, y)
80 | self.test.debugLog("成功获取元素位置:%s" % str(actual))
81 | except Exception as e:
82 | self.test.errorLog("无法获取元素位置")
83 | raise e
84 | else:
85 | self.test.context[save_name] = actual
86 |
87 | def get_ele_x(self, system, element, save_name):
88 | """提取元素X坐标"""
89 | try:
90 | if system == "android":
91 | x, y = self.find_element(element).center()
92 | actual = x
93 | else:
94 | x, y = self.find_element(element).bounds.center
95 | actual = x
96 | self.test.debugLog("成功获取元素X坐标:%s" % str(actual))
97 | except Exception as e:
98 | self.test.errorLog("无法获取元素X坐标")
99 | raise e
100 | else:
101 | self.test.context[save_name] = actual
102 |
103 | def get_ele_y(self, system, element, save_name):
104 | """提取元素Y坐标"""
105 | try:
106 | if system == "android":
107 | x, y = self.find_element(element).center()
108 | actual = y
109 | else:
110 | x, y = self.find_element(element).bounds.center
111 | actual = y
112 | self.test.debugLog("成功获取元素Y坐标:%s" % str(actual))
113 | except Exception as e:
114 | self.test.errorLog("无法获取元素Y坐标")
115 | raise e
116 | else:
117 | self.test.context[save_name] = actual
118 |
119 | def get_alert_text(self, save_name):
120 | """提取弹框文本 IOS专属"""
121 | try:
122 | actual = self.device.alert.text
123 | self.test.debugLog("成功获取弹框文本:%s" % str(actual))
124 | except Exception as e:
125 | self.test.errorLog("无法获取弹框文本")
126 | raise e
127 | else:
128 | self.test.context[save_name] = actual
129 |
130 | def custom(self, **kwargs):
131 | """自定义"""
132 | code = kwargs["code"]
133 | names = locals()
134 | names["element"] = kwargs["element"]
135 | names["data"] = kwargs["data"]
136 | names["device"] = self.device
137 | names["test"] = self.test
138 | try:
139 | """关联操作需要返回被关联的值 以sys_return(value)返回"""
140 | def print(*args, sep=' ', end='\n', file=None, flush=False):
141 | if file is None or file in (sys.stdout, sys.stderr):
142 | file = names["test"].stdout_buffer
143 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
144 |
145 | def sys_return(res):
146 | names["_exec_result"] = res
147 |
148 | def sys_get(name):
149 | if name in names["test"].context:
150 | return names["test"].context[name]
151 | elif name in names["test"].common_params:
152 | return names["test"].common_params[name]
153 | else:
154 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
155 |
156 | def sys_put(name, val, ps=False):
157 | if ps:
158 | names["test"].common_params[name] = val
159 | else:
160 | names["test"].context[name] = val
161 |
162 | exec(code)
163 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
164 | except UiObjectNotFoundError as e:
165 | raise e
166 | except Exception as e:
167 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
168 | raise e
169 | else:
170 | self.test.context[kwargs["data"]["save_name"]] = names["_exec_result"]
171 |
172 |
--------------------------------------------------------------------------------
/core/app/device/scenarioOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from uiautomator2 import UiObjectNotFoundError
4 | from core.app.device import Operation
5 |
6 |
7 | class Scenario(Operation):
8 | """场景类操作"""
9 |
10 | def custom(self, **kwargs):
11 | """自定义"""
12 | code = kwargs["code"]
13 | names = locals()
14 | names["element"] = kwargs["element"]
15 | names["data"] = kwargs["data"]
16 | names["device"] = self.device
17 | names["test"] = self.test
18 | try:
19 | def print(*args, sep=' ', end='\n', file=None, flush=False):
20 | if file is None or file in (sys.stdout, sys.stderr):
21 | file = names["test"].stdout_buffer
22 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
23 |
24 | def sys_get(name):
25 | if name in names["test"].context:
26 | return names["test"].context[name]
27 | elif name in names["test"].common_params:
28 | return names["test"].common_params[name]
29 | else:
30 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
31 |
32 | def sys_put(name, val, ps=False):
33 | if ps:
34 | names["test"].common_params[name] = val
35 | else:
36 | names["test"].context[name] = val
37 |
38 | exec(code)
39 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
40 | except UiObjectNotFoundError as e:
41 | raise e
42 | except Exception as e:
43 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
44 | raise e
45 |
--------------------------------------------------------------------------------
/core/app/device/systemOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from time import sleep
3 |
4 | from uiautomator2 import UiObjectNotFoundError
5 | from wda import WDAElementNotFoundError
6 | from core.app.device import Operation
7 |
8 |
9 | class System(Operation):
10 | """系统操作"""
11 |
12 | def start_app(self, app_id):
13 | """启动应用"""
14 | try:
15 | self.device.app_start(app_id)
16 | self.test.debugLog("成功执行启动应用")
17 | except Exception as e:
18 | self.test.errorLog("无法执行关闭应用")
19 | raise e
20 |
21 | def close_app(self, app_id):
22 | """关闭应用"""
23 | try:
24 | self.device.app_stop(app_id)
25 | self.test.debugLog("成功执行关闭应用")
26 | except Exception as e:
27 | self.test.errorLog("无法执行关闭应用")
28 | raise e
29 |
30 | def swipe_left(self, system):
31 | """左滑"""
32 | try:
33 | if system == "android":
34 | self.device.swipe_ext("left")
35 | else:
36 | self.device.swipe_left()
37 | self.test.debugLog("成功执行左滑")
38 | except Exception as e:
39 | self.test.errorLog("无法执行左滑")
40 | raise e
41 |
42 | def swipe_right(self, system):
43 | """右滑"""
44 | try:
45 | if system == "android":
46 | self.device.swipe_ext("right")
47 | else:
48 | self.device.swipe_right()
49 | self.test.debugLog("成功执行右滑")
50 | except Exception as e:
51 | self.test.errorLog("无法执行右滑")
52 | raise e
53 |
54 | def swipe_up(self, system):
55 | """上滑"""
56 | try:
57 | if system == "android":
58 | self.device.swipe_ext("up")
59 | else:
60 | self.device.swipe_up()
61 | self.test.debugLog("成功执行上滑")
62 | except Exception as e:
63 | self.test.errorLog("无法执行上滑")
64 | raise e
65 |
66 | def swipe_down(self, system):
67 | """下滑"""
68 | try:
69 | if system == "android":
70 | self.device.swipe_ext("down")
71 | else:
72 | self.device.swipe_down()
73 | self.test.debugLog("成功执行下滑")
74 | except Exception as e:
75 | self.test.errorLog("无法执行下滑")
76 | raise e
77 |
78 | def home(self, system):
79 | """系统首页"""
80 | try:
81 | if system == "android":
82 | self.device.keyevent("home")
83 | else:
84 | self.device.home()
85 | self.test.debugLog("成功执行返回系统首页")
86 | except Exception as e:
87 | self.test.errorLog("无法执行返回系统首页")
88 | raise e
89 |
90 | def back(self):
91 | """系统返回 安卓专用"""
92 | try:
93 | self.device.keyevent("back")
94 | self.test.debugLog("成功执行返回")
95 | except Exception as e:
96 | self.test.errorLog("无法执行返回")
97 | raise e
98 |
99 | def press(self, keycode):
100 | """系统按键"""
101 | try:
102 | self.device.press(keycode)
103 | self.test.debugLog("成功执行按下系统键位: %s" % keycode)
104 | except Exception as e:
105 | self.test.errorLog("无法执行按下系统键位: %s" % keycode)
106 | raise e
107 |
108 | def screenshot(self, name):
109 | """屏幕截图"""
110 | try:
111 | screenshot = self.device.screenshot(format='raw')
112 | self.test.saveScreenShot(name, screenshot)
113 | self.test.debugLog("成功执行屏幕截图")
114 | except Exception as e:
115 | self.test.errorLog("无法执行屏幕截图")
116 | raise e
117 |
118 | def screen_on(self, system):
119 | """亮屏"""
120 | try:
121 | if system == "android":
122 | self.device.screen_on()
123 | else:
124 | self.device.unlock()
125 | self.test.debugLog("成功执行亮屏")
126 | except Exception as e:
127 | self.test.errorLog("无法执行亮屏")
128 | raise e
129 |
130 | def screen_off(self, system):
131 | """息屏"""
132 | try:
133 | if system == "android":
134 | self.device.screen_off()
135 | else:
136 | self.device.lock()
137 | self.test.debugLog("成功执行息屏")
138 | except Exception as e:
139 | self.test.errorLog("无法执行息屏")
140 | raise e
141 |
142 | def sleep(self, second):
143 | """强制等待"""
144 | try:
145 | sleep(second)
146 | self.test.debugLog("成功执行sleep %ds" % second)
147 | except Exception as e:
148 | self.test.errorLog("无法执行sleep %ds" % second)
149 | raise e
150 |
151 | def implicitly_wait(self, second):
152 | """隐式等待"""
153 | try:
154 | self.device.implicitly_wait(second)
155 | self.test.debugLog("成功执行implicitly wait %ds" % second)
156 | except Exception as e:
157 | self.test.errorLog("无法执行implicitly wait %ds" % second)
158 | raise e
159 |
160 | def custom(self, **kwargs):
161 | """自定义"""
162 | code = kwargs["code"]
163 | names = locals()
164 | names["element"] = kwargs["element"]
165 | names["data"] = kwargs["data"]
166 | names["device"] = self.device
167 | names["test"] = self.test
168 | try:
169 | def print(*args, sep=' ', end='\n', file=None, flush=False):
170 | if file is None or file in (sys.stdout, sys.stderr):
171 | file = names["test"].stdout_buffer
172 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
173 |
174 | def sys_get(name):
175 | if name in names["test"].context:
176 | return names["test"].context[name]
177 | elif name in names["test"].common_params:
178 | return names["test"].common_params[name]
179 | else:
180 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
181 |
182 | def sys_put(name, val, ps=False):
183 | if ps:
184 | names["test"].common_params[name] = val
185 | else:
186 | names["test"].context[name] = val
187 |
188 | exec(code)
189 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
190 | except UiObjectNotFoundError as e:
191 | raise e
192 | except WDAElementNotFoundError as e:
193 | raise e
194 | except Exception as e:
195 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
196 | raise e
197 |
--------------------------------------------------------------------------------
/core/app/device/viewOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from uiautomator2 import UiObjectNotFoundError
4 | from uiautomator2.xpath import XPath
5 | from wda import WDAElementNotFoundError
6 | from core.app.device import Operation, ElementNotFoundError, ElementNotDisappearError
7 |
8 |
9 | class View(Operation):
10 | """视图类操作"""
11 |
12 | def click(self, element):
13 | """单击"""
14 | try:
15 | self.find_element(element).click_exists(timeout=3)
16 | self.test.debugLog("成功单击")
17 | except Exception as e:
18 | self.test.errorLog("无法单击")
19 | raise e
20 |
21 | def double_click(self, system, element):
22 | """双击"""
23 | try:
24 | if system == "android":
25 | self.device.double_click(*self.find_element(element).center())
26 | else:
27 | self.device.double_tap(*self.find_element(element).center())
28 | self.test.debugLog("成功双击")
29 | except Exception as e:
30 | self.test.errorLog("无法双击")
31 | raise e
32 |
33 | def long_click(self, system, element, second):
34 | """长按"""
35 | try:
36 | if system == "android":
37 | if "xpath" in element:
38 | self.find_element(element).long_click()
39 | else:
40 | self.find_element(element).long_click(second)
41 | else:
42 | self.find_element(element).tap_hold(second)
43 | self.test.debugLog("成功长按%sS" % str(second))
44 | except Exception as e:
45 | self.test.errorLog("无法长按%sS" % str(second))
46 | raise e
47 |
48 | def click_coord(self, x, y):
49 | """坐标单击 百分比或坐标值"""
50 | try:
51 | self.device.click(x, y)
52 | self.test.debugLog("成功坐标单击")
53 | except Exception as e:
54 | self.test.errorLog("无法坐标单击")
55 | raise e
56 |
57 | def double_click_coord(self, system, x, y):
58 | """坐标双击 百分比或坐标值"""
59 | try:
60 | if system == "android":
61 | self.device.double_click(x, y)
62 | else:
63 | self.device.double_tap(x, y)
64 | self.test.debugLog("成功坐标双击")
65 | except Exception as e:
66 | self.test.errorLog("无法坐标双击")
67 | raise e
68 |
69 | def long_click_coord(self, system, x, y, second):
70 | """坐标长按 百分比或坐标值"""
71 | try:
72 | if system == "android":
73 | self.device.long_click(x, y, second)
74 | else:
75 | self.device.tap_hold(x, y, second)
76 | self.test.debugLog("成功坐标长按%sS" % str(second))
77 | except Exception as e:
78 | self.test.errorLog("无法坐标长按%sS" % str(second))
79 | raise e
80 |
81 | def swipe(self, system, fx, fy, tx, ty, duration=None):
82 | """坐标滑动 百分比或坐标值"""
83 | try:
84 | if system == "android":
85 | if duration == "":
86 | duration = None
87 | self.device.swipe(fx, fy, tx, ty, duration)
88 | else:
89 | if duration == "" or duration is None:
90 | duration = 0
91 | self.device.swipe(fx, fy, tx, ty, duration)
92 | self.test.debugLog("成功执行滑动")
93 | except Exception as e:
94 | self.test.errorLog("无法执行滑动")
95 | raise e
96 |
97 | def input_text(self, element, text):
98 | """输入"""
99 | try:
100 | self.find_element(element).set_text(text)
101 | self.test.debugLog("成功输入%s" % str(text))
102 | except Exception as e:
103 | self.test.errorLog("无法输入%s" % str(text))
104 | raise e
105 |
106 | def clear_text(self, system, element):
107 | """清空"""
108 | try:
109 | ele = self.find_element(element)
110 | if system == "android" and len(element) == 1 and "xpath" in element:
111 | xe = ele.get()
112 | ele._d.set_fastinput_ime()
113 | xe.click()
114 | ele._parent._d.set_fastinput_ime()
115 | ele._parent._d.clear_text()
116 | else:
117 | ele.clear_text()
118 | self.test.debugLog("成功清空")
119 | except Exception as e:
120 | self.test.errorLog("无法清空")
121 | raise e
122 |
123 | def scroll_to_ele(self, system, element, direction):
124 | """滑动到元素出现"""
125 | try:
126 | if system == "android":
127 | if "xpath" in element:
128 | XPath(self.device).scroll_to(element["xpath"], direction)
129 | elif direction == "up":
130 | self.device(scrollable=True).forward.to(**element)
131 | elif direction == "down":
132 | self.device(scrollable=True).backward.to(**element)
133 | elif direction == "left":
134 | self.device(scrollable=True).horiz.forward.to(**element)
135 | else:
136 | self.device(scrollable=True).horiz.backward.to(**element)
137 | else:
138 | self.find_element(element).scroll(direction)
139 | self.test.debugLog("成功滑动到元素出现")
140 | except Exception as e:
141 | self.test.errorLog("无法滑动到元素出现")
142 | raise e
143 |
144 | def pinch_in(self, system, element):
145 | """缩小 安卓仅支持属性定位"""
146 | try:
147 | if system == "android":
148 | self.find_element(element).pinch_in()
149 | else:
150 | self.find_element(element).pinch(0.5, -1)
151 | self.test.debugLog("成功缩小")
152 | except Exception as e:
153 | self.test.errorLog("无法缩小")
154 | raise e
155 |
156 | def pinch_out(self, system, element):
157 | """放大 安卓仅支持属性定位"""
158 | try:
159 | if system == "android":
160 | self.find_element(element).pinch_out()
161 | else:
162 | self.find_element(element).pinch(2, 1)
163 | self.test.debugLog("成功放大")
164 | except Exception as e:
165 | self.test.errorLog("无法放大")
166 | raise e
167 |
168 | def wait(self, element, second):
169 | """等待元素出现"""
170 | try:
171 | if self.find_element(element).wait(timeout=second):
172 | self.test.debugLog("成功等待元素出现")
173 | else:
174 | self.test.errorLog("等待元素出现失败 元素不存在")
175 | raise ElementNotFoundError("element not exists")
176 | except ElementNotFoundError as e:
177 | raise e
178 | except Exception as e:
179 | self.test.errorLog("无法等待元素出现")
180 | raise e
181 |
182 | def wait_gone(self, system, element, second):
183 | """等待元素消失"""
184 | try:
185 | if system == "android":
186 | res = self.find_element(element).wait_gone(timeout=second)
187 | else:
188 | res = self.find_element(element).wait_gone(timeout=second, raise_error=False)
189 | if res:
190 | self.test.debugLog("成功等待元素消失")
191 | else:
192 | self.test.errorLog("等待元素消失失败 元素仍存在")
193 | raise ElementNotDisappearError("element exists")
194 | except ElementNotDisappearError as e:
195 | raise e
196 | except Exception as e:
197 | self.test.errorLog("无法等待元素消失")
198 | raise e
199 |
200 | def drag_to_ele(self, start_element, end_element):
201 | """拖动到元素 安卓专属 只支持属性定位"""
202 | try:
203 | self.find_element(start_element).drag_to(**end_element)
204 | self.test.debugLog("成功拖动到元素")
205 | except Exception as e:
206 | self.test.errorLog("无法拖动到元素")
207 | raise e
208 |
209 | def drag_to_coord(self, element, x, y):
210 | """拖动到坐标 安卓专属 只支持属性定位"""
211 | try:
212 | self.find_element(element).drag_to(x, y)
213 | self.test.debugLog("成功拖动到坐标")
214 | except Exception as e:
215 | self.test.errorLog("无法拖动到坐标")
216 | raise e
217 |
218 | def drag_coord(self, fx, fy, tx, ty):
219 | """坐标拖动 安卓专属"""
220 | try:
221 | self.device.drag(fx, fy, tx, ty)
222 | self.test.debugLog("成功坐标拖动")
223 | except Exception as e:
224 | self.test.errorLog("无法坐标拖动")
225 | raise e
226 |
227 | def swipe_ele(self, element, direction):
228 | """元素内滑动 安卓专属"""
229 | try:
230 | self.find_element(element).swipe(direction)
231 | self.test.debugLog("成功元素内滑动")
232 | except Exception as e:
233 | self.test.errorLog("无法元素内滑动")
234 | raise e
235 |
236 | def alert_wait(self, second):
237 | """等待弹框出现 IOS专属"""
238 | try:
239 | self.device.alert.wait(second)
240 | self.test.debugLog("成功等待弹框出现")
241 | except Exception as e:
242 | self.test.errorLog("无法等待弹框出现")
243 | raise e
244 |
245 | def alert_accept(self):
246 | """弹框确认 IOS专属"""
247 | try:
248 | self.device.alert.accept()
249 | self.test.debugLog("成功弹框确认")
250 | except Exception as e:
251 | self.test.errorLog("无法弹框确认")
252 | raise e
253 |
254 | def alert_dismiss(self):
255 | """弹框取消 IOS专属"""
256 | try:
257 | self.device.alert.dismiss()
258 | self.test.debugLog("成功弹框取消")
259 | except Exception as e:
260 | self.test.errorLog("无法弹框取消")
261 | raise e
262 |
263 | def alert_click(self, name):
264 | """弹框点击 IOS专属"""
265 | try:
266 | self.device.alert.click(name)
267 | self.test.debugLog("成功弹框点击%s" % name)
268 | except Exception as e:
269 | self.test.errorLog("无法弹框点击%s" % name)
270 | raise e
271 |
272 | def custom(self, **kwargs):
273 | """自定义"""
274 | code = kwargs["code"]
275 | names = locals()
276 | names["element"] = kwargs["element"]
277 | names["data"] = kwargs["data"]
278 | names["device"] = self.device
279 | names["test"] = self.test
280 | try:
281 | def print(*args, sep=' ', end='\n', file=None, flush=False):
282 | if file is None or file in (sys.stdout, sys.stderr):
283 | file = names["test"].stdout_buffer
284 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
285 |
286 | def sys_get(name):
287 | if name in names["test"].context:
288 | return names["test"].context[name]
289 | elif name in names["test"].common_params:
290 | return names["test"].common_params[name]
291 | else:
292 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
293 |
294 | def sys_put(name, val, ps=False):
295 | if ps:
296 | names["test"].common_params[name] = val
297 | else:
298 | names["test"].context[name] = val
299 |
300 | exec(code)
301 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
302 | except UiObjectNotFoundError as e:
303 | raise e
304 | except WDAElementNotFoundError as e:
305 | raise e
306 | except Exception as e:
307 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
308 | raise e
309 |
--------------------------------------------------------------------------------
/core/app/testcase.py:
--------------------------------------------------------------------------------
1 | from core.template import Template
2 | from core.app.collector import AppOperationCollector
3 | from core.app.teststep import AppTestStep
4 | from core.app.device import connect_device
5 | from tools.utils.utils import get_case_message, handle_operation_data, handle_params_data
6 | import re
7 |
8 |
9 | class AppTestCase:
10 | def __init__(self, test):
11 | self.test = test
12 | self.context = test.context
13 | self.case_message = get_case_message(test.test_data)
14 | self.id = self.case_message['caseId']
15 | self.name = self.case_message['caseName']
16 | setattr(test, 'test_case_name', self.case_message['caseName'])
17 | setattr(test, 'test_case_desc', self.case_message['comment'])
18 | self.functions = self.case_message['functions']
19 | self.params = handle_params_data(self.case_message['params'])
20 | test.common_params = self.params
21 | self.device = self.before_execute()
22 | self.template = Template(self.test, self.context, self.functions, self.params)
23 | self.comp = re.compile(r"\{\{.*?\}\}")
24 |
25 | def execute(self):
26 | if self.case_message['optList'] is None:
27 | self.after_execute()
28 | raise RuntimeError("无法获取APP测试相关数据, 请重试!!!")
29 | try:
30 | self.loop_execute(self.case_message['optList'], [])
31 | finally:
32 | self.after_execute()
33 |
34 | def loop_execute(self, opt_list, skip_opts, step_n=0):
35 | while step_n < len(opt_list):
36 | opt_content = opt_list[step_n]
37 | # 定义收集器
38 | collector = AppOperationCollector()
39 | step = AppTestStep(self.test, self.device, self.context, collector)
40 | # 定义事务
41 | self.test.defineTrans(opt_content["operationId"], opt_content['operationTrans'],
42 | self.get_opt_content(opt_content['operationElement']), opt_content['operationDesc'])
43 | if step_n in skip_opts:
44 | self.test.updateTransStatus(3)
45 | self.test.debugLog('[{}]操作在条件控制之外不被执行'.format(opt_content['operationTrans']))
46 | step_n += 1
47 | continue
48 | # 收集步骤信息
49 | step.collector.collect(opt_content)
50 | try:
51 | if step.collector.opt_type == "looper":
52 | looper_step_num = step.looper_controller(self, opt_list, step_n)
53 | step_n += looper_step_num + 1
54 | else:
55 | # 渲染主体
56 | self.render_content(step)
57 | step.execute()
58 | step.assert_controller()
59 | skip_opts.extend(step.condition_controller(step_n))
60 | step_n += 1
61 | except Exception as e:
62 | if not isinstance(e, AssertionError):
63 | self.test.saveScreenShot(opt_content['operationTrans'], self.device.screenshot(format='raw'))
64 | raise e
65 |
66 | @staticmethod
67 | def get_opt_content(elements):
68 | content = ""
69 | if elements is not None:
70 | for key, element in elements.items():
71 | content = "%s\n %s: %s" % (content, key, element["target"])
72 | return content
73 |
74 | def before_execute(self):
75 | if self.case_message['deviceUrl'] is None:
76 | raise Exception("执行设备不在线 本用例执行失败")
77 | device = connect_device(self.case_message['deviceSystem'], f"http://{self.case_message['deviceUrl']}")
78 | if self.case_message['deviceSystem'] == 'android':
79 | device.healthcheck()
80 | device.app_start(self.case_message['appId'], self.case_message['activity'])
81 | return device
82 | else:
83 | device = device.session(self.case_message['appId'])
84 | device._wda_url = f"http://{self.case_message['deviceUrl']}"
85 | return device
86 |
87 | def after_execute(self):
88 | self.device.app_stop(self.case_message['appId'])
89 |
90 | def render_looper(self, looper):
91 | self.template.init(looper)
92 | _looper = self.template.render()
93 | for name, param in _looper.items():
94 | if name != "target" or name != "expect": # 断言实际值不作数据处理
95 | _looper[name] = handle_operation_data(param["type"], param["value"])
96 | if "times" in _looper:
97 | try:
98 | times = int(_looper["times"])
99 | except:
100 | times = 1
101 | _looper["times"] = times
102 | return _looper
103 |
104 | def render_content(self, step):
105 | if step.collector.opt_element is not None:
106 | for name, expression in step.collector.opt_element.items():
107 | if self.comp.search(str(expression)) is not None:
108 | self.template.init(expression)
109 | expression = self.template.render()
110 | step.collector.opt_element[name] = expression
111 | if step.collector.opt_data is not None:
112 | data = {}
113 | for name, param in step.collector.opt_data.items():
114 | param_value = param["value"]
115 | if isinstance(param_value, str) and self.comp.search(param_value) is not None:
116 | self.template.init(param_value)
117 | param_value = self.template.render()
118 | data[name] = handle_operation_data(param["type"], param_value)
119 | step.collector.opt_data = data
120 |
121 |
--------------------------------------------------------------------------------
/core/app/teststep.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from datetime import datetime
3 | from core.app.find_opt import *
4 | from core.assertion import LMAssert
5 |
6 |
7 | class AppTestStep:
8 | def __init__(self, test, device, context, collector):
9 | self.test = test
10 | self.device = device
11 | self.context = context
12 | self.collector = collector
13 | self.result = None
14 |
15 | def execute(self):
16 | try:
17 | self.test.debugLog('APP操作[{}]开始'.format(self.collector.opt_name))
18 | opt_type = self.collector.opt_type
19 | if opt_type == "system":
20 | func = find_system_opt(self.collector.opt_name)
21 | elif opt_type == "view":
22 | func = find_view_opt(self.collector.opt_name)
23 | elif opt_type == "condition":
24 | func = find_condition_opt(self.collector.opt_name)
25 | elif opt_type == "assertion":
26 | func = find_assertion_opt(self.collector.opt_name)
27 | elif opt_type == "relation":
28 | func = find_relation_opt(self.collector.opt_name)
29 | else:
30 | func = find_scenario_opt(self.collector.opt_name)
31 | if func is None:
32 | raise NotExistedAppOperation("未定义操作")
33 | opt_content = {
34 | "system": self.collector.opt_system,
35 | "trans": self.collector.opt_trans,
36 | "code": self.collector.opt_code,
37 | "element": self.collector.opt_element,
38 | "data": self.collector.opt_data
39 | }
40 | self.result = func(self.test, self.device, **opt_content)
41 | self.log_show()
42 | finally:
43 | self.test.debugLog('APP操作[{}]结束'.format(self.collector.opt_name))
44 |
45 | def looper_controller(self, case, opt_list, step_n):
46 | """循环控制器"""
47 | if self.collector.opt_trans == "While循环":
48 | loop_start_time = datetime.now()
49 | timeout = int(self.collector.opt_data["timeout"]["value"])
50 | index_name = self.collector.opt_data["indexName"]["value"]
51 | steps = int(self.collector.opt_data["steps"]["value"])
52 | index = 0
53 | while timeout == 0 or (datetime.now() - loop_start_time).seconds * 1000 < timeout:
54 | # timeout为0时可能会死循环 慎重选择
55 | self.context[index_name] = index # 给循环索引赋值第几次循环 母循环和子循环的索引名不应一样
56 | _looper = case.render_looper(self.collector.opt_data) # 渲染循环控制控制器 每次循环都需要渲染
57 | index += 1
58 | result, _ = LMAssert(_looper['assertion'], _looper['target'], _looper['expect']).compare()
59 | if not result:
60 | break
61 | _opt_list = opt_list[step_n+1: (step_n + _looper["steps"]+1)] # 循环操作本身不参与循环 不然死循环
62 | case.loop_execute(_opt_list, [])
63 | return steps
64 | else:
65 | _looper = case.render_looper(self.collector.opt_data) # 渲染循环控制控制器 for只需渲染一次
66 | for index in range(_looper["times"]): # 本次循环次数
67 | self.context[_looper["indexName"]] = index # 给循环索引赋值第几次循环 母循环和子循环的索引名不应一样
68 | _opt_list = opt_list[step_n+1: (step_n + _looper["steps"]+1)]
69 | case.loop_execute(_opt_list, [])
70 | return _looper["steps"]
71 |
72 | def assert_controller(self):
73 | if self.collector.opt_type == "assertion":
74 | if self.result[0]:
75 | self.test.debugLog('[{}]断言成功: {}'.format(self.collector.opt_trans,
76 | self.result[1]))
77 | else:
78 | self.test.errorLog('[{}]断言失败: {}'.format(self.collector.opt_trans,
79 | self.result[1]))
80 | self.test.saveScreenShot(self.collector.opt_trans, self.device.screenshot(format='raw'))
81 | if "continue" in self.collector.opt_data and self.collector.opt_data["continue"] is True:
82 | try:
83 | raise AssertionError(self.result[1])
84 | except AssertionError:
85 | error_info = sys.exc_info()
86 | self.test.recordFailStatus(error_info)
87 | else:
88 | raise AssertionError(self.result[1])
89 |
90 | def condition_controller(self, current):
91 | if self.collector.opt_type == "condition":
92 | offset_true = self.collector.opt_data["true"]
93 | if not isinstance(offset_true, int):
94 | offset_true = 0
95 | offset_false = self.collector.opt_data["false"]
96 | if not isinstance(offset_false, int):
97 | offset_false = 0
98 | if self.result[0]:
99 | self.test.debugLog('[{}]判断成功, 执行成功分支: {}'.format(self.collector.opt_name,
100 | self.result[1]))
101 | return [current + i for i in range(offset_true + 1, offset_true + offset_false + 1)]
102 | else:
103 | self.test.errorLog('[{}]判断失败, 执行失败分支: {}'.format(self.collector.opt_name,
104 | self.result[1]))
105 | return [current + i for i in range(1, offset_true + 1)]
106 | return []
107 |
108 | def log_show(self):
109 | msg = ""
110 | if self.collector.opt_element is not None:
111 | for k, v in self.collector.opt_element.items():
112 | msg += '元素定位: {}: {}
'.format(k, v)
113 | if self.collector.opt_data is not None:
114 | data_log = '{'
115 | for k, v in self.collector.opt_data.items():
116 | class_name = type(v).__name__
117 | data_log += "{}: {}, ".format(k, v)
118 | if len(data_log) > 1:
119 | data_log = data_log[:-2]
120 | data_log += '}'
121 | msg += '操作数据: {}'.format(data_log)
122 | if msg != "":
123 | msg = '操作信息:
' + msg
124 | self.test.debugLog(msg)
125 |
126 |
127 | class NotExistedAppOperation(Exception):
128 | """未定义的操作"""
129 |
--------------------------------------------------------------------------------
/core/template.py:
--------------------------------------------------------------------------------
1 | from functools import reduce
2 | from hashlib import md5
3 | from jsonpath import jsonpath
4 | from jsonpath_ng.parser import JsonPathParser
5 | from tools.funclib import get_func_lib
6 | import json
7 | import re
8 | import time
9 |
10 | from tools.utils.utils import extract_by_jsonpath, quotation_marks
11 |
12 |
13 | class Template:
14 |
15 | def __init__(self, test, context, functions, params, variable_start_string='{{', variable_end_string='}}', function_prefix='@', param_prefix='$'):
16 | self.test = test
17 | self.param_prefix = param_prefix
18 | self.data = None
19 | self.context = context # 关联参数
20 | self.params = params # 公共参数
21 | self.variable_start_string = variable_start_string
22 | self.variable_end_string = variable_end_string
23 | self.function_prefix = function_prefix
24 | self.param_prefix = param_prefix
25 | self.stack = list()
26 | # 动态存储接口的请求信息 以便渲染
27 | self.request_url = None
28 | self.request_path = None
29 | self.request_headers = None
30 | self.request_query = None
31 | self.request_body = None
32 | self.func_lib = get_func_lib(test, functions, self.context, self.params)
33 | self.bytes_map = dict()
34 | self.parser = JsonPathParser()
35 |
36 | def init(self, data):
37 | self.data = json.dumps(data, ensure_ascii=False)
38 | self.stack.clear()
39 | self.bytes_map.clear()
40 |
41 | def set_help_data(self, url, path: str, headers: dict, query: dict, body: dict):
42 | self.request_url = url
43 | self.request_path = path
44 | self.request_headers = headers
45 | self.request_query = query
46 | self.request_body = body
47 |
48 | def render(self):
49 | start_stack = list()
50 | start_length = len(self.variable_start_string)
51 | end_length = len(self.variable_end_string)
52 | top = 0
53 | flag = False
54 | for cur in range(len(self.data)):
55 | self.stack.append(self.data[cur])
56 | top += 1
57 | if flag:
58 | self.stack.pop()
59 | top -= 1
60 | flag = False
61 | continue
62 | if reduce(lambda x, y: x + y, self.stack[-start_length:]) == self.variable_start_string:
63 | start_stack.append(top - start_length)
64 | if reduce(lambda x, y: x + y, self.stack[-end_length:]) == self.variable_end_string:
65 | if len(start_stack) == 0:
66 | continue
67 | recent = start_stack.pop()
68 | tmp = ''
69 | for _ in range(top - recent):
70 | tmp += self.stack.pop()
71 | top -= 1
72 | if self.stack[-1] == '"' and self.data[cur + 1] == '"':
73 | self.stack.pop()
74 | top -= 1
75 | flag = True
76 | else:
77 | flag = False
78 | tmp = tmp[::-1]
79 | key = tmp[start_length:-end_length].strip()
80 | key, json_path = self.split_key(key)
81 | try:
82 | if key.startswith(self.function_prefix):
83 | name_args = self.split_func(key, self.function_prefix)
84 | value = self.func_lib(name_args[0], *name_args[1:])
85 | elif key in self.context: # 优先从关联参数中取
86 | if json_path is None:
87 | value = self.context.get(key)
88 | else:
89 | value = extract_by_jsonpath(self.context.get(key), json_path)
90 | elif key in self.params:
91 | if json_path is None:
92 | value = self.params.get(key)
93 | else:
94 | value = extract_by_jsonpath(self.params.get(key), json_path)
95 | elif key.startswith(self.param_prefix) and key[1:] in self.params: # 兼容老版本
96 | if json_path is None:
97 | value = self.params.get(key[1:])
98 | else:
99 | value = extract_by_jsonpath(self.params.get(key[1:]), json_path)
100 | else:
101 | value = tmp
102 | except:
103 | value = tmp
104 | print('不存在的公共参数、关联变量或内置函数: {}'.format(key), file=self.test.stdout_buffer)
105 |
106 | if not flag and isinstance(value, str):
107 | if '"' in value and value != tmp:
108 | value = json.dumps(value)[1:-1]
109 | final_value = value
110 | elif isinstance(value, bytes):
111 | final_value = self._bytes_save(value, flag)
112 | elif isinstance(value, list):
113 | final_value = list()
114 | for list_item in value:
115 | if isinstance(list_item, bytes):
116 | final_value.append(self._bytes_save(list_item, False))
117 | else:
118 | final_value.append(list_item)
119 | final_value = json.dumps(final_value)
120 | else:
121 | if value == tmp and isinstance(value, str):
122 | final_value = '"'+value+'"'
123 | else:
124 | final_value = json.dumps(value)
125 | for s in final_value:
126 | self.stack.append(s)
127 | top += 1
128 | res = json.loads(reduce(lambda x, y: x + y, self.stack))
129 |
130 | if len(self.bytes_map) > 0:
131 | pattern = r'#\{(bytes_\w+_\d+?)\}'
132 | if isinstance(res, str):
133 | bytes_value = self._bytes_slove(res, pattern)
134 | if bytes_value is not None:
135 | res = bytes_value
136 | elif isinstance(res, dict) or isinstance(res, list):
137 | for i, j in zip(jsonpath(res, '$..'), jsonpath(res, '$..', result_type='PATH')):
138 | if isinstance(i, str):
139 | bytes_value = self._bytes_slove(i, pattern)
140 | if bytes_value is not None:
141 | expression = self.parser.parse(j)
142 | expression.update(res, bytes_value)
143 | return res
144 |
145 | def _bytes_save(self, value, flag):
146 | bytes_map_key = 'bytes_{}_{}'.format(md5(value).hexdigest(), int(time.time() * 1000000000))
147 | self.bytes_map[bytes_map_key] = value
148 | change_value = '#{%s}' % bytes_map_key
149 | if flag:
150 | final_value = json.dumps(change_value)
151 | else:
152 | final_value = change_value
153 | return final_value
154 |
155 | def _bytes_slove(self, s, pattern):
156 | search_result = re.search(pattern, s)
157 | if search_result is not None:
158 | expr = search_result.group(1)
159 | return self.bytes_map[expr]
160 |
161 | def replace_param(self, param):
162 | param = param.strip()
163 | search_result = re.search(r'#\{(.*?)\}', param)
164 | if search_result is not None:
165 | expr = search_result.group(1).strip()
166 | if expr.lower() == '_request_url':
167 | return self.request_url
168 | elif expr.lower() == '_request_path':
169 | return self.request_path
170 | elif expr.lower() == '_request_header':
171 | return self.request_headers
172 | elif expr.lower() == '_request_body':
173 | return self.request_body
174 | elif expr.lower() == '_request_query':
175 | return self.request_query
176 | elif expr.startswith('bytes_'):
177 | return self.bytes_map[expr]
178 | else:
179 | # 支持从请求头和查询参数中取单个数据
180 | if expr.lower().startswith("_request_header."):
181 | data = self.request_headers
182 | expr = '$.' + expr[16:]
183 | elif expr.lower().startswith("_request_query."):
184 | data = self.request_query
185 | expr = '$.' + expr[15:]
186 | else:
187 | data = self.request_body
188 | if expr.lower().startswith("_request_body."):
189 | expr = '$.' + expr[14:]
190 | elif not expr.startswith('$'):
191 | expr = '$.' + expr
192 | try:
193 | return extract_by_jsonpath(data, expr)
194 | except:
195 | return param
196 | else:
197 | return param
198 |
199 | def split_key(self, key: str):
200 | if key.startswith(self.function_prefix):
201 | return key, None
202 | key_list = key.split(".")
203 | key = key_list[0]
204 | json_path = None
205 | if len(key_list) > 1:
206 | json_path = reduce(lambda x, y: x + '.' + y, key_list[1:])
207 | if key.endswith(']') and '[' in key:
208 | keys = key.split("[")
209 | key = keys[0]
210 | if json_path is None:
211 | json_path = keys[-1][:-1]
212 | else:
213 | json_path = keys[-1][:-1] + "." + json_path
214 | if json_path is not None:
215 | json_path = "$." + json_path
216 | return key, json_path
217 |
218 | def split_func(self, statement: str, flag: 'str' = '@'):
219 | pattern = flag + r'([_a-zA-Z][_a-zA-Z0-9]*)(\(.*?\))?'
220 | m = re.match(pattern, statement)
221 | result = list()
222 | if m is not None:
223 | name, _ = m.groups()
224 | args = statement.replace(flag+name, "")
225 | result.append(name)
226 | if args is not None and args != '()':
227 | argList = [str(_) for _ in map(self.replace_param, args[1:-1].split(','))]
228 | argList_length = len(argList)
229 | if not (argList_length == 1 and len(argList[0]) == 0):
230 | if name not in self.func_lib.func_param:
231 | for i in range(argList_length):
232 | result.append(argList[i])
233 | else:
234 | type_list = self.func_lib.func_param[name]
235 | j = 0
236 | for i in range(len(type_list)):
237 | if j >= argList_length:
238 | break
239 | if type_list[i] is str:
240 | result.append(quotation_marks(argList[j]))
241 | j += 1
242 | elif type_list[i] is int:
243 | result.append(int(argList[j]))
244 | j += 1
245 | elif type_list[i] is float:
246 | result.append(float(argList[j]))
247 | j += 1
248 | elif type_list[i] is bool:
249 | result.append(False if argList[j].lower() == 'false' else True)
250 | j += 1
251 | elif type_list[i] is dict:
252 | j, r = self.concat(j, argList, '}')
253 | result.append(r)
254 | elif type_list[i] is list:
255 | j, r = self.concat(j, argList, ']')
256 | result.append(r)
257 | elif type_list[i] is bytes:
258 | result.append(argList[j])
259 | j += 1
260 | elif type_list[i] is None:
261 | result.append(argList[j])
262 | j += 1
263 | else:
264 | raise SplitFunctionError('函数{}第{}个参数类型错误: {}'.format(name, i + 1, type_list[i]))
265 | return result
266 | else:
267 | raise SplitFunctionError('函数错误: {}'.format(statement))
268 |
269 | @staticmethod
270 | def concat(start: int, arg_list: list, terminal_char: str):
271 | end = start
272 | length = len(arg_list)
273 | for i in range(start, length):
274 | if terminal_char in arg_list[i]:
275 | end = i
276 | s = reduce(lambda x, y: x + ',' + y, arg_list[start:end + 1])
277 | try:
278 | return end + 1, eval(quotation_marks(s))
279 | except:
280 | try:
281 | s = '"'+s+'"'
282 | return end + 1, eval(json.loads(s))
283 | except:
284 | continue
285 | else:
286 | s = reduce(lambda x, y: x + ',' + y, arg_list[start:end + 1])
287 | return end + 1, s
288 |
289 |
290 | class SplitFunctionError(Exception):
291 | """函数处理错误"""
292 |
--------------------------------------------------------------------------------
/core/web/collector.py:
--------------------------------------------------------------------------------
1 | from selenium.webdriver.common.by import By
2 |
3 |
4 | locator = {
5 | "ID": By.ID,
6 | "XPATH": "xpath",
7 | "LINK": "link text",
8 | "PARTIAL": "partial link text",
9 | "NAME": "name",
10 | "TAG": "tag name",
11 | "CLASS": "class name",
12 | "CSS": "css selector"
13 | }
14 |
15 |
16 | class WebOperationCollector:
17 |
18 | def __init__(self):
19 | self.id = None
20 | self.opt_type = None
21 | self.opt_name = None
22 | self.opt_trans = None
23 | self.opt_element = None
24 | self.opt_data = None
25 | self.opt_code = None
26 |
27 | @staticmethod
28 | def __parse(ui_data: dict, name):
29 | if name not in ui_data:
30 | return None
31 | return ui_data.get(name)
32 |
33 | def collect_id(self, ui_data):
34 | self.id = WebOperationCollector.__parse(ui_data, "operationId")
35 |
36 | def collect_opt_type(self, ui_data):
37 | self.opt_type = WebOperationCollector.__parse(ui_data, "operationType")
38 |
39 | def collect_opt_name(self, ui_data):
40 | self.opt_name = WebOperationCollector.__parse(ui_data, "operationName")
41 |
42 | def collect_opt_trans(self, ui_data):
43 | self.opt_trans = WebOperationCollector.__parse(ui_data, "operationTrans")
44 |
45 | def collect_opt_code(self, ui_data):
46 | self.opt_code = WebOperationCollector.__parse(ui_data, "operationCode")
47 |
48 | def collect_opt_element(self, ui_data):
49 | opt_element = WebOperationCollector.__parse(ui_data, "operationElement")
50 | if opt_element is None or len(opt_element) == 0:
51 | self.opt_element = None
52 | else:
53 | elements = {}
54 | for name, element in opt_element.items():
55 | elements[name] = (locator[element["by"]], element["expression"])
56 | self.opt_element = elements
57 |
58 | def collect_opt_data(self, ui_data):
59 | opt_data = WebOperationCollector.__parse(ui_data, "operationData")
60 | if opt_data is None or len(opt_data) == 0:
61 | self.opt_data = None
62 | else:
63 | self.opt_data = opt_data
64 |
65 | def collect(self, ui_data):
66 | self.collect_id(ui_data)
67 | self.collect_opt_type(ui_data)
68 | self.collect_opt_name(ui_data)
69 | self.collect_opt_trans(ui_data)
70 | self.collect_opt_element(ui_data)
71 | self.collect_opt_data(ui_data)
72 | self.collect_opt_code(ui_data)
73 |
--------------------------------------------------------------------------------
/core/web/driver/__init__.py:
--------------------------------------------------------------------------------
1 | from selenium.common.exceptions import NoSuchElementException
2 |
3 |
4 | class Operation(object):
5 | def __init__(self, test, driver):
6 | self.driver = driver
7 | self.test = test
8 | self.print = print
9 |
10 | def find_element(self, ele):
11 | """查找单个元素"""
12 | try:
13 | element = self.driver.find_element(*tuple(ele))
14 | self.test.debugLog("成功定位元素 'By: %s Expression: %s'" % ele)
15 | return element
16 | except Exception as e:
17 | self.test.errorLog("无法定位元素 'By: %s Expression: %s'" % ele)
18 | raise e
19 |
20 | def find_elements(self, ele):
21 | """查找批量元素"""
22 | try:
23 | elements = self.driver.find_elements(*tuple(ele))
24 | if len(elements) > 0:
25 | self.test.debugLog("成功定位元素 'By: %s Expression: %s'" % ele)
26 | return elements
27 | else:
28 | self.test.errorLog("无法定位元素 'By: %s Expression: %s'" % ele)
29 | raise NoSuchElementException("Failed to find elements 'By: %s Expression: %s'" % ele)
30 | except Exception as e:
31 | self.test.errorLog("无法定位元素 'By: %s Expression: %s'" % ele)
32 | raise e
33 |
34 |
--------------------------------------------------------------------------------
/core/web/driver/browserOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from selenium.common.exceptions import NoSuchElementException
4 |
5 | from core.web.driver import Operation
6 | from datetime import datetime
7 | from time import sleep
8 |
9 | from tools.utils.utils import url_join
10 |
11 |
12 | class Browser(Operation):
13 | """浏览器类操作"""
14 |
15 | def max_window(self):
16 | """最大化窗口"""
17 | try:
18 | self.driver.maximize_window()
19 | self.test.debugLog("成功执行maximize window")
20 | except Exception as e:
21 | self.test.errorLog("无法执行maximize window")
22 | raise e
23 |
24 | def min_window(self):
25 | """最小化窗口"""
26 | try:
27 | self.driver.minimize_window()
28 | self.test.debugLog("成功执行minimize window")
29 | except Exception as e:
30 | self.test.errorLog("无法执行minimize window")
31 | raise e
32 |
33 | def full_window(self):
34 | """全屏窗口"""
35 | try:
36 | self.driver.fullscreen_window()
37 | self.test.debugLog("成功执行full screen window")
38 | except Exception as e:
39 | self.test.errorLog("无法执行full screen window")
40 | raise e
41 |
42 | def set_position_window(self, x, y):
43 | """设置窗口位置"""
44 | """0,0是左上角"""
45 | try:
46 | self.driver.set_window_position(x, y)
47 | self.test.debugLog("成功执行set window position")
48 | except Exception as e:
49 | self.test.errorLog("无法执行set window position")
50 | raise e
51 |
52 | def set_size_window(self, width, height):
53 | """设置窗口大小"""
54 | try:
55 | self.driver.set_window_size(width, height)
56 | self.test.debugLog("成功执行set window size")
57 | except Exception as e:
58 | self.test.errorLog("无法执行set window size")
59 | raise e
60 |
61 | def switch_to_window(self, window):
62 | """切换窗口"""
63 | try:
64 | self.driver.switch_to.window(window)
65 | self.test.debugLog("成功执行switch window")
66 | except Exception as e:
67 | self.test.errorLog("无法执行switch window")
68 | raise e
69 |
70 | def close_window(self):
71 | """关闭窗口"""
72 | try:
73 | self.driver.close()
74 | self.test.debugLog("成功执行close window")
75 | except Exception as e:
76 | self.test.errorLog("无法执行close window")
77 | raise e
78 |
79 | def save_screenshot(self, name):
80 | """屏幕截图"""
81 | try:
82 | screenshot = self.driver.get_screenshot_as_png()
83 | self.test.saveScreenShot(name, screenshot)
84 | self.test.debugLog("成功执行screen shot")
85 | except Exception as e:
86 | self.test.errorLog("无法执行screen shot")
87 | raise e
88 |
89 | def click_to_new_window(self, element):
90 | """单击跳转新窗口"""
91 | try:
92 | current = self.driver.window_handles
93 | # 点击打开新窗口
94 | self.find_element(element).click()
95 | # 等待新窗口出现
96 | current_time = datetime.now()
97 | while (datetime.now()-current_time).seconds < 60:
98 | if len(self.driver.window_handles) > len(current):
99 | for window_handle in self.driver.window_handles:
100 | if window_handle not in current:
101 | self.driver.switch_to.window(window_handle)
102 | self.test.debugLog("成功执行click and switch to new window")
103 | return
104 | else:
105 | sleep(2)
106 | except NoSuchElementException as e:
107 | raise e
108 | except Exception as e:
109 | self.test.errorLog("无法执行click and switch to new window")
110 | raise e
111 |
112 | def back_and_close_window(self, window):
113 | """返回并关闭当前窗口"""
114 | try:
115 | self.driver.close()
116 | self.driver.switch_to.window(window)
117 | self.test.debugLog("成功执行back and close window")
118 | except Exception as e:
119 | self.test.errorLog("无法执行back and close window")
120 | raise e
121 |
122 | def open_url(self, domain, path):
123 | """打开网页"""
124 | try:
125 | url = url_join(domain, path)
126 | self.driver.get(url)
127 | self.driver.implicitly_wait(2)
128 | self.test.debugLog("成功打开 '%s'" % url_join(domain, path))
129 | except Exception as e:
130 | self.test.errorLog("无法打开 '%s'" % url_join(domain, path))
131 | raise e
132 |
133 | def refresh(self):
134 | """刷新页面"""
135 | try:
136 | self.driver.refresh()
137 | self.test.debugLog("成功执行refresh")
138 | except Exception as e:
139 | self.test.errorLog("无法执行refresh")
140 | raise e
141 |
142 | def back(self):
143 | """页面后退"""
144 | try:
145 | self.driver.back()
146 | self.test.debugLog("成功执行back")
147 | except Exception as e:
148 | self.test.errorLog("无法执行back")
149 | raise e
150 |
151 | def forward(self):
152 | """页面前进"""
153 | try:
154 | self.driver.forward()
155 | self.test.debugLog("成功执行forward")
156 | except Exception as e:
157 | self.test.errorLog("无法执行forward")
158 | raise e
159 |
160 | def add_cookie(self, name, value):
161 | """添加cookie"""
162 | try:
163 | self.driver.add_cookie({'name': name, 'value': value})
164 | self.test.debugLog("成功执行add cookie: %s:%s" % (name, value))
165 | except Exception as e:
166 | self.test.errorLog("无法执行add cookie: %s:%s" % (name, value))
167 | raise e
168 |
169 | def delete_cookie(self, name):
170 | """删除cookie"""
171 | try:
172 | self.driver.delete_cookie(name)
173 | self.test.debugLog("成功执行delete cookie:%s" % name)
174 | except Exception as e:
175 | self.test.errorLog("无法执行delete cookie:%s" % name)
176 | raise e
177 |
178 | def delete_cookies(self):
179 | """删除cookies"""
180 | try:
181 | self.driver.delete_all_cookies()
182 | self.test.debugLog("成功执行delete cookies")
183 | except Exception as e:
184 | self.test.errorLog("无法执行delete cookies")
185 | raise e
186 |
187 | def execute_script(self, script, arg:tuple):
188 | """执行脚本"""
189 | try:
190 | self.driver.execute_script(script, *arg)
191 | self.test.debugLog("成功执行execute script:%s" % script)
192 | except NoSuchElementException as e:
193 | raise e
194 | except Exception as e:
195 | self.test.errorLog("无法执行execute script:%s" % script)
196 | raise e
197 |
198 | def execute_async_script(self, script, arg:tuple):
199 | """执行异步脚本"""
200 | try:
201 | self.driver.execute_async_script(script, *arg)
202 | self.test.debugLog("成功执行execute async script:%s" % script)
203 | except NoSuchElementException as e:
204 | raise e
205 | except Exception as e:
206 | self.test.errorLog("无法执行execute async script:%s" % script)
207 | raise e
208 |
209 | def sleep(self, second):
210 | """强制等待"""
211 | try:
212 | sleep(second)
213 | self.test.debugLog("成功执行sleep %ds" % second)
214 | except Exception as e:
215 | self.test.errorLog("无法执行sleep %ds" % second)
216 | raise e
217 |
218 | def implicitly_wait(self, second):
219 | """隐式等待"""
220 | try:
221 | self.driver.implicitly_wait(second)
222 | self.test.debugLog("成功执行implicitly wait %ds" % second)
223 | except Exception as e:
224 | self.test.errorLog("无法执行implicitly wait %ds" % second)
225 | raise e
226 |
227 | def custom(self, **kwargs):
228 | """自定义"""
229 | code = kwargs["code"]
230 | names = locals()
231 | names["element"] = kwargs["element"]
232 | names["data"] = kwargs["data"]
233 | names["driver"] = self.driver
234 | names["test"] = self.test
235 | try:
236 | def print(*args, sep=' ', end='\n', file=None, flush=False):
237 | if file is None or file in (sys.stdout, sys.stderr):
238 | file = names["test"].stdout_buffer
239 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
240 |
241 | def sys_get(name):
242 | if name in names["test"].context:
243 | return names["test"].context[name]
244 | elif name in names["test"].common_params:
245 | return names["test"].common_params[name]
246 | else:
247 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
248 |
249 | def sys_put(name, val, ps=False):
250 | if ps:
251 | names["test"].common_params[name] = val
252 | else:
253 | names["test"].context[name] = val
254 |
255 | exec(code)
256 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
257 | except NoSuchElementException as e:
258 | raise e
259 | except Exception as e:
260 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
261 | raise e
262 |
--------------------------------------------------------------------------------
/core/web/driver/pageOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from selenium.common.exceptions import NoSuchElementException
4 | from selenium.webdriver import ActionChains
5 | from selenium.webdriver.common.keys import Keys
6 | from selenium.webdriver.support import wait, expected_conditions
7 | from selenium.webdriver.support.wait import WebDriverWait
8 |
9 | from core.web.driver import Operation
10 |
11 |
12 | class Page(Operation):
13 | """网页类操作"""
14 |
15 | def switch_frame(self, frame):
16 | """切换框架"""
17 | try:
18 | frame_reference = self.find_element(frame)
19 | self.driver.switch_to.frame(frame_reference)
20 | self.test.debugLog("成功切换frame:%s" % frame[1])
21 | except NoSuchElementException as e:
22 | raise e
23 | except Exception as e:
24 | self.test.errorLog("无法切换frame:%s" % frame[1])
25 | raise e
26 |
27 | def switch_content(self):
28 | """返回默认框架"""
29 | try:
30 | self.driver.switch_to.default_content()
31 | self.test.debugLog("成功切换default content")
32 | except Exception as e:
33 | self.test.errorLog("无法切换default content")
34 | raise e
35 |
36 | def switch_parent(self):
37 | """返回父框架"""
38 | try:
39 | self.driver.switch_to.parent_frame()
40 | self.test.debugLog("成功切换parent content")
41 | except Exception as e:
42 | self.test.errorLog("无法切换parent content")
43 | raise e
44 |
45 | def alert_accept(self):
46 | """弹出框确认"""
47 | try:
48 | alert = wait.WebDriverWait(self.driver, timeout=30).until(expected_conditions.alert_is_present())
49 | alert.accept()
50 | self.test.debugLog("成功执行alert accept")
51 | except Exception as e:
52 | self.test.errorLog("无法执行alert accept")
53 | raise e
54 |
55 | def alert_input(self, text):
56 | """弹出框输入"""
57 | try:
58 | alert = wait.WebDriverWait(self.driver, timeout=30).until(expected_conditions.alert_is_present())
59 | alert.send_keys(text)
60 | self.test.debugLog("成功执行alert input")
61 | except Exception as e:
62 | self.test.errorLog("无法执行alert input")
63 | raise e
64 |
65 | def alert_cancel(self):
66 | """弹出框取消"""
67 | try:
68 | alert = wait.WebDriverWait(self.driver, timeout=30).until(expected_conditions.alert_is_present())
69 | alert.dismiss()
70 | self.test.debugLog("成功执行alert cancel")
71 | except Exception as e:
72 | self.test.errorLog("无法执行alert cancel")
73 | raise e
74 |
75 | def free_click(self):
76 | """鼠标单击"""
77 | try:
78 | ActionChains(self.driver).click().perform()
79 | self.test.debugLog("成功执行free click")
80 | except NoSuchElementException as e:
81 | raise e
82 | except Exception as e:
83 | self.test.errorLog("无法执行free click")
84 | raise e
85 |
86 | def clear(self, element):
87 | """清空"""
88 | try:
89 | self.find_element(element).clear()
90 | self.test.debugLog("成功执行clear")
91 | except NoSuchElementException as e:
92 | raise e
93 | except Exception as e:
94 | self.test.errorLog("无法执行clear")
95 | raise e
96 |
97 | def input_text(self, element, text):
98 | """输入"""
99 | try:
100 | self.find_element(element).send_keys(text)
101 | self.test.debugLog("成功执行文本输入:'%s'" % text)
102 | except NoSuchElementException as e:
103 | raise e
104 | except Exception as e:
105 | self.test.errorLog("无法执行文本输入:'%s'" % text)
106 | raise e
107 |
108 | def click(self, element):
109 | """单击"""
110 | try:
111 | self.find_element(element).click()
112 | self.test.debugLog("成功执行click")
113 | except NoSuchElementException as e:
114 | raise e
115 | except Exception as e:
116 | self.test.errorLog("无法执行click")
117 | raise e
118 |
119 | def submit(self, element):
120 | """提交"""
121 | try:
122 | self.find_element(element).submit()
123 | self.test.debugLog("成功执行submit")
124 | except NoSuchElementException as e:
125 | raise e
126 | except Exception as e:
127 | self.test.errorLog("无法执行submit")
128 | raise e
129 |
130 | def click_and_hold(self, element):
131 | """单击保持"""
132 | try:
133 | ele = self.find_element(element)
134 | ActionChains(self.driver).click_and_hold(ele).perform()
135 | self.test.debugLog("成功执行click and hold")
136 | except NoSuchElementException as e:
137 | raise e
138 | except Exception as e:
139 | self.test.errorLog("无法执行click and hold")
140 | raise e
141 |
142 | def context_click(self, element):
143 | """右键点击"""
144 | try:
145 | ele = self.find_element(element)
146 | ActionChains(self.driver).context_click(ele).perform()
147 | self.test.debugLog("成功执行context click")
148 | except NoSuchElementException as e:
149 | raise e
150 | except Exception as e:
151 | self.test.errorLog("无法执行context click")
152 | raise e
153 |
154 | def double_click(self, element):
155 | """双击"""
156 | try:
157 | ele = self.find_element(element)
158 | ActionChains(self.driver).double_click(ele).perform()
159 | self.test.debugLog("成功执行double click")
160 | except NoSuchElementException as e:
161 | raise e
162 | except Exception as e:
163 | self.test.errorLog("无法执行double click")
164 | raise e
165 |
166 | def drag_and_drop(self, start_element, end_element):
167 | """拖拽"""
168 | try:
169 | ele = self.find_element(start_element)
170 | tar_ele = self.find_element(end_element)
171 | ActionChains(self.driver).drag_and_drop(ele, tar_ele).perform()
172 | self.test.debugLog("成功执行drag and drop to element")
173 | except NoSuchElementException as e:
174 | raise e
175 | except Exception as e:
176 | self.test.errorLog("无法执行drag and drop to element")
177 | raise e
178 |
179 | def drag_and_drop_by_offset(self, element, x, y):
180 | """偏移拖拽"""
181 | try:
182 | ele = self.find_element(element)
183 | ActionChains(self.driver).drag_and_drop_by_offset(ele, x, y).perform()
184 | self.test.debugLog("成功执行drag and drop to (%s, %s)" % (x,y))
185 | except NoSuchElementException as e:
186 | raise e
187 | except Exception as e:
188 | self.test.errorLog("无法执行drag and drop to (%s, %s)" % (x,y))
189 | raise e
190 |
191 | def key_down(self, element, value):
192 | """按下键位"""
193 | try:
194 | ele = self.find_element(element)
195 | if hasattr(Keys, value.upper()):
196 | keys = getattr(Keys, value)
197 | else:
198 | raise Exception("键位%s不存在" % value)
199 | ActionChains(self.driver).key_down(keys, ele).perform()
200 | self.test.debugLog("成功执行key down %s" % value)
201 | except NoSuchElementException as e:
202 | raise e
203 | except Exception as e:
204 | self.test.errorLog("无法执行key down %s" % value)
205 | raise e
206 |
207 | def key_up(self, element, value):
208 | """释放键位"""
209 | try:
210 | ele = self.find_element(element)
211 | if hasattr(Keys, value.upper()):
212 | keys = getattr(Keys, value)
213 | else:
214 | raise Exception("键位%s不存在" % value)
215 | ActionChains(self.driver).key_up(keys, ele).perform()
216 | self.test.debugLog("成功执行key up %s" % value)
217 | except NoSuchElementException as e:
218 | raise e
219 | except Exception as e:
220 | self.test.errorLog("无法执行key up %s" % value)
221 | raise e
222 |
223 | def move_by_offset(self, x, y):
224 | """鼠标移动到坐标"""
225 | try:
226 | ActionChains(self.driver).move_by_offset(x, y).perform()
227 | self.test.debugLog("成功执行move mouse to (%s, %s)" % (x,y))
228 | except NoSuchElementException as e:
229 | raise e
230 | except Exception as e:
231 | self.test.errorLog("无法执行move mouse to (%s, %s)" % (x,y))
232 | raise e
233 |
234 | def move_to_element(self, element):
235 | """鼠标移动到元素"""
236 | try:
237 | ele = self.find_element(element)
238 | ActionChains(self.driver).move_to_element(ele).perform()
239 | self.test.debugLog("成功执行move mouse to element")
240 | except NoSuchElementException as e:
241 | raise e
242 | except Exception as e:
243 | self.test.errorLog("无法执行move mouse to element")
244 | raise e
245 |
246 | def move_to_element_with_offset(self, element, x, y):
247 | """鼠标移动到元素坐标"""
248 | try:
249 | ele = self.find_element(element)
250 | ActionChains(self.driver).move_to_element_with_offset(ele, x, y).perform()
251 | self.test.debugLog("成功执行move mouse to element with (%s, %s)" % (x,y))
252 | except NoSuchElementException as e:
253 | raise e
254 | except Exception as e:
255 | self.test.errorLog("无法执行move mouse to element with (%s, %s)" % (x,y))
256 | raise e
257 |
258 | def release(self, element):
259 | """释放点击保持状态"""
260 | try:
261 | ele = self.find_element(element)
262 | ActionChains(self.driver).release(ele).perform()
263 | self.test.debugLog("成功执行release mouse")
264 | except NoSuchElementException as e:
265 | raise e
266 | except Exception as e:
267 | self.test.errorLog("无法执行release mouse")
268 | raise e
269 |
270 | def wait_element_appear(self, element, second):
271 | """等待元素出现"""
272 | try:
273 | WebDriverWait(self.driver, second, 0.2).until(expected_conditions.presence_of_element_located(element))
274 | self.test.debugLog("成功执行wait %ds until element appear" % second)
275 | except Exception as e:
276 | self.test.errorLog("无法执行wait %ds until element appear" % second)
277 | raise e
278 |
279 | def wait_element_disappear(self, element, second):
280 | """等待元素消失"""
281 | try:
282 | WebDriverWait(self.driver, second, 0.2).until_not(expected_conditions.presence_of_element_located(element))
283 | self.test.debugLog("成功执行wait %ds until element disappear" % second)
284 | except Exception as e:
285 | self.test.errorLog("无法执行wait %ds until element disappear" % second)
286 | raise e
287 |
288 | def custom(self, **kwargs):
289 | """自定义"""
290 | code = kwargs["code"]
291 | names = locals()
292 | names["element"] = kwargs["element"]
293 | names["data"] = kwargs["data"]
294 | names["driver"] = self.driver
295 | names["test"] = self.test
296 | try:
297 | def print(*args, sep=' ', end='\n', file=None, flush=False):
298 | if file is None or file in (sys.stdout, sys.stderr):
299 | file = names["test"].stdout_buffer
300 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
301 |
302 | def sys_get(name):
303 | if name in names["test"].context:
304 | return names["test"].context[name]
305 | elif name in names["test"].common_params:
306 | return names["test"].common_params[name]
307 | else:
308 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
309 |
310 | def sys_put(name, val, ps=False):
311 | if ps:
312 | names["test"].common_params[name] = val
313 | else:
314 | names["test"].context[name] = val
315 |
316 | exec(code)
317 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
318 | except NoSuchElementException as e:
319 | raise e
320 | except Exception as e:
321 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
322 | raise e
323 |
--------------------------------------------------------------------------------
/core/web/driver/relationOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from selenium.common.exceptions import NoSuchElementException
4 |
5 | from core.web.driver import Operation
6 |
7 |
8 | class Relation(Operation):
9 | """关联类操作"""
10 | def get_page_title(self, save_name):
11 | """获取页面标题"""
12 | try:
13 | actual = self.driver.title
14 | self.test.debugLog("成功获取title:%s" % str(actual))
15 | except Exception as e:
16 | self.test.errorLog("无法获取title")
17 | raise e
18 | else:
19 | self.test.context[save_name] = actual
20 |
21 | def get_page_url(self, save_name):
22 | """获取页面url"""
23 | try:
24 | actual = self.driver.current_url
25 | self.test.debugLog("成功获取url:%s" % str(actual))
26 | except Exception as e:
27 | self.test.errorLog("无法获取url")
28 | raise e
29 | else:
30 | self.test.context[save_name] = actual
31 |
32 | def get_ele_text(self, element, save_name):
33 | """获取元素文本"""
34 | try:
35 | actual = self.find_element(element).text
36 | self.test.debugLog("成功获取元素text:%s" % str(actual))
37 | except NoSuchElementException as e:
38 | raise e
39 | except Exception as e:
40 | self.test.errorLog("无法获取元素text")
41 | raise e
42 | else:
43 | self.test.context[save_name] = actual
44 |
45 | def get_ele_tag(self, element, save_name):
46 | """获取元素tag"""
47 | try:
48 | actual = self.find_element(element).tag_name
49 | self.test.debugLog("成功获取元素tag name:%s" % str(actual))
50 | except NoSuchElementException as e:
51 | raise e
52 | except Exception as e:
53 | self.test.errorLog("无法获取元素tag name")
54 | raise e
55 | else:
56 | self.test.context[save_name] = actual
57 |
58 | def get_ele_size(self, element, save_name):
59 | """获取元素尺寸"""
60 | try:
61 | actual = self.find_element(element).size
62 | self.test.debugLog("成功获取元素size:%s" % str(actual))
63 | except NoSuchElementException as e:
64 | raise e
65 | except Exception as e:
66 | self.test.errorLog("无法获取元素size")
67 | raise e
68 | else:
69 | self.test.context[save_name] = actual
70 |
71 | def get_ele_height(self, element, save_name):
72 | """获取元素高度"""
73 | try:
74 | actual = self.find_element(element).size.get("height")
75 | self.test.debugLog("成功获取元素height:%s" % str(actual))
76 | except NoSuchElementException as e:
77 | raise e
78 | except Exception as e:
79 | self.test.errorLog("无法获取元素height")
80 | raise e
81 | else:
82 | self.test.context[save_name] = actual
83 |
84 | def get_ele_width(self, element, save_name):
85 | """获取元素宽度"""
86 | try:
87 | actual = self.find_element(element).size.get("width")
88 | self.test.debugLog("成功获取元素width:%s" % str(actual))
89 | except NoSuchElementException as e:
90 | raise e
91 | except Exception as e:
92 | self.test.errorLog("无法获取元素width")
93 | raise e
94 | else:
95 | self.test.context[save_name] = actual
96 |
97 | def get_ele_location(self, element, save_name):
98 | """获取元素位置"""
99 | try:
100 | actual = self.find_element(element).location
101 | self.test.debugLog("成功获取元素location:%s" % str(actual))
102 | except NoSuchElementException as e:
103 | raise e
104 | except Exception as e:
105 | self.test.errorLog("无法获取元素location")
106 | raise e
107 | else:
108 | self.test.context[save_name] = actual
109 |
110 | def get_ele_x(self, element, save_name):
111 | """获取元素X坐标"""
112 | try:
113 | actual = self.find_element(element).location.get("x")
114 | self.test.debugLog("成功获取元素location x:%s" % str(actual))
115 | except NoSuchElementException as e:
116 | raise e
117 | except Exception as e:
118 | self.test.errorLog("无法获取元素location x")
119 | raise e
120 | else:
121 | self.test.context[save_name] = actual
122 |
123 | def get_ele_y(self, element, save_name):
124 | """获取元素Y坐标"""
125 | try:
126 | actual = self.find_element(element).location.get("y")
127 | self.test.debugLog("成功获取元素location y:%s" % str(actual))
128 | except NoSuchElementException as e:
129 | raise e
130 | except Exception as e:
131 | self.test.errorLog("无法获取元素location y")
132 | raise e
133 | else:
134 | self.test.context[save_name] = actual
135 |
136 | def get_ele_attribute(self, element, name, save_name):
137 | """获取元素属性"""
138 | try:
139 | actual = self.find_element(element).get_attribute(name)
140 | self.test.debugLog("成功获取元素attribute:%s" % str(actual))
141 | except NoSuchElementException as e:
142 | raise e
143 | except Exception as e:
144 | self.test.errorLog("无法获取元素attribute")
145 | raise e
146 | else:
147 | self.test.context[save_name] = actual
148 |
149 | def get_ele_css(self, element, name, save_name):
150 | """获取元素css样式"""
151 | try:
152 | actual = self.find_element(element).value_of_css_property(name)
153 | self.test.debugLog("成功获取元素css %s:%s" % (name, str(actual)))
154 | except NoSuchElementException as e:
155 | raise e
156 | except Exception as e:
157 | self.test.errorLog("无法获取元素css %s" % name)
158 | raise e
159 | else:
160 | self.test.context[save_name] = actual
161 |
162 | def get_window_position(self, save_name):
163 | """获取窗口位置"""
164 | try:
165 | actual = self.driver.get_window_position()
166 | self.test.debugLog("成功获取窗口position:%s" % str(actual))
167 | except Exception as e:
168 | self.test.errorLog("无法获取窗口position")
169 | raise e
170 | else:
171 | self.test.context[save_name] = actual
172 |
173 | def get_window_x(self, save_name):
174 | """获取窗口X坐标"""
175 | try:
176 | actual = self.driver.get_window_position().get("x")
177 | self.test.debugLog("成功获取窗口position x:%s" % str(actual))
178 | except Exception as e:
179 | self.test.errorLog("无法获取窗口position x")
180 | raise e
181 | else:
182 | self.test.context[save_name] = actual
183 |
184 | def get_window_y(self, save_name):
185 | """获取窗口Y坐标"""
186 | try:
187 | actual = self.driver.get_window_position().get("y")
188 | self.test.debugLog("成功获取窗口position y:%s" % str(actual))
189 | except Exception as e:
190 | self.test.errorLog("无法获取窗口position y")
191 | raise e
192 | else:
193 | self.test.context[save_name] = actual
194 |
195 | def get_window_size(self, save_name):
196 | """获取窗口大小"""
197 | try:
198 | actual = self.driver.get_window_size()
199 | self.test.debugLog("成功获取窗口size:%s" % str(actual))
200 | except Exception as e:
201 | self.test.errorLog("无法获取窗口size")
202 | raise e
203 | else:
204 | self.test.context[save_name] = actual
205 |
206 | def get_window_width(self, save_name):
207 | """获取窗口宽度"""
208 | try:
209 | actual = self.driver.get_window_size().get("width")
210 | self.test.debugLog("成功获取窗口width:%s" % str(actual))
211 | except Exception as e:
212 | self.test.errorLog("无法获取窗口width")
213 | raise e
214 | else:
215 | self.test.context[save_name] = actual
216 |
217 | def get_window_height(self, save_name):
218 | """获取窗口高度"""
219 | try:
220 | actual = self.driver.get_window_size().get("height")
221 | self.test.debugLog("成功获取窗口height:%s" % str(actual))
222 | except Exception as e:
223 | self.test.errorLog("无法获取窗口height")
224 | raise e
225 | else:
226 | self.test.context[save_name] = actual
227 |
228 | def get_current_handle(self, save_name):
229 | """获取当前窗口句柄"""
230 | try:
231 | actual = self.driver.current_window_handle
232 | self.test.debugLog("成功获取当前窗口handle:%s" % str(actual))
233 | except Exception as e:
234 | self.test.errorLog("无法获取当前窗口handle")
235 | raise e
236 | else:
237 | self.test.context[save_name] = actual
238 |
239 | def get_all_handle(self, save_name):
240 | """获取所有窗口句柄"""
241 | try:
242 | actual = self.driver.window_handles
243 | self.test.debugLog("成功获取所有窗口handle:%s" % str(actual))
244 | except Exception as e:
245 | self.test.errorLog("无法获取所有窗口handle")
246 | raise e
247 | else:
248 | self.test.context[save_name] = actual
249 |
250 | def get_cookies(self, save_name):
251 | """获取cookies"""
252 | try:
253 | actual = self.driver.get_cookies()
254 | self.test.debugLog("成功获取cookies:%s" % str(actual))
255 | except Exception as e:
256 | self.test.errorLog("无法获取cookies")
257 | raise e
258 | else:
259 | self.test.context[save_name] = actual
260 |
261 | def get_cookie(self, name, save_name):
262 | """获取cookie"""
263 | try:
264 | actual = self.driver.get_cookie(name)
265 | self.test.debugLog("成功获取cookie %s:%s" % (name, str(actual)))
266 | except Exception as e:
267 | self.test.errorLog("无法获取cookie:%s" % name)
268 | raise e
269 | else:
270 | self.test.context[save_name] = actual
271 |
272 | def custom(self, **kwargs):
273 | """自定义"""
274 | code = kwargs["code"]
275 | names = locals()
276 | names["element"] = kwargs["element"]
277 | names["data"] = kwargs["data"]
278 | names["driver"] = self.driver
279 | names["test"] = self.test
280 | try:
281 | """关联操作需要返回被断言的值 以sys_return(value)返回"""
282 |
283 | def print(*args, sep=' ', end='\n', file=None, flush=False):
284 | if file is None or file in (sys.stdout, sys.stderr):
285 | file = names["test"].stdout_buffer
286 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
287 |
288 | def sys_return(res):
289 | names["_exec_result"] = res
290 |
291 | def sys_get(name):
292 | if name in names["test"].context:
293 | return names["test"].context[name]
294 | elif name in names["test"].common_params:
295 | return names["test"].common_params[name]
296 | else:
297 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
298 |
299 | def sys_put(name, val, ps=False):
300 | if ps:
301 | names["test"].common_params[name] = val
302 | else:
303 | names["test"].context[name] = val
304 |
305 | exec(code)
306 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
307 | except NoSuchElementException as e:
308 | raise e
309 | except Exception as e:
310 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
311 | raise e
312 | else:
313 | self.test.context[kwargs["data"]["save_name"]] = names["_exec_result"]
314 |
315 |
--------------------------------------------------------------------------------
/core/web/driver/scenarioOpt.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from selenium.common.exceptions import NoSuchElementException
4 | from core.web.driver import Operation
5 |
6 |
7 | class Scenario(Operation):
8 | """场景类类操作"""
9 |
10 | def custom(self, **kwargs):
11 | """自定义"""
12 | code = kwargs["code"]
13 | names = locals()
14 | names["element"] = kwargs["element"]
15 | names["data"] = kwargs["data"]
16 | names["driver"] = self.driver
17 | names["test"] = self.test
18 | try:
19 | def print(*args, sep=' ', end='\n', file=None, flush=False):
20 | if file is None or file in (sys.stdout, sys.stderr):
21 | file = names["test"].stdout_buffer
22 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
23 |
24 | def sys_get(name):
25 | if name in names["test"].context:
26 | return names["test"].context[name]
27 | elif name in names["test"].common_params:
28 | return names["test"].common_params[name]
29 | else:
30 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
31 |
32 | def sys_put(name, val, ps=False):
33 | if ps:
34 | names["test"].common_params[name] = val
35 | else:
36 | names["test"].context[name] = val
37 |
38 | exec(code)
39 | self.test.debugLog("成功执行 %s" % kwargs["trans"])
40 | except NoSuchElementException as e:
41 | raise e
42 | except Exception as e:
43 | self.test.errorLog("无法执行 %s" % kwargs["trans"])
44 | raise e
45 |
--------------------------------------------------------------------------------
/core/web/testcase.py:
--------------------------------------------------------------------------------
1 | import re
2 | from selenium import webdriver
3 | from core.template import Template
4 | from core.web.collector import WebOperationCollector
5 | from core.web.teststep import WebTestStep
6 | from tools.utils.utils import get_case_message, handle_operation_data, handle_params_data
7 |
8 |
9 | class WebTestCase:
10 | def __init__(self, test):
11 | self.test = test
12 | self.context = test.context
13 | self.case_message = get_case_message(test.test_data)
14 | self.id = self.case_message['caseId']
15 | self.name = self.case_message['caseName']
16 | setattr(test, 'test_case_name', self.case_message['caseName'])
17 | setattr(test, 'test_case_desc', self.case_message['comment'])
18 | self.functions = self.case_message['functions']
19 | self.params = handle_params_data(self.case_message['params'])
20 | test.common_params = self.params
21 | self.template = Template(self.test, self.context, self.functions, self.params)
22 | self.driver = self.before_execute()
23 | self.comp = re.compile(r"\{\{.*?\}\}")
24 |
25 | def execute(self):
26 | if self.case_message['optList'] is None:
27 | self.after_execute()
28 | raise RuntimeError("无法获取WEB测试相关数据, 请重试!!!")
29 | try:
30 | self.loop_execute(self.case_message['optList'], [])
31 | finally:
32 | self.after_execute()
33 |
34 | def loop_execute(self, opt_list, skip_opts, step_n=0):
35 | while step_n < len(opt_list):
36 | opt_content = opt_list[step_n]
37 | # 定义收集器
38 | collector = WebOperationCollector()
39 | step = WebTestStep(self.test, self.driver, self.context, collector)
40 | # 定义事务
41 | self.test.defineTrans(opt_content["operationId"], opt_content['operationTrans'],
42 | self.get_opt_content(opt_content['operationElement']), opt_content['operationDesc'])
43 | if step_n in skip_opts:
44 | self.test.updateTransStatus(3)
45 | self.test.debugLog('[{}]操作在条件控制之外不被执行'.format(opt_content['operationTrans']))
46 | step_n += 1
47 | continue
48 | # 收集步骤信息
49 | step.collector.collect(opt_content)
50 | try:
51 | if step.collector.opt_type == "looper":
52 | looper_step_num = step.looper_controller(self, opt_list, step_n)
53 | step_n += looper_step_num + 1
54 | else:
55 | # 渲染主体
56 | self.render_content(step)
57 | step.execute()
58 | step.assert_controller()
59 | skip_opts.extend(step.condition_controller(step_n))
60 | step_n += 1
61 | except Exception as e:
62 | if not isinstance(e, AssertionError):
63 | self.test.saveScreenShot(opt_content['operationTrans'], self.driver.get_screenshot_as_png())
64 | raise e
65 |
66 | @staticmethod
67 | def get_opt_content(elements):
68 | content = ""
69 | if elements is not None:
70 | for key, element in elements.items():
71 | content = "%s\n %s: %s" % (content, key, element["target"])
72 | return content
73 |
74 | def before_execute(self):
75 | old_driver = self.test.driver.driver
76 | if self.case_message["startDriver"]:
77 | # 读取配置
78 | opt = webdriver.ChromeOptions()
79 | driver_setting = self.render_driver(self.case_message["driverSetting"])
80 | if "arguments" in driver_setting.keys():
81 | for item in driver_setting["arguments"]:
82 | if item["value"] != "":
83 | opt.add_argument(item["value"])
84 | if "experimentals" in driver_setting.keys():
85 | for item in driver_setting["experimentals"]:
86 | if item["name"] != "" and item["value"] != "":
87 | opt.add_experimental_option(item["name"], handle_operation_data(item["type"], item["value"]))
88 | if "extensions" in driver_setting.keys():
89 | for item in driver_setting["extensions"]:
90 | if item["value"] != "":
91 | opt.add_encoded_extension(item["value"])
92 | if "files" in driver_setting.keys():
93 | for item in driver_setting["files"]:
94 | if item["value"] != "":
95 | opt.add_extension(item["value"])
96 | if "binary" in driver_setting.keys() and driver_setting["binary"] != "":
97 | opt.binary_location = driver_setting["binary"]
98 | if self.test.driver.browser_opt == "headless":
99 | opt.add_argument("--headless")
100 | opt.add_argument("--no-sandbox")
101 | elif self.test.driver.browser_opt == "remote":
102 | caps = {
103 | 'browserName': 'chrome'
104 | }
105 | else:
106 | opt.add_experimental_option('excludeSwitches', ['enable-logging'])
107 | if old_driver is not None:
108 | old_driver.quit()
109 | self.test.driver.driver = None
110 | if self.test.driver.browser_opt == "remote":
111 | return webdriver.Remote(command_executor=self.test.driver.browser_path,
112 | desired_capabilities=caps, options=opt)
113 | else:
114 | return webdriver.Chrome(executable_path=self.test.driver.browser_path, options=opt)
115 | else:
116 | if old_driver is not None:
117 | return old_driver
118 | else:
119 | raise RuntimeError("无法找到已启动的浏览器进程 请检查用例开关驱动配置")
120 |
121 | def after_execute(self):
122 | if self.case_message["closeDriver"]:
123 | self.driver.quit()
124 | self.test.driver.driver = None
125 | else:
126 | self.test.driver.driver = self.driver
127 |
128 | def render_driver(self, driver_setting):
129 | self.template.init(driver_setting)
130 | return self.template.render()
131 |
132 | def render_looper(self, looper):
133 | self.template.init(looper)
134 | _looper = self.template.render()
135 | for name, param in _looper.items():
136 | if name != "target" or name != "expect": # 断言实际值不作数据处理
137 | _looper[name] = handle_operation_data(param["type"], param["value"])
138 | if "times" in _looper:
139 | try:
140 | times = int(_looper["times"])
141 | except:
142 | times = 1
143 | _looper["times"] = times
144 | return _looper
145 |
146 | def render_content(self, step):
147 | if step.collector.opt_element is not None:
148 | for name, expressions in step.collector.opt_element.items():
149 | expression = expressions[1]
150 | if self.comp.search(str(expression)) is not None:
151 | self.template.init(expression)
152 | render_value = self.template.render()
153 | expressions = (expressions[0], str(render_value))
154 | step.collector.opt_element[name] = expressions
155 | if step.collector.opt_data is not None:
156 | data = {}
157 | for name, param in step.collector.opt_data.items():
158 | param_value = param["value"]
159 | if isinstance(param_value, str) and self.comp.search(param_value) is not None:
160 | self.template.init(param_value)
161 | param_value = self.template.render()
162 | data[name] = handle_operation_data(param["type"], param_value)
163 | step.collector.opt_data = data
164 |
165 |
--------------------------------------------------------------------------------
/core/web/teststep.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from datetime import datetime
3 | from core.assertion import LMAssert
4 | from core.web.find_opt import *
5 |
6 |
7 | class WebTestStep:
8 | def __init__(self, test, driver, context, collector):
9 | self.test = test
10 | self.driver = driver
11 | self.context = context
12 | self.collector = collector
13 | self.result = None
14 |
15 | def execute(self):
16 | try:
17 | self.test.debugLog('WEB操作[{}]开始'.format(self.collector.opt_name))
18 | opt_type = self.collector.opt_type
19 | if opt_type == "browser":
20 | func = find_browser_opt(self.collector.opt_name)
21 | elif opt_type == "page":
22 | func = find_page_opt(self.collector.opt_name)
23 | elif opt_type == "condition":
24 | func = find_condition_opt(self.collector.opt_name)
25 | elif opt_type == "assertion":
26 | func = find_assertion_opt(self.collector.opt_name)
27 | elif opt_type == "relation":
28 | func = find_relation_opt(self.collector.opt_name)
29 | else:
30 | func = find_scenario_opt(self.collector.opt_name)
31 | if func is None:
32 | raise NotExistedWebOperation("未定义操作")
33 | opt_content = {
34 | "trans": self.collector.opt_trans,
35 | "code": self.collector.opt_code,
36 | "element": self.collector.opt_element,
37 | "data": self.collector.opt_data
38 | }
39 | self.result = func(self.test, self.driver, **opt_content)
40 | self.log_show()
41 | finally:
42 | self.test.debugLog('WEB操作[{}]结束'.format(self.collector.opt_name))
43 |
44 | def looper_controller(self, case, opt_list, step_n):
45 | """循环控制器"""
46 | if self.collector.opt_trans == "While循环":
47 | loop_start_time = datetime.now()
48 | timeout = int(self.collector.opt_data["timeout"]["value"])
49 | index_name = self.collector.opt_data["indexName"]["value"]
50 | steps = int(self.collector.opt_data["steps"]["value"])
51 | index = 0
52 | while timeout == 0 or (datetime.now() - loop_start_time).seconds * 1000 < timeout:
53 | # timeout为0时可能会死循环 慎重选择
54 | self.context[index_name] = index # 给循环索引赋值第几次循环 母循环和子循环的索引名不应一样
55 | _looper = case.render_looper(self.collector.opt_data) # 渲染循环控制控制器 每次循环都需要渲染
56 | index += 1
57 | result, _ = LMAssert(_looper['assertion'], _looper['target'], _looper['expect']).compare()
58 | if not result:
59 | break
60 | _opt_list = opt_list[step_n+1: (step_n + _looper["steps"]+1)] # 循环操作本身不参与循环 不然死循环
61 | case.loop_execute(_opt_list, [])
62 | return steps
63 | else:
64 | _looper = case.render_looper(self.collector.opt_data) # 渲染循环控制控制器 for只需渲染一次
65 | for index in range(_looper["times"]): # 本次循环次数
66 | self.context[_looper["indexName"]] = index # 给循环索引赋值第几次循环 母循环和子循环的索引名不应一样
67 | _opt_list = opt_list[step_n+1: (step_n + _looper["steps"]+1)]
68 | case.loop_execute(_opt_list, [])
69 | return _looper["steps"]
70 |
71 | def assert_controller(self):
72 | if self.collector.opt_type == "assertion":
73 | if self.result[0]:
74 | self.test.debugLog('[{}]断言成功: {}'.format(self.collector.opt_trans,
75 | self.result[1]))
76 | else:
77 | self.test.errorLog('[{}]断言失败: {}'.format(self.collector.opt_trans,
78 | self.result[1]))
79 | self.test.saveScreenShot(self.collector.opt_trans, self.driver.get_screenshot_as_png())
80 | if "continue" in self.collector.opt_data and self.collector.opt_data["continue"] is True:
81 | try:
82 | raise AssertionError(self.result[1])
83 | except AssertionError:
84 | error_info = sys.exc_info()
85 | self.test.recordFailStatus(error_info)
86 | else:
87 | raise AssertionError(self.result[1])
88 |
89 | def condition_controller(self, current):
90 | if self.collector.opt_type == "condition":
91 | offset_true = self.collector.opt_data["true"]
92 | if not isinstance(offset_true, int):
93 | offset_true = 0
94 | offset_false = self.collector.opt_data["false"]
95 | if not isinstance(offset_false, int):
96 | offset_false = 0
97 | if self.result[0]:
98 | self.test.debugLog('[{}]判断成功, 执行成功分支: {}'.format(self.collector.opt_name,
99 | self.result[1]))
100 | return [current + i for i in range(offset_true + 1, offset_true + offset_false + 1)]
101 | else:
102 | self.test.errorLog('[{}]判断失败, 执行失败分支: {}'.format(self.collector.opt_name,
103 | self.result[1]))
104 | return [current + i for i in range(1, offset_true + 1)]
105 | return []
106 |
107 | def log_show(self):
108 | msg = ""
109 | if self.collector.opt_element is not None:
110 | for k, v in self.collector.opt_element.items():
111 | msg += '元素定位: {}: {}
'.format(k, v)
112 | if self.collector.opt_data is not None:
113 | data_log = '{'
114 | for k, v in self.collector.opt_data.items():
115 | class_name = type(v).__name__
116 | data_log += "{}: {}, ".format(k, v)
117 | if len(data_log) > 1:
118 | data_log = data_log[:-2]
119 | data_log += '}'
120 | msg += '操作数据: {}'.format(data_log)
121 | if msg != "":
122 | msg = '操作信息:
' + msg
123 | self.test.debugLog(msg)
124 |
125 |
126 | class NotExistedWebOperation(Exception):
127 | """未定义的WEB操作"""
128 |
--------------------------------------------------------------------------------
/lm/lm_api.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import base64
3 | import requests
4 | import time
5 | from lm.lm_config import *
6 | from lm.lm_log import DebugLogger, ErrorLogger
7 |
8 |
9 | class Api(object):
10 |
11 | def __init__(self):
12 | config = LMConfig()
13 | self.url = config.url[:-1] if config.url.endswith("/") else config.url
14 | self.engine = config.engine
15 | self.secret = config.secret
16 | self.proxy = None
17 |
18 | def request(self, url, data):
19 | header = self.load_header()
20 | response = requests.post(url=url, json=data, headers=header, proxies=self.proxy, timeout=30)
21 | return response
22 |
23 | def download(self, url):
24 | header = self.load_header()
25 | response = requests.get(url=url, headers=header, proxies=self.proxy, stream=True, timeout=30)
26 | return response
27 |
28 | @staticmethod
29 | def save_token(token):
30 | reader = IniReader()
31 | DebugLogger("更新token")
32 | reader.modify("Header", "token", token)
33 |
34 | @staticmethod
35 | def load_header():
36 | config = LMConfig()
37 | header = config.header
38 | return header
39 |
40 |
41 | class LMApi(Api):
42 |
43 | def apply_token(self):
44 | """"申请token"""
45 | url = self.url + "/openapi/engine/token/apply"
46 | data = {
47 | "engineCode": self.engine,
48 | "engineSecret": self.secret,
49 | "timestamp": int(time.time()),
50 | }
51 | try:
52 | res = self.request(url=url, data=data)
53 | if res.status_code == 200:
54 | status = res.json()["status"]
55 | if status == 0:
56 | token = res.json()["data"]
57 | self.save_token(token)
58 | elif status == 2050:
59 | DebugLogger("调用申请token接口 引擎id或秘钥错误")
60 | else:
61 | DebugLogger("调用申请token接口 token生成失败")
62 | else:
63 | DebugLogger("调用申请token接口 响应状态为:%s" % res.status_code)
64 | except Exception as e:
65 | ErrorLogger("调用申请token接口 发生错误 错误信息为:%s" % e)
66 |
67 | def fetch_task(self):
68 | """"获取任务"""
69 | url = self.url + "/openapi/engine/task/fetch"
70 | for index in range(2):
71 | data = {
72 | "engineCode": self.engine,
73 | "timestamp": int(time.time())
74 | }
75 | try:
76 | if index > 0:
77 | DebugLogger("-------重试调用获取引擎任务接口--------")
78 | res = self.request(url, data)
79 | if res.status_code == 200:
80 | status = res.json()["status"]
81 | if status == 0:
82 | return res.json()["data"]
83 | elif status in (2020, 2030, 2040):
84 | DebugLogger("token校验错误 重新申请token")
85 | self.apply_token()
86 | continue
87 | else:
88 | DebugLogger("获取引擎任务请求失败")
89 | else:
90 | DebugLogger("调用获取引擎任务接口 响应状态为:%s" % res.status_code)
91 | except Exception as e:
92 | ErrorLogger("调用获取引擎任务接口 发生错误 错误信息为:%s" % e)
93 | break
94 |
95 | def upload_result(self, task_id, data_type, result):
96 | """"上传执行结果"""
97 | url = self.url + "/openapi/engine/result/upload"
98 | for index in range(2):
99 | data = {
100 | "engineCode": self.engine,
101 | "timestamp": int(time.time()),
102 | "taskId": task_id,
103 | "caseResultList": result
104 | }
105 | try:
106 | if index > 0:
107 | DebugLogger("-------重试调用上传执行结果接口--------")
108 | res = self.request(url, data)
109 | if res.status_code == 200:
110 | status = res.json()["status"]
111 | if status == 0:
112 | return True
113 | elif status in (2020, 2030, 2040):
114 | DebugLogger("token校验错误 重新申请token")
115 | self.apply_token()
116 | continue
117 | else:
118 | DebugLogger("上传执行结果请求失败")
119 | else:
120 | DebugLogger("调用上传执行结果接口 响应状态为:%s" % res.status_code)
121 | except Exception as e:
122 | ErrorLogger("调用上传执行结果接口 发生错误 错误信息为:%s" % e)
123 | break
124 |
125 | def complete_task(self, task_id):
126 | """"反馈任务结束"""
127 | url = self.url + "/openapi/engine/task/complete"
128 | for index in range(2):
129 | data = {
130 | "engineCode": self.engine,
131 | "timestamp": int(time.time()),
132 | "taskId": task_id
133 | }
134 | try:
135 | if index > 0:
136 | DebugLogger("-------重试调用反馈任务结束接口--------")
137 | res = self.request(url, data)
138 | if res.status_code == 200:
139 | status = res.json()["status"]
140 | if status == 0:
141 | return True
142 | elif status in (2020, 2030, 2040):
143 | DebugLogger("token校验错误 重新申请token")
144 | self.apply_token()
145 | continue
146 | else:
147 | DebugLogger("反馈任务结束请求失败")
148 | else:
149 | DebugLogger("调用反馈任务结束接口 响应状态为:%s" % res.status_code)
150 | except Exception as e:
151 | ErrorLogger("调用反馈任务结束接口 发生错误 错误信息为:%s" % e)
152 | break
153 |
154 | def download_task_file(self, path):
155 | """下载任务文件"""
156 | url = self.url + path
157 | for index in range(2):
158 | try:
159 | if index > 0:
160 | DebugLogger("-------重试调用下载任务文件接口--------")
161 | res = self.download(url)
162 | if res.status_code == 200:
163 | if not isinstance(res.content, bytes):
164 | status = res.json()["status"]
165 | if status in (2020, 2030, 2040):
166 | DebugLogger("token校验错误 重新申请token")
167 | self.apply_token()
168 | continue
169 | else:
170 | DebugLogger("下载任务文件失败")
171 | else:
172 | return res
173 | else:
174 | DebugLogger("调用下载任务文件接口 响应状态为:%s" % res.status_code)
175 | except Exception as e:
176 | ErrorLogger("调用下载任务文件接口 发生错误 错误信息为:%s" % e)
177 | break
178 |
179 | def download_test_file(self, uuid):
180 | """下载测试文件"""
181 | url = self.url + "/openapi/download/test/file/" + uuid
182 | for index in range(2):
183 | try:
184 | if index > 0:
185 | DebugLogger("-------重试调用下载测试文件接口--------")
186 | res = self.download(url)
187 | if res.status_code == 200:
188 | if not isinstance(res.content, bytes):
189 | status = res.json()["status"]
190 | if status in (2020, 2030, 2040):
191 | DebugLogger("token校验错误 重新申请token")
192 | self.apply_token()
193 | continue
194 | else:
195 | DebugLogger("下载测试文件失败")
196 | else:
197 | return res
198 | else:
199 | DebugLogger("调用下载测试文件接口 响应状态为:%s" % res.status_code)
200 | except Exception as e:
201 | ErrorLogger("调用下载测试文件接口 发生错误 错误信息为:%s" % e)
202 | break
203 |
204 | def upload_screen_shot(self,task_image_path, uuid, log_path):
205 | """"上传执行截图"""
206 | url = self.url + "/openapi/engine/screenshot/upload"
207 | for index in range(2):
208 | data = {
209 | "fileName": "%s.png" % uuid,
210 | "engineCode": self.engine,
211 | "timestamp": int(time.time())
212 | }
213 | with open(os.path.join(task_image_path, "%s.png" % uuid), "rb") as f:
214 | file = base64.b64encode(f.read()).decode()
215 | data["base64String"] = file
216 | try:
217 | res = self.request(url, data)
218 | if res.status_code == 200:
219 | status = res.json()["status"]
220 | if status == 0:
221 | DebugLogger("截图%s上传成功" % uuid, file_path=log_path)
222 | return True
223 | elif status in (2020, 2030, 2040):
224 | DebugLogger("token校验错误 重新申请token", file_path=log_path)
225 | self.apply_token()
226 | continue
227 | else:
228 | ErrorLogger("截图%s上传失败" % uuid, file_path=log_path)
229 | else:
230 | DebugLogger("调用上传截图接口 响应状态为:%s" % res.status_code, file_path=log_path)
231 | except Exception as e:
232 | ErrorLogger("调用上传截图接口 发生错误 错误信息为:%s" % e, file_path=log_path)
233 | break
234 | else:
235 | ErrorLogger("截图%s上传失败" % uuid, file_path=log_path)
236 | return False
237 |
--------------------------------------------------------------------------------
/lm/lm_case.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import io
3 | import os
4 | import datetime
5 | import sys
6 | import time
7 | import unittest
8 | import traceback
9 | from uuid import uuid1
10 | from core.api.testcase import ApiTestCase
11 | from core.web.testcase import WebTestCase
12 | from core.app.testcase import AppTestCase
13 | from lm.lm_config import IMAGE_PATH, LMConfig
14 |
15 |
16 | class LMCase(unittest.TestCase):
17 |
18 | def __init__(self, case_name, test_data, case_type="API"):
19 | self.test_data = test_data
20 | self.trans_list = []
21 | self.case_name = case_name
22 | self.case_type = case_type
23 | unittest.TestCase.__init__(self, case_name)
24 |
25 | def testEntrance(self):
26 | if self.case_type == "API":
27 | ApiTestCase(test=self).execute()
28 | elif self.case_type == "WEB":
29 | WebTestCase(test=self).execute()
30 | else:
31 | AppTestCase(test=self).execute()
32 |
33 | def doCleanups(self):
34 | unittest.TestCase.doCleanups(self)
35 | self.handleResult()
36 |
37 | def debugLog(self, log_info):
38 | """执行日志"""
39 | if len(self.trans_list) > 0:
40 | current_time = datetime.datetime.now()
41 | log = "%s - Debug - %s" % (current_time.strftime('%Y-%m-%d %H:%M:%S.%f'), log_info)
42 | if self.trans_list[-1]["log"] != "":
43 | if self.case_type == "API":
44 | log = "
" + log
45 | else:
46 | log = "
" + log
47 | self.trans_list[-1]["log"] = self.trans_list[-1]["log"] + log
48 |
49 | def errorLog(self, log_info):
50 | """错误日志"""
51 | if len(self.trans_list) > 0:
52 | current_time = datetime.datetime.now()
53 | log = "%s - Error - %s" % (current_time.strftime('%Y-%m-%d %H:%M:%S.%f'), log_info)
54 | if self.trans_list[-1]["log"] != "":
55 | if self.case_type == "API":
56 | log = "
" + log
57 | else:
58 | log = "
" + log
59 | self.trans_list[-1]["log"] = self.trans_list[-1]["log"] + log
60 |
61 | def recordTransDuring(self, during):
62 | """记录事务时长"""
63 | if len(self.trans_list) > 0:
64 | self.trans_list[-1]["during"] = during
65 |
66 | def defineTrans(self, id, name, content="", desc=None):
67 | """定义事务"""
68 | if len(self.trans_list) > 0:
69 | self.complete_output()
70 | if self.trans_list[-1]["status"] == "":
71 | self.trans_list[-1]["status"] = 0
72 | trans_dict = {
73 | "id": id,
74 | "name": name,
75 | "content": content,
76 | "description": desc,
77 | "log": "",
78 | "during": 0,
79 | "status": "",
80 | "screenShotList": []
81 | }
82 | self.trans_list.append(trans_dict)
83 |
84 | def complete_output(self):
85 | """获取控制台输出"""
86 | stdout_buffer = getattr(self, "stdout_buffer", io.StringIO())
87 | output = stdout_buffer.getvalue()
88 | stdout_buffer.truncate(0)
89 | if output:
90 | output = output.replace("\n", "
")
91 | self.debugLog("控制台输出:
%s" % output)
92 |
93 | def deleteTrans(self, index):
94 | """删除事务"""
95 | if len(self.trans_list) > index:
96 | del self.trans_list[index]
97 |
98 | def updateTransStatus(self, status):
99 | if len(self.trans_list) > 0:
100 | self.trans_list[-1]["status"] = status
101 |
102 | def recordFailStatus(self, exc_info=None):
103 | """记录断言失败"""
104 | self._outcome.errors.append((self, exc_info))
105 | if len(self.trans_list) > 0:
106 | self.trans_list[-1]["status"] = 1 # 记录当前事务为失败
107 | self.errorLog(str(exc_info[1]))
108 |
109 | def recordErrorStatus(self, exc_info=None):
110 | """记录程序错误"""
111 | self._outcome.errors.append((self, exc_info))
112 | if len(self.trans_list) > 0:
113 | self.trans_list[-1]["status"] = 2 # 记录当前事务为错误
114 | self.errorLog(str(exc_info[1]))
115 | if LMConfig().enable_stderr.lower() == "true":
116 | # 此处可以打印详细报错的代码
117 | tb_e = traceback.TracebackException(exc_info[0], exc_info[1], exc_info[2])
118 | msg_lines = list(tb_e.format())
119 | err_msg = "程序错误信息: "
120 | for msg in msg_lines:
121 | err_msg = err_msg + "
" + msg
122 | self.errorLog(str(err_msg))
123 |
124 | def saveScreenShot(self, name, screen_shot):
125 | """保存截图"""
126 | uuid = time.strftime("%Y%m%d") + "_" +str(uuid1())
127 | task_id = getattr(self, "task_id")
128 | task_image_path = os.path.join(IMAGE_PATH, task_id)
129 | try:
130 | filename = "%s.png" % uuid
131 | if not os.path.exists(task_image_path):
132 | os.makedirs(task_image_path)
133 | file_path = os.path.join(task_image_path, filename)
134 | with open(file_path, 'wb') as f:
135 | f.write(screen_shot)
136 | except:
137 | self.errorLog("Fail: Failed to save screen shot %s" % name)
138 | else:
139 | if len(self.trans_list) > 0:
140 | self.trans_list[-1]["screenShotList"].append(uuid)
141 |
142 | def handleResult(self):
143 | """结果处理"""
144 | if len(self.trans_list) == 0:
145 | self.defineTrans(self.case_name.split("_")[1], "未知", "未知")
146 | self.complete_output()
147 | isFail = False
148 | isError = False
149 | error_type = None
150 | error_value = None
151 | error_tb = None
152 | # 处理用例执行过程中的错误和失败 以此来判断用例最终状态
153 | for index, (test, exc_info) in enumerate(self._outcome.errors):
154 | if exc_info is not None:
155 | if issubclass(exc_info[0], AssertionError):
156 | isFail = True
157 | if not isError: # 默认错误优先级高
158 | error_type = AssertionError
159 | error_value = exc_info[1]
160 | error_tb = exc_info[2]
161 | else:
162 | isError = True
163 | error_type = exc_info[0]
164 | error_value = exc_info[1]
165 | error_tb = exc_info[2]
166 | # 根据用例原始成功状态来判断最后一个事务是否是成功的
167 | if self._outcome.success is True:
168 | if self.trans_list[-1]["status"] == "": # 如果最后一个事务没有状态,则设为pass
169 | self.trans_list[-1]["status"] = 0
170 | if isError or isFail: # 有错误或者失败的话用例修改状态
171 | self._outcome.errors.clear()
172 | self._outcome.errors.append((self, (error_type, error_value, error_tb)))
173 | self._outcome.success = False
174 | else:
175 | # 如果用例原始成功状态为否 则说明最后一个事务是失败或者错误的
176 | exc_info = self._outcome.errors[-2][-1] # 倒数第二个errors是最后一个事务的
177 | if issubclass(exc_info[0], AssertionError):
178 | self.trans_list[-1]["status"] = 1 # 最后一步设为fail
179 | else:
180 | self.errorLog(str(exc_info[1]))
181 | self.trans_list[-1]["status"] = 2 # 最后一步设为error
182 | if LMConfig().enable_stderr.lower() == "true":
183 | # 此处可以打印详细报错的代码
184 | tb_e = traceback.TracebackException(exc_info[0], exc_info[1], exc_info[2])
185 | msg_lines = list(tb_e.format())
186 | err_msg = "程序错误信息: "
187 | for msg in msg_lines:
188 | err_msg = err_msg + "
" + msg
189 | self.errorLog(str(err_msg))
190 | self._outcome.errors.clear()
191 | self._outcome.errors.append((self, (error_type, error_value, error_tb)))
192 |
--------------------------------------------------------------------------------
/lm/lm_config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import configparser
4 |
5 | BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
6 | DATA_PATH = os.path.join(BASE_PATH, "data")
7 | FILE_PATH = os.path.join(BASE_PATH, "file")
8 | LOG_PATH = os.path.join(BASE_PATH, "log")
9 | CONFIG_PATH = os.path.join(BASE_PATH, "config", "config.ini")
10 | IMAGE_PATH = os.path.join(BASE_PATH, "image")
11 | BROWSER_PATH = os.path.join(BASE_PATH, "browser")
12 |
13 |
14 | class IniReader:
15 |
16 | def __init__(self, config_ini=CONFIG_PATH):
17 | if os.path.exists(config_ini):
18 | self.ini_file = config_ini
19 | else:
20 | raise FileNotFoundError('文件不存在!')
21 |
22 | def data(self, section, option):
23 | config = configparser.ConfigParser()
24 | config.read(self.ini_file, encoding="utf-8")
25 | value = config.get(section, option)
26 | return value
27 |
28 | def option(self, section):
29 | config = configparser.ConfigParser()
30 | config.read(self.ini_file, encoding="utf-8")
31 | options = config.options(section)
32 | option = {}
33 | for key in options:
34 | option[key] = self.data(section, key)
35 | return option
36 |
37 | def modify(self, section, option, value):
38 | config = configparser.ConfigParser()
39 | config.read(self.ini_file, encoding="utf-8")
40 | config.set(section, option, value)
41 | config.write(open(self.ini_file, "r+", encoding="utf-8"))
42 |
43 |
44 | class LMConfig(object):
45 | """"配置文件"""
46 | def __init__(self, path=CONFIG_PATH):
47 | reader = IniReader(path)
48 | self.url = reader.data("Platform", "url")
49 | self.enable_stderr = reader.data("Platform", "enable-stderr")
50 | self.engine = reader.data("Engine", "engine-code")
51 | self.secret = reader.data("Engine", "engine-secret")
52 | self.header = reader.option("Header")
53 | self.browser_opt = reader.data("WebDriver", "options")
54 | if self.browser_opt == "remote" or "/" in reader.data("WebDriver", "path"):
55 | self.browser_path = reader.data("WebDriver", "path")
56 | else:
57 | self.browser_path = os.path.join(BROWSER_PATH, reader.data("WebDriver", "path"))
58 | self.max_run = reader.data("RunSetting", "max-run")
59 |
60 |
--------------------------------------------------------------------------------
/lm/lm_log.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import logging
4 | import threading
5 | from lm.lm_config import LOG_PATH
6 |
7 |
8 | class LMLogger(object):
9 |
10 | def __init__(self, logger_name='Auto Test'):
11 | self.logger = logging.getLogger(logger_name)
12 | self.formatter = logging.Formatter("%(asctime)s - %(levelname)-4s - %(message)s")
13 | self.logger.setLevel(logging.INFO)
14 |
15 | def get_handler(self, file_path):
16 | p, f = os.path.split(file_path)
17 | if not (os.path.exists(p)):
18 | os.makedirs(p)
19 | file_handler = logging.FileHandler(file_path, encoding="utf8")
20 | file_handler.setFormatter(self.formatter)
21 | return file_handler
22 |
23 |
24 | my_logger = LMLogger()
25 | default_log_path = os.path.join(LOG_PATH, "engine_run.log")
26 | my_lock = threading.RLock()
27 |
28 |
29 | def DebugLogger(log_info, file_path=default_log_path):
30 | try:
31 | if my_lock.acquire():
32 | file_handler = my_logger.get_handler(file_path)
33 | my_logger.logger.addHandler(file_handler)
34 | my_logger.logger.info(log_info)
35 | my_logger.logger.removeHandler(file_handler)
36 |
37 | my_lock.release()
38 | except Exception as e:
39 | print("Failed to record debug log. Reason:\n %s" % str(e))
40 |
41 |
42 | def ErrorLogger(log_info, file_path=default_log_path):
43 | try:
44 | if my_lock.acquire():
45 | file_handler = my_logger.get_handler(file_path)
46 | my_logger.logger.addHandler(file_handler)
47 | my_logger.logger.error(log_info)
48 | my_logger.logger.removeHandler(file_handler)
49 |
50 | my_lock.release()
51 | except Exception as e:
52 | print("Failed to record error log. Reason:\n %s" % str(e))
53 |
--------------------------------------------------------------------------------
/lm/lm_report.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import datetime, time
3 | import os
4 | import shutil
5 | from lm.lm_api import LMApi
6 | from lm.lm_log import DebugLogger, ErrorLogger
7 | from lm.lm_config import DATA_PATH
8 |
9 |
10 | class LMReport(object):
11 | def __init__(self, message_queue, case_result_queue):
12 | self.case_result_queue = case_result_queue
13 | self.message_queue = message_queue
14 | self.api = LMApi()
15 |
16 | def monitor_result(self):
17 | not_send_result = []
18 | last_send_time = datetime.datetime.now()
19 | while True:
20 | try:
21 | message = self.case_result_queue.get()
22 | except Exception as e:
23 | DebugLogger("获取执行结果报错 错误信息%s" % str(e))
24 | else:
25 | if isinstance(message, str):
26 | if "run_all_start" in message:
27 | task_id = message.split("--")[1]
28 | data_type = message.split("--")[-1]
29 | DebugLogger("任务执行启动 开始监听执行结果 任务id: %s" % task_id)
30 | elif "run_all_stop" in message:
31 | if len(not_send_result) != 0:
32 | self.api.upload_result(task_id, data_type, not_send_result)
33 | self.post_stop(task_id) # 执行结束
34 | self.message_queue.put({"type": "completed", "data": task_id}) # 通知任务管理器清空当前执行任务
35 | time.sleep(2)
36 | break
37 | else: # start_run_index--n
38 | if len(not_send_result) != 0:
39 | self.api.upload_result(task_id, data_type, not_send_result)
40 | not_send_result.clear()
41 | index = int(message.split("--")[-1])
42 | if index > 0:
43 | DebugLogger("用例有执行错误 重试执行 任务id: %s" % task_id)
44 | else:
45 | """控制请求频率"""
46 | result = message
47 | not_send_result.append(result)
48 | current_time = datetime.datetime.now()
49 | during = (current_time - last_send_time).seconds
50 | if during < 3:
51 | pass
52 | else:
53 | self.api.upload_result(task_id, data_type, not_send_result)
54 | last_send_time = current_time
55 | not_send_result.clear()
56 |
57 | def post_stop(self, task_id=None):
58 | DebugLogger("任务执行结束 调用接口通知平台 任务id: %s" % task_id)
59 | self.api.complete_task(task_id)
60 | data = os.path.join(DATA_PATH, str(task_id))
61 | if os.path.exists(data):
62 | try:
63 | shutil.rmtree(data)
64 | except Exception as e:
65 | ErrorLogger("删除测试数据失败 失败原因:%s 任务id: %s" % (str(e), task_id))
66 |
--------------------------------------------------------------------------------
/lm/lm_result.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import datetime
3 | import io
4 | import sys
5 | import unittest
6 |
7 |
8 | class LMResult(unittest.TestResult):
9 |
10 | def __init__(self, result, lock, queue):
11 | unittest.TestResult.__init__(self)
12 | self.stdout_buffer = None
13 | self.original_stdout = sys.stdout
14 | self.default_result = result
15 | self.default_lock = lock
16 | self.queue = queue
17 | self.result = []
18 |
19 | def startTest(self, test):
20 | unittest.TestResult.startTest(self, test)
21 | self.setupStdout()
22 | test.stdout_buffer = self.stdout_buffer
23 | test.start_time = datetime.datetime.now()
24 |
25 | def setupStdout(self):
26 | if self.stdout_buffer is None:
27 | self.stdout_buffer = io.StringIO()
28 |
29 | def stopTest(self, test):
30 | unittest.TestResult.stopTest(self, test)
31 | test.stop_time = datetime.datetime.now()
32 | if self.default_lock.acquire():
33 | status, test_case, error = self.result[-1]
34 | case_info = {
35 | "status": status,
36 | "startTime": test_case.start_time.timestamp()*1000,
37 | "endTime": test_case.stop_time.timestamp()*1000,
38 | "collectionId": test_case.__class__.__doc__.split("_")[-1],
39 | "caseId": getattr(test, "case_name", " _ ").split("_")[1],
40 | "caseType": getattr(test, "case_type", "API"),
41 | "caseName": getattr(test, "test_case_name", "未知"),
42 | "caseDesc": getattr(test, "test_case_desc", None),
43 | "index": int(getattr(test, "case_name", " _0").split("_")[-1]),
44 | "runTimes": getattr(test, "run_index", 1),
45 | "transactionList": test_case.trans_list
46 | }
47 | self.default_result.append(case_info)
48 | self.queue.put(case_info)
49 | self.default_lock.release()
50 |
51 | def restoreStdout(self):
52 | self.stdout_buffer.seek(0)
53 | self.stdout_buffer.truncate()
54 |
55 | def addSuccess(self, test):
56 | unittest.TestResult.addSuccess(self, test)
57 | self.mergeResult(0, test, "")
58 |
59 | def addFailure(self, test, err):
60 | unittest.TestResult.addFailure(self, test, err)
61 | _, _exc_str = self.failures[-1]
62 | self.mergeResult(1, test, _exc_str)
63 |
64 | def addError(self, test, err):
65 | unittest.TestResult.addError(self, test, err)
66 | _, _exc_str = self.errors[-1]
67 | self.mergeResult(2, test, _exc_str)
68 |
69 | def addSkip(self, test, reason):
70 | unittest.TestResult.addSkip(self, test, reason)
71 | self.mergeResult(3, test, reason)
72 |
73 | def mergeResult(self, n, test, e):
74 | self.result.append((n, test, e))
75 |
--------------------------------------------------------------------------------
/lm/lm_run.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import unittest, threading
3 | from lm import lm_case, lm_result
4 | from lm.lm_log import ErrorLogger
5 | from lm.lm_config import LMConfig
6 |
7 |
8 | class LMRun(object):
9 | def __init__(self, plan_tuple, run_index, default_result, default_lock, queue):
10 | self.plan_tuple = plan_tuple
11 | self.run_index = run_index
12 | self.default_result = default_result
13 | self.default_lock = default_lock
14 | self.queue = queue
15 |
16 | def run_test(self):
17 | suite = unittest.TestSuite()
18 | for case in self.plan_tuple:
19 | cls_name = case["test_class"]
20 | try:
21 | cls = eval(cls_name)
22 | except:
23 | cls = type(cls_name, (lm_case.LMCase,), {'__doc__': cls_name})
24 | case_name = case["test_case"]
25 | case_type = case["test_type"]
26 | setattr(cls, case_name, lm_case.LMCase.testEntrance)
27 | case_data = case["test_data"]
28 | test_case = cls(case_name, case_data, case_type)
29 | test_case.task_id = case["task_id"]
30 | test_case.driver = case["driver"]
31 | test_case.session = case["session"]
32 | test_case.context = case["context"]
33 | test_case.run_index = self.run_index
34 | suite.addTest(test_case)
35 |
36 | result = lm_result.LMResult(self.default_result, self.default_lock, self.queue)
37 |
38 | try:
39 | suite(result)
40 | # 执行测试用例
41 | except Exception as ex:
42 | ErrorLogger("Failed to run test(RunTime:run%s & ThreadName:%s), Error info:%s" %
43 | (self.run_index, threading.current_thread().name, ex))
44 |
--------------------------------------------------------------------------------
/lm/lm_setting.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import threading
4 | from concurrent.futures import ThreadPoolExecutor, as_completed
5 | from requests import Session
6 | import zipfile
7 | from lm.lm_run import LMRun
8 | from lm.lm_log import DebugLogger, ErrorLogger
9 | from lm.lm_config import DATA_PATH, LMConfig
10 | from lm.lm_api import LMApi
11 |
12 |
13 | class LMSetting(object):
14 | def __init__(self, task):
15 | self.task = task
16 | self.data_path = DATA_PATH
17 | self.config = LMConfig()
18 |
19 | def data_pull(self):
20 | data_url = self.task["downloadUrl"]
21 | if not os.path.exists(self.data_path):
22 | os.makedirs(self.data_path)
23 | try:
24 | file = LMApi().download_task_file(data_url)
25 | except Exception as e:
26 | ErrorLogger("数据拉取失败 错误信息: %s 任务id: %s" % (str(e), self.task["taskId"]))
27 | return None
28 | else:
29 | file_path = os.path.join(self.data_path, str(self.task["taskId"]) + ".zip")
30 | with open(file_path, 'wb+') as f:
31 | for chunk in file.iter_content(chunk_size=1024):
32 | if chunk:
33 | f.write(chunk)
34 | f.close()
35 | DebugLogger("数据拉取成功 任务id: %s" % self.task["taskId"])
36 | return file_path
37 |
38 | def file_unzip(self, file_path):
39 | r = zipfile.is_zipfile(file_path)
40 | if r:
41 | with zipfile.ZipFile(file_path, 'r') as fz:
42 | for file in fz.namelist():
43 | fz.extract(file, self.data_path)
44 | os.remove(file_path)
45 |
46 | def task_analysis(self):
47 | test_plan = {}
48 | if self.task["taskType"] != 'debug':
49 | file_path = self.data_pull()
50 | if file_path is not None:
51 | self.file_unzip(file_path)
52 | for collection_map in self.task["testCollectionList"]:
53 | collection = collection_map["collectionId"]
54 | test_case_list = collection_map["testCaseList"]
55 | session = LMSession()
56 | driver = LMDriver()
57 | context = dict()
58 | for case in test_case_list:
59 | test_case = {
60 | "driver": driver,
61 | "session": session,
62 | "context": context,
63 | "task_id": self.task["taskId"],
64 | "test_type": case["caseType"],
65 | "test_class": "class_" + collection,
66 | "test_case": "case_%s_%s" % (case["caseId"], case["index"]),
67 | "test_data": os.path.join(self.data_path, self.task["taskId"], collection, case["caseId"] + ".json")
68 | }
69 | if collection not in test_plan.keys():
70 | test_plan[collection] = []
71 | test_plan[collection].append(test_case)
72 | else:
73 | collection_map = self.task["testCollectionList"][0]
74 | collection = collection_map["collectionId"]
75 | session = LMSession()
76 | driver = LMDriver()
77 | context = dict()
78 | test_case = {
79 | "driver": driver,
80 | "session": session,
81 | "context": context,
82 | "task_id": self.task["taskId"],
83 | "test_type": collection_map["testCaseList"][0]["caseType"],
84 | "test_class": "class_" + collection,
85 | "test_case": "case_%s_%s" % (collection_map["testCaseList"][0]["caseId"], collection_map["testCaseList"][0]["index"]),
86 | "test_data": self.task["debugData"]
87 | }
88 | test_plan[collection] = [test_case]
89 | return test_plan
90 |
91 | def create_thread(self, plan, queue, current_exec_status):
92 | runTime = 1
93 | if self.task["reRun"]:
94 | runTime = 2
95 | task_id = self.task["taskId"]
96 | max_thread = self.task["maxThread"]
97 | queue.put("run_all_start--%s" % task_id)
98 | for index in range(runTime):
99 | if index == 0:
100 | test_plan = plan
101 | else:
102 | test_plan = self.read_fail_case(test_plan, default_result)
103 | default_result = []
104 | if len(test_plan) > 0:
105 | queue.put("start_run_index--%s" % index)
106 | default_lock = threading.RLock()
107 | # 进行线程池管理执行 设置最大并发
108 | with ThreadPoolExecutor(max_workers=max_thread) as t:
109 | executors = [t.submit(LMRun(test_case_list, index + 1, default_result, default_lock,
110 | queue).run_test, ) for test_case_list in test_plan.values()]
111 | as_completed(executors)
112 |
113 | queue.put("run_all_stop--%s" % task_id)
114 | current_exec_status.value = 1
115 |
116 | @staticmethod
117 | def read_fail_case(test_plan, result):
118 | new_test_plan = {}
119 | for collection, test_case_list in test_plan.items():
120 | for test in test_case_list:
121 | case_id = test["test_case"].split("_")[1]
122 | index = test["test_case"].split("_")[-1]
123 | for case in result:
124 | if case["collectionId"] == collection and case["caseId"] == case_id and case["index"] == int(index):
125 | if case["status"] in (1, 2):
126 | if collection not in new_test_plan:
127 | new_test_plan[collection] = []
128 | new_test_plan[collection].append(test)
129 | result.remove(case)
130 | break
131 | return new_test_plan
132 |
133 |
134 | class LMSession(object):
135 | """API测试专用"""
136 | def __init__(self):
137 | self.session = Session()
138 |
139 |
140 | class LMDriver(object):
141 | """WEB测试专用"""
142 | def __init__(self):
143 | self.driver = None
144 | self.config = LMConfig()
145 | self.browser_opt = self.config.browser_opt
146 | self.browser_path = self.config.browser_path
147 |
148 |
--------------------------------------------------------------------------------
/lm/lm_start.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import threading
3 | from multiprocessing import Process, Queue, Value
4 | import time, os
5 | from lm.lm_api import LMApi
6 | from lm.lm_setting import LMSetting
7 | from lm.lm_report import LMReport
8 | from lm.lm_log import DebugLogger, ErrorLogger
9 | from lm.lm_config import LOG_PATH, IMAGE_PATH, LMConfig
10 | from lm.lm_upload import LMUpload
11 | import psutil
12 | from lm.lm_ws import Client
13 |
14 |
15 | class LMStart(object):
16 |
17 | def __init__(self):
18 | self.api = LMApi()
19 | self.config = LMConfig()
20 | self.exec_processes = {}
21 |
22 | def main(self):
23 | """"启动入口"""
24 | message_queue = Queue() # 消息队列
25 | status_thread = threading.Thread(target=self.send_heartbeat, args=(message_queue,))
26 | status_thread.start() # 启动心跳链接
27 | task_queue = Queue() # 任务队列
28 | task_thread = threading.Thread(target=self.fetch_task, args=(task_queue,))
29 | task_thread.start() # 启动拉取任务 初始化一次 避免任务遗漏
30 | monitor_thread = threading.Thread(target=self.monitor_message, args=(message_queue, task_queue))
31 | monitor_thread.start() # 启动消息监听
32 | while True:
33 | try:
34 | task = task_queue.get(True, 1)
35 | except:
36 | continue
37 | else:
38 | DebugLogger("接受任务成功 启动执行进程 任务id: %s" % task["taskId"])
39 | case_result_queue = Queue()
40 | current_exec_status = Value("i", 0) # 0 执行中、 1 执行结束
41 | run_process = Process(target=self.run_test, args=(task, case_result_queue, current_exec_status))
42 | run_process.start()
43 | report_process = Process(target=self.push_result, args=(message_queue, case_result_queue))
44 | report_process.start()
45 | upload_process = Process(target=self.upload_image, args=(task, current_exec_status))
46 | upload_process.start()
47 | self.exec_processes[task["taskId"]] = [run_process, report_process, upload_process] # 保存当前进程
48 |
49 | def send_heartbeat(self, queue):
50 | while True:
51 | log_path = os.path.join(LOG_PATH, "engine_status.log")
52 | domain = self.config.url[:-1] if self.config.url.endswith("/") else self.config.url
53 | url = domain.replace("http", "ws") + "/websocket/engine/heartbeat?engineCode={}&engineSecret={}". \
54 | format(self.config.engine, self.config.secret)
55 | try:
56 | ws = Client(url, queue)
57 | ws.connect()
58 | while True:
59 | time.sleep(30)
60 | ws.send(bytes(0)) # 每隔30秒更新心跳
61 | DebugLogger("-------------------------------------------------", file_path=log_path)
62 | DebugLogger("心跳更新成功", file_path=log_path)
63 | DebugLogger("-------------------------------------------------", file_path=log_path)
64 | except KeyboardInterrupt:
65 | ws.close()
66 | except Exception as e:
67 | DebugLogger("-------------------------------------------------", file_path=log_path)
68 | ErrorLogger("心跳连接失败 1秒钟后重试 失败原因%s" % e, file_path=log_path)
69 | DebugLogger("-------------------------------------------------", file_path=log_path)
70 | time.sleep(1)
71 |
72 | def fetch_task(self, queue):
73 | while True:
74 | if len(self.exec_processes) < int(self.config.max_run):
75 | task = self.api.fetch_task()
76 | if task:
77 | self.exec_processes[task["taskId"]] = []
78 | DebugLogger("引擎获取任务成功 任务id: %s" % (task["taskId"]))
79 | queue.put(task)
80 | else: # 没有任务 停止获取
81 | break
82 | else:
83 | time.sleep(3)
84 |
85 | def monitor_message(self, message_queue, task_queue):
86 | while True:
87 | try:
88 | message = message_queue.get(True, 0.1)
89 | except:
90 | continue
91 | else:
92 | if message["type"] == "start":
93 | task_thread = threading.Thread(target=self.fetch_task, args=(task_queue,))
94 | task_thread.start()
95 | elif message["type"] == "stop":
96 | if message["data"] in self.exec_processes:
97 | processes = self.exec_processes[message["data"]]
98 | for process in processes:
99 | if process.is_alive():
100 | process.terminate()
101 | del self.exec_processes[message["data"]]
102 | DebugLogger("引擎终止任务成功 任务id: %s" % message["data"])
103 | elif message["type"] == "stopAll":
104 | for task_id, processes in self.exec_processes.items():
105 | for process in processes:
106 | if process.is_alive():
107 | process.terminate()
108 | DebugLogger("引擎终止任务成功 任务id: %s" % task_id)
109 | self.exec_processes.clear()
110 | else: # completed
111 | if message["data"] in self.exec_processes:
112 | del self.exec_processes[message["data"]]
113 |
114 | @staticmethod
115 | def run_test(task, queue, current_exec_status):
116 | s = LMSetting(task)
117 | plan = s.task_analysis()
118 | s.create_thread(plan, queue, current_exec_status)
119 |
120 | @staticmethod
121 | def push_result(message_queue, case_result_queue):
122 | report = LMReport(message_queue, case_result_queue)
123 | report.monitor_result()
124 |
125 | @staticmethod
126 | def upload_image(task, current_exec_status):
127 | log_path = os.path.join(LOG_PATH, "engine_image.log")
128 | current_process = psutil.Process(os.getpid())
129 | task_image_path = os.path.join(IMAGE_PATH, task["taskId"])
130 | if not os.path.exists(task_image_path):
131 | os.makedirs(task_image_path)
132 | while True:
133 | if current_process.parent() is None:
134 | current_process.kill()
135 | files = os.listdir(task_image_path)
136 | if len(files) > 0:
137 | DebugLogger("-------------------------------------------------", file_path=log_path)
138 | DebugLogger("上传截图", file_path=log_path)
139 | LMUpload(files, log_path).set_upload(task_image_path)
140 | DebugLogger("-------------------------------------------------", file_path=log_path)
141 | else:
142 | if current_exec_status.value:
143 | os.rmdir(task_image_path)
144 | current_process.terminate()
145 | time.sleep(1)
146 |
--------------------------------------------------------------------------------
/lm/lm_upload.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import threading
4 | from lm.lm_api import LMApi
5 |
6 |
7 | class LMUpload(object):
8 |
9 | def __init__(self, files, log_path):
10 | self.files = files
11 | self.log_path = log_path
12 | self.api = LMApi()
13 |
14 | def set_upload(self, task_image_path):
15 | threads = []
16 | for file in self.files:
17 | if file.endswith(".png"):
18 | uuid = file[:-4]
19 | thread = threading.Thread(target=self.upload, args=(task_image_path, uuid, file))
20 | threads.append(thread)
21 | else:
22 | os.remove(os.path.join(task_image_path, file))
23 | else:
24 | for t in threads:
25 | t.start()
26 | for t in threads:
27 | t.join()
28 |
29 | def upload(self, task_image_path, uuid, file):
30 | try:
31 | self.api.upload_screen_shot(task_image_path, uuid, self.log_path)
32 | os.remove(os.path.join(task_image_path, file))
33 | except:
34 | pass
35 |
36 |
--------------------------------------------------------------------------------
/lm/lm_ws.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from ws4py.client.threadedclient import WebSocketClient
4 | from lm.lm_config import LOG_PATH
5 | from lm.lm_log import DebugLogger
6 |
7 |
8 | class Client(WebSocketClient):
9 |
10 | def __init__(self, url, queue):
11 | self.queue = queue
12 | WebSocketClient.__init__(self, url)
13 | self.log_path = os.path.join(LOG_PATH, "engine_status.log")
14 |
15 | def opened(self):
16 | DebugLogger("-------------------------------------------------", file_path=self.log_path)
17 | DebugLogger("心跳连接成功", file_path=self.log_path)
18 | DebugLogger("-------------------------------------------------", file_path=self.log_path)
19 |
20 | def closed(self, code, reason=None):
21 | DebugLogger("-------------------------------------------------", file_path=self.log_path)
22 | DebugLogger("心跳关闭 原因%s %s" % (code, reason), file_path=self.log_path)
23 | DebugLogger("-------------------------------------------------", file_path=self.log_path)
24 |
25 | def received_message(self, resp):
26 | self.queue.put(json.loads(str(resp)))
27 |
28 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | assertpy==1.1
2 | certifi==2020.6.20
3 | chardet==3.0.4
4 | decorator==5.0.5
5 | Faker==6.0.0
6 | idna==2.10
7 | jsonpath==0.82
8 | jsonpath-ng==1.5.2
9 | ply==3.11
10 | psutil==5.8.0
11 | pypinyin==0.40.0
12 | python-dateutil==2.8.1
13 | requests==2.24.0
14 | selenium==3.141.0
15 | six==1.15.0
16 | text-unidecode==1.3
17 | urllib3==1.25.10
18 | uiautomator2==2.16.21
19 | facebook-wda==1.4.6
20 | pymssql==2.2.7
21 | PyMySQL==1.0.2
22 | cx_Oracle==8.3.0
23 | psycopg2==2.9.5
24 | ws4py~=0.5.1
25 | websocket~=0.2.1
26 |
--------------------------------------------------------------------------------
/startup.py:
--------------------------------------------------------------------------------
1 | from lm.lm_start import LMStart
2 |
3 |
4 | __version__ = "1.4.1"
5 |
6 |
7 | if __name__ == '__main__':
8 | print("-------------------------------------------------")
9 | print("当前所属版本号: %s" % __version__)
10 | print("流马测试引擎已启动")
11 | print("-------------------------------------------------")
12 | LMStart().main()
13 |
--------------------------------------------------------------------------------
/tools/funclib/__init__.py:
--------------------------------------------------------------------------------
1 | from .load_faker import CustomFaker
2 | import time
3 |
4 |
5 | def get_func_lib(test=None, lm_func=None, context=None, params=None):
6 | temp = {
7 | "context": context,
8 | "params": params
9 | }
10 | faker = CustomFaker(locale='zh_cn', package='provider', test=test, lm_func=lm_func, temp=temp)
11 | CustomFaker.seed(str(time.time()))
12 | return faker
13 |
14 |
--------------------------------------------------------------------------------
/tools/funclib/load_faker.py:
--------------------------------------------------------------------------------
1 | import os
2 | from faker import Faker
3 | from importlib import import_module, reload
4 | import sys
5 | from faker.providers import BaseProvider
6 | from tools.funclib.params_enum import PARAMS_ENUM
7 |
8 |
9 | class CustomFaker(Faker):
10 | def __init__(self, package='provider', test=None, lm_func=None, temp=None, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 | if lm_func is None:
13 | lm_func = []
14 | self.package = package
15 | self.test = test
16 | self.print = print
17 | self.lm_func = lm_func
18 | self.temp = temp
19 | self.func_param = PARAMS_ENUM
20 | self._load_module()
21 | self._load_lm_func()
22 |
23 | def __call__(self, name, *args, **kwargs):
24 | return getattr(self, name)(*args, **kwargs)
25 |
26 | def _read_module(self):
27 | module_path = os.path.join(os.path.dirname(__file__), self.package)
28 | module_list = []
29 | for file_name in os.listdir(module_path):
30 | if file_name[-2:] == "py":
31 | module_name = __package__ + "." + self.package + "." + file_name[0:-3]
32 | module_list.append(module_name)
33 | return module_list
34 |
35 | def _load_module(self):
36 | for name in self._read_module():
37 | if name not in sys.modules:
38 | module = import_module(name)
39 | else:
40 | module = sys.modules.get(name)
41 | reload(module)
42 | for value in module.__dict__.values():
43 | if type(value) is type and BaseProvider in value.__bases__:
44 | self.add_provider(value)
45 |
46 | def _load_lm_func(self):
47 | for custom in self.lm_func:
48 | func = self._lm_custom_func(custom["code"], custom["params"]["names"], self.test, self.temp)
49 | params = []
50 | for value in custom["params"]["types"]:
51 | if value == "Int":
52 | params.append(int)
53 | elif value == "Float":
54 | params.append(float)
55 | elif value == "Boolean":
56 | params.append(bool)
57 | elif value == "Bytes":
58 | params.append(bytes)
59 | elif value == "JSONObject":
60 | params.append(dict)
61 | elif value == "JSONArray":
62 | params.append(list)
63 | elif value == "Other":
64 | params.append(None)
65 | else:
66 | params.append(str)
67 | self.func_param[custom["name"]] = params
68 | setattr(self, custom["name"], func)
69 |
70 | def _lm_custom_func(self, code, params, test, temp):
71 | def func(*args):
72 | def print(*args, sep=' ', end='\n', file=None, flush=False):
73 | if file is None or file in (sys.stdout, sys.stderr):
74 | file = names["_test"].stdout_buffer
75 | self.print(*args, sep=sep, end=end, file=file, flush=flush)
76 |
77 | def sys_return(res):
78 | names["_exec_result"] = res
79 |
80 | def sys_get(name):
81 | if name in names["_test_context"]:
82 | return names["_test_context"][name]
83 | elif name in names["_test_params"]:
84 | return names["_test_params"][name]
85 | else:
86 | raise KeyError("不存在的公共参数或关联变量: {}".format(name))
87 |
88 | def sys_put(name, val, ps=False):
89 | if ps:
90 | names["_test_params"][name] = val
91 | else:
92 | names["_test_context"][name] = val
93 |
94 | names = locals()
95 | names["_test_context"] = temp["context"]
96 | names["_test_params"] = temp["params"]
97 | names["_test"] = test
98 | for index, value in enumerate(params):
99 | names[value] = args[index]
100 | exec(code)
101 | return names["_exec_result"]
102 | return func
103 |
--------------------------------------------------------------------------------
/tools/funclib/params_enum.py:
--------------------------------------------------------------------------------
1 | PARAMS_ENUM = {
2 | "bothify": [str, str],
3 | "lexify": [str, str],
4 | "numerify": [str],
5 | "random_int": [int, int, int],
6 | "random_number": [int, bool],
7 | "country_code": [str],
8 | "ean": [int],
9 | "localized_ean": [int],
10 | "color": [str, str, str],
11 | "credit_card_full": [str],
12 | "credit_card_number": [str],
13 | "credit_card_provider": [str],
14 | "credit_card_security_code": [str],
15 | "date": [str],
16 | "time": [str],
17 | "file_extension": [str],
18 | "file_name": [str, str],
19 | "file_path": [int, str, str],
20 | "mime_type": [str],
21 | "unix_device": [str],
22 | "unix_partition": [str],
23 | "domain_name": [int],
24 | "email": [str],
25 | "hostname": [int],
26 | "image_url": [int, int],
27 | "ipv4": [bool, str, bool],
28 | "ipv4_private": [bool, str],
29 | "ipv4_public": [bool, str],
30 | "ipv6": [bool],
31 | "uri_path": [int],
32 | "isbn10": [str],
33 | "isbn13": [str],
34 | "paragraph": [int, bool],
35 | "paragraphs": [int],
36 | "sentence": [int, bool],
37 | "sentences": [int],
38 | "text": [int],
39 | "texts": [int, int],
40 | "word": [int],
41 | "words": [int],
42 | "password": [int, bool, bool, bool, bool],
43 | "pyfloat": [int, int, bool],
44 | "loadfile": [str],
45 | "savefile": [str],
46 | "b64encode_str": [str],
47 | "b64encode_bytes": [bytes],
48 | "b64encode_file": [str],
49 | "b64decode_toStr": [str],
50 | "b64decode_toBytes": [str],
51 | "arithmetic": [str],
52 | "current_time": [str],
53 | "year_shift": [float, str],
54 | "month_shift": [float, str],
55 | "week_shift": [float, str],
56 | "date_shift": [float, str],
57 | "hour_shift": [float, str],
58 | "minute_shift": [float, str],
59 | "second_shift": [float, str],
60 | "lenof": [list],
61 | "indexof": [list, int],
62 | "keyof": [dict, str],
63 | "pinyin": [str],
64 | "substing": [str, int, int],
65 | "extract": [str],
66 | "replace": [str, str, str],
67 | "map_dumps": [dict],
68 | "array_dumps": [list],
69 | }
70 |
--------------------------------------------------------------------------------
/tools/funclib/provider/lm_provider.py:
--------------------------------------------------------------------------------
1 | import os
2 | from functools import reduce
3 | from faker.providers import BaseProvider
4 | import time
5 | from lm.lm_api import LMApi
6 | from pypinyin import lazy_pinyin
7 | import base64
8 | import datetime
9 | import json
10 | from dateutil.relativedelta import relativedelta
11 | from lm.lm_config import FILE_PATH
12 |
13 |
14 | class LiuMaProvider(BaseProvider):
15 |
16 | @staticmethod
17 | def loadfile(uuid):
18 | try:
19 | res = LMApi().download_test_file(uuid)
20 | except:
21 | raise Exception("拉取测试文件失败")
22 | else:
23 | return res.content
24 |
25 | @staticmethod
26 | def savefile(uuid):
27 | try:
28 | res = LMApi().download_test_file(uuid)
29 | except:
30 | raise Exception("拉取测试文件失败")
31 | else:
32 | file_name = res.headers.get("Content-Disposition").split("=")[1][1:-1]
33 | dir_path = os.path.join(FILE_PATH, uuid)
34 | file_path = os.path.join(dir_path, file_name)
35 | if not os.path.exists(dir_path):
36 | os.makedirs(dir_path)
37 | with open(file_path, 'wb+') as f:
38 | for chunk in res.iter_content(chunk_size=1024):
39 | if chunk:
40 | f.write(chunk)
41 | f.close()
42 | return file_path
43 |
44 | @staticmethod
45 | def b64encode_str(s: str):
46 | return base64.b64encode(s.encode('utf-8')).decode()
47 |
48 | @staticmethod
49 | def b64encode_bytes(s: bytes):
50 | return base64.b64encode(s).decode()
51 |
52 | def b64encode_file(self, uuid):
53 | content = self.loadfile(uuid)
54 | return base64.b64encode(content).decode()
55 |
56 | @staticmethod
57 | def b64decode_toStr(s: str):
58 | return base64.b64decode(s).decode()
59 |
60 | @staticmethod
61 | def b64decode_toBytes(s: str):
62 | return base64.b64decode(s)
63 |
64 | @staticmethod
65 | def arithmetic(expression: str):
66 | try:
67 | return eval(expression)
68 | except Exception:
69 | raise Exception("四则运算表达式错误:%s" % expression)
70 |
71 | @staticmethod
72 | def current_time(s: str = '%Y-%m-%d'):
73 | if s.lower() == "none":
74 | return int(time.time() * 1000)
75 | return time.strftime(s)
76 |
77 | @staticmethod
78 | def year_shift(shift, s: str = '%Y-%m-%d'):
79 | now_date = datetime.datetime.now()
80 | shift_date = now_date + relativedelta(years=shift)
81 | if s.lower() == "none":
82 | return int(shift_date.timestamp() * 1000)
83 | return shift_date.strftime(s)
84 |
85 | @staticmethod
86 | def month_shift(shift, s: str = '%Y-%m-%d'):
87 | now_date = datetime.datetime.now()
88 | shift_date = now_date + relativedelta(months=shift)
89 | if s.lower() == "none":
90 | return int(shift_date.timestamp() * 1000)
91 | return shift_date.strftime(s)
92 |
93 | @staticmethod
94 | def week_shift(shift, s: str = '%Y-%m-%d'):
95 | now_date = datetime.datetime.now()
96 | delta = datetime.timedelta(weeks=shift)
97 | shift_date = now_date + delta
98 | if s.lower() == "none":
99 | return int(shift_date.timestamp() * 1000)
100 | return shift_date.strftime(s)
101 |
102 | @staticmethod
103 | def date_shift(shift, s: str = '%Y-%m-%d'):
104 | now_date = datetime.datetime.now()
105 | delta = datetime.timedelta(days=shift)
106 | shift_date = now_date + delta
107 | if s.lower() == "none":
108 | return int(shift_date.timestamp() * 1000)
109 | return shift_date.strftime(s)
110 |
111 | @staticmethod
112 | def hour_shift(shift, s: str = '%Y-%m-%d %H:%M:%S'):
113 | now_date = datetime.datetime.now()
114 | delta = datetime.timedelta(hours=shift)
115 | shift_date = now_date + delta
116 | if s.lower() == "none":
117 | return int(shift_date.timestamp() * 1000)
118 | return shift_date.strftime(s)
119 |
120 | @staticmethod
121 | def minute_shift(shift, s: str = '%Y-%m-%d %H:%M:%S'):
122 | now_date = datetime.datetime.now()
123 | delta = datetime.timedelta(minutes=shift)
124 | shift_date = now_date + delta
125 | if s.lower() == "none":
126 | return int(shift_date.timestamp() * 1000)
127 | return shift_date.strftime(s)
128 |
129 | @staticmethod
130 | def second_shift(shift, s: str = '%Y-%m-%d %H:%M:%S'):
131 | now_date = datetime.datetime.now()
132 | delta = datetime.timedelta(seconds=shift)
133 | shift_date = now_date + delta
134 | if s.lower() == "none":
135 | return int(shift_date.timestamp() * 1000)
136 | return shift_date.strftime(s)
137 |
138 | @staticmethod
139 | def lenof(array):
140 | return len(array)
141 |
142 | @staticmethod
143 | def indexof(array, index):
144 | return array[index]
145 |
146 | @staticmethod
147 | def keyof(map, key):
148 | return map[key]
149 |
150 | @staticmethod
151 | def pinyin(cname: str):
152 | return reduce(lambda x, y: x + y, lazy_pinyin(cname))
153 |
154 | @staticmethod
155 | def substing(s, start: int=0, end: int=-1):
156 | return s[start:end]
157 |
158 | @staticmethod
159 | def extract(data):
160 | return data
161 |
162 | @staticmethod
163 | def replace(s, old, new):
164 | return s.replace(old, new)
165 |
166 | @staticmethod
167 | def map_dumps(tar):
168 | return json.dumps(tar)
169 |
170 | @staticmethod
171 | def array_dumps(tar):
172 | return json.dumps(tar)
173 |
--------------------------------------------------------------------------------
/tools/utils/sql.py:
--------------------------------------------------------------------------------
1 | import decimal
2 | import pymssql as mssql
3 | import pymysql as mysql
4 | import psycopg2 as pgsql
5 | import cx_Oracle as oracle
6 |
7 |
8 | class SQLConnect:
9 |
10 | def __init__(self, tpz, host, port, db, user, password):
11 | self.tpz = tpz
12 | self.host = host
13 | self.port = int(port)
14 | self.db = db
15 | self.user = user
16 | self.pwd = password
17 | self.conn = None
18 |
19 | def connect(self):
20 | """得到连接信息"""
21 | if self.tpz == "mysql":
22 | self.conn = mysql.connect(host=self.host, user=self.user,
23 | password=self.pwd, database=self.db, port=self.port, charset='utf8')
24 | elif self.tpz == "mssql":
25 | self.conn = mssql.connect(server=self.host, user=self.user,
26 | password=self.pwd, database=self.db, port=self.port, charset='utf8')
27 | elif self.tpz == "pgsql":
28 | self.conn = pgsql.connect(host=self.host, user=self.user,
29 | password=self.pwd, database=self.db, port=self.port)
30 | elif self.tpz == "oracle":
31 | try:
32 | self.conn = oracle.connect(self.user, self.pwd, f"{self.host}:{self.port}/{self.db}")
33 | except:
34 | sn = oracle.makedsn(self.host, self.port, sid=self.db)
35 | self.conn = oracle.connect(self.user, self.pwd, sn)
36 | else:
37 | raise TypeError("不支持的数据库类型")
38 | cur = self.conn.cursor()
39 | if not cur:
40 | raise RuntimeError("连接数据库失败")
41 | else:
42 | return cur
43 |
44 | def query(self, sql):
45 | """执行查询语句"""
46 | cur = self.connect()
47 | cur.execute(sql)
48 | resList = cur.fetchall()
49 | self.conn.close()
50 | results = []
51 | for res in resList:
52 | for index, value in enumerate(res):
53 | if len(results) < index + 1:
54 | results.append([])
55 | if isinstance(value, decimal.Decimal):
56 | if len(str(value).split(".")[1]) > 16:
57 | results[index].append(str(value))
58 | else:
59 | results[index].append(float(value))
60 | else:
61 | results[index].append(value)
62 | return results
63 |
64 | def exec(self, sql):
65 | """执行非查询语句"""
66 | cur = self.connect()
67 | cur.execute(sql)
68 | self.conn.commit()
69 | self.conn.close()
--------------------------------------------------------------------------------
/tools/utils/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 | from urllib.parse import quote
4 | import jsonpath
5 | import copy
6 |
7 |
8 | def extract_by_jsonpath(data: (dict,list, str), expression: str):
9 | if not isinstance(data, dict) and not isinstance(data, list):
10 | raise ExtractValueError('被提取的值不是json, 不支持jsonpath')
11 | value = jsonpath.jsonpath(data, expression)
12 | if value:
13 | return value[0] if len(value) == 1 else value
14 | else:
15 | raise ExtractValueError('jsonpath表达式错误: {}'.format(expression))
16 |
17 |
18 | def extract_by_regex(data: (dict, str), pattern: str):
19 | if isinstance(data, dict):
20 | content = json.dumps(data, ensure_ascii=False)
21 | else:
22 | content = data
23 | result = re.findall(pattern, content)
24 | if len(result) > 0:
25 | return result[0] if len(result) == 1 else result
26 | else:
27 | raise ExtractValueError("正则表达式匹配失败: {}".format(pattern))
28 |
29 |
30 | def quotation_marks(s):
31 | if s[0] in ["'", '"', b'\xe2\x80\x98'.decode('utf-8'), b'\xe2\x80\x99'.decode('utf-8'),
32 | b'\xe2\x80\x9c'.decode('utf-8'), b'\xe2\x80\x9d'.decode('utf-8')]:
33 | before = 1
34 | elif s[0:2] in ["\\'", '\\"']:
35 | before = 2
36 | else:
37 | return s
38 | # 后引号, 先判断转义的,在判断单个引号
39 | if s[-2:] in ["\\'", '\\"']:
40 | after = -2
41 | elif s[-1] in ["'", '"', b'\xe2\x80\x98'.decode('utf-8'), b'\xe2\x80\x99'.decode('utf-8'),
42 | b'\xe2\x80\x9c'.decode('utf-8'), b'\xe2\x80\x9d'.decode('utf-8')]:
43 | after = -1
44 | else:
45 | return s
46 | return s[before:after]
47 |
48 |
49 | def url_join(host: str, path: str):
50 | url = "" if host is None or host == "" else (host if host.endswith('/') else host + '/')
51 | api = "" if path is None or path == "" else (path[1:] if path.startswith('/') else path)
52 | return url + api
53 |
54 |
55 | def proxies_join(proxies: dict):
56 | if 'url' not in proxies or proxies['url'] is None or len(proxies['url']) == 0:
57 | raise ProxiesError("未设置代理网址")
58 | if not proxies['url'].startswith('http'):
59 | proxies['url'] = 'http://' + proxies['url']
60 | if 'username' not in proxies or proxies['username'] is None or len(proxies['username']) == 0:
61 | proxies['username'] = None
62 | else:
63 | proxies['username'] = quote(proxies['username'], safe='')
64 | if 'password' not in proxies or proxies['password'] is None or len(proxies['password']) == 0:
65 | proxies['password'] = None
66 | else:
67 | proxies['password'] = quote(proxies['password'], safe='')
68 | scheme = proxies['url'].split(':')[0]
69 | if proxies['username'] is not None and proxies['password'] is not None:
70 | pre, suf = proxies['url'].split('//', maxsplit=1)
71 | url = '{}//{}:{}@{}'.format(pre, proxies['username'], proxies['password'], suf)
72 | return {scheme: url}
73 | elif proxies['username'] is None and proxies['password'] is None:
74 | return {scheme: proxies['url']}
75 | else:
76 | raise ProxiesError("未设置代理账号或密码")
77 |
78 |
79 | def extract(name: str, data: (dict, list, str), expression: str):
80 | if name == 'jsonpath':
81 | return extract_by_jsonpath(data, expression)
82 | elif name == 'regular':
83 | return extract_by_regex(data, expression)
84 | else:
85 | raise ExtractValueError("未定义提取函数: {}".format(name))
86 |
87 |
88 | def get_case_message(data):
89 | if isinstance(data, dict):
90 | return data
91 | else:
92 | try:
93 | return json.loads(data)
94 | except json.decoder.JSONDecodeError:
95 | with open(data, 'rb') as f:
96 | return json.load(f)
97 |
98 |
99 | def handle_operation_data(data_type, data_value):
100 | try:
101 | if data_type == "JSONObject":
102 | data_value = eval(data_value)
103 | elif data_type == "JSONArray":
104 | data_value = eval(data_value)
105 | elif data_type == "Boolean":
106 | if data_value.lower() == "true":
107 | data_value = True
108 | else:
109 | data_value = False
110 | elif data_type == "Int":
111 | data_value = int(data_value)
112 | elif data_type == "Float":
113 | data_value = float(data_value)
114 | elif data_type == "Number":
115 | data_value = float(data_value) if "." in data_value else int(data_value)
116 | else:
117 | data_value = data_value
118 | except:
119 | pass
120 | return data_value
121 |
122 |
123 | def handle_params_data(params):
124 | result = {}
125 | for key, item in params.items():
126 | data_type = item["type"]
127 | data_value = item["value"]
128 | try:
129 | if data_type == "JSONObject":
130 | data_value = eval(data_value)
131 | elif data_type == "JSONArray":
132 | data_value = eval(data_value)
133 | elif data_type == "Boolean":
134 | if data_value.lower() == "true":
135 | data_value = True
136 | else:
137 | data_value = False
138 | elif data_type == "Int":
139 | data_value = int(data_value)
140 | elif data_type == "Float":
141 | data_value = float(data_value)
142 | except:
143 | pass
144 | result[key] = data_value
145 | return result
146 |
147 |
148 | def handle_form_data(form):
149 | form_data = {}
150 | form_file = {}
151 | for item in form:
152 | try:
153 | if item["type"] == "File":
154 | form_file[item["name"]] = "{{@loadfile(%s)}}" % item["value"]
155 | elif item["type"] == "JSONObject":
156 | form_data[item["name"]] = eval(item["value"])
157 | elif item["type"] == "JSONArray":
158 | form_data[item["name"]] = eval(item["value"])
159 | elif item["type"] == "Boolean":
160 | if item["value"].lower() == 'true':
161 | form_data[item["name"]] = True
162 | else:
163 | form_data[item["name"]] = False
164 | elif item["type"] == "Int":
165 | form_data[item["name"]] = int(item["value"])
166 | elif item["type"] == "Float":
167 | form_data[item["name"]] = float(item["value"])
168 | else:
169 | form_data[item["name"]] = item["value"]
170 | except:
171 | form_data[item["name"]] = item["value"]
172 | return form_data, form_file
173 |
174 |
175 | def handle_files(files):
176 | body_files = []
177 | for item in files:
178 | file_name = item["name"]
179 | file_value = "{{@loadfile(%s)}}" % item["id"]
180 | body_files.append(("file", (file_name, file_value)))
181 | return body_files
182 |
183 |
184 | def json_to_path(data):
185 | queue = [("$", data)]
186 | fina = {}
187 | while len(queue) != 0:
188 | (path, tar) = queue.pop()
189 | if len(tar) == 0:
190 | fina["%s" % path] = tar
191 | if isinstance(tar, dict):
192 | for key, value in tar.items():
193 | try:
194 | if key.isdigit():
195 | key = "'%s'" % str(key)
196 | except:
197 | key = "'%s'" % str(key)
198 | if isinstance(value, dict) or isinstance(value, list):
199 | queue.append(("%s.%s" % (path, key), value))
200 | else:
201 | fina["%s.%s" % (path, key)] = value
202 | else:
203 | for index, value in enumerate(tar):
204 | if isinstance(value, dict) or isinstance(value, list):
205 | queue.append(("%s[%d]" % (path, index), value))
206 | else:
207 | fina["%s[%d]" % (path, index)] = value
208 | return fina
209 |
210 |
211 | def relate_sort(data, data_from):
212 | not_relate_list = []
213 | relate_list = []
214 | for key, value in data.items():
215 | if "#{" in str(value):
216 | relate_list.append((key, value))
217 | else:
218 | not_relate_list.append((key, value))
219 | copy_list = copy.deepcopy(relate_list)
220 | sorted_list = []
221 | for index in range(len(relate_list)):
222 | for (key, value) in copy_list:
223 | for (com_key, com_value) in copy_list:
224 | if com_key[0:2] == "$.":
225 | json_path = com_key[2:]
226 | else:
227 | json_path = com_key[1:]
228 | if json_path in str(value) and com_key != key:
229 | break
230 | else:
231 | sorted_list.append((key, value))
232 | copy_list.remove((key, value))
233 | break
234 | for (key, value) in sorted_list:
235 | if data_from == "query":
236 | sign = "#{_request_query}"
237 | elif data_from == "headers":
238 | sign = "#{_request_header}"
239 | else:
240 | sign = "#{_request_body}"
241 | if sign in str(value).lower():
242 | sorted_list.remove((key, value))
243 | sorted_list.append((key, value))
244 | break
245 | return not_relate_list + sorted_list
246 |
247 |
248 | def get_json_relation(data: dict, data_from: str):
249 | return relate_sort(json_to_path(data), data_from)
250 |
251 |
252 | class ExtractValueError(Exception):
253 | """提取值失败"""
254 |
255 |
256 | class ProxiesError(Exception):
257 | """错误代理"""
258 |
--------------------------------------------------------------------------------