├── .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 | ![system](https://user-images.githubusercontent.com/96771570/221833391-9d35308a-3f90-47c7-9e9d-e62fc1201f18.png) 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 | ![qr](https://user-images.githubusercontent.com/96771570/161195670-3868f409-ed49-431f-8650-185e3e179679.png) 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 | --------------------------------------------------------------------------------