├── .gitignore ├── LICENSE ├── README.md ├── configs ├── allpath.ini ├── appiumService.ini ├── db.ini ├── email.ini ├── framework.ini ├── logging.conf ├── permission.json └── run.ini ├── framework ├── __init__.py ├── base │ ├── AppFileCtrl.py │ ├── DeviceInfoCtrl.py │ ├── DriverBaseCase.py │ ├── GetAllPathCtrl.py │ ├── PackageCtrl.py │ ├── PerformanceCtrl.py │ ├── PycFileCtrl.py │ └── __init__.py ├── core │ ├── __init__.py │ ├── adb │ │ ├── AdbCommand.py │ │ ├── EventKeys.py │ │ └── __init__.py │ ├── appiumapi │ │ ├── AppiumBaseApi.py │ │ └── __init__.py │ ├── dos │ │ ├── DosCommand.py │ │ └── __init__.py │ └── exceptions │ │ ├── Exception.py │ │ └── __init__.py ├── domain │ ├── __init__.py │ └── mobile_infos.py ├── initdriver │ ├── InitAppiumDriver.py │ ├── InitConfig.py │ └── __init__.py ├── initservice │ ├── InitService.py │ └── __init__.py └── utils │ ├── __init__.py │ ├── bat │ ├── startAppium.bat │ └── stopAppium.bat │ ├── databaseutils │ ├── ExcelDataUtil.py │ ├── SQLController.py │ └── __init__.py │ ├── emailutils │ ├── SendEmail.py │ └── __init__.py │ ├── fileutils │ ├── ConfigCommonUtil.py │ ├── CreateConfigUtil.py │ ├── FileCheckAndGetPath.py │ ├── JsonUtil.py │ ├── XMLCheckUtil.py │ ├── ZipUtil.py │ └── __init__.py │ ├── formatutils │ ├── DateTimeUtil.py │ └── __init__.py │ └── reporterutils │ ├── HtmlReportUtil.py │ ├── ImageUtil.py │ ├── LogWithConfUtil.py │ ├── LoggingUtil.py │ └── __init__.py ├── requirements.txt ├── test_case ├── __init__.py └── test_atp_base_api.py └── test_result ├── other └── android_devices_info.json └── screenshots └── find_element_by_want-20170417-223949366000.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | .idea/ 12 | testresult/ 13 | src/test_framework/ 14 | test_project/ 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # IPython Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppiumTestProject 2 | 3 | >从 2017 年后没再更新了,2022 再出发 4 | 5 | `Appium` 自动化测试工具,比较好用的自动化工具,值得学习和研究。这个工程主要是个人对 `Appium` 结合 `Python` 在 C 端进行自动化实施上理解的一个应用方案,可通过和使用该工程进行 app 自动化测试学习和研究。 6 | 工程还在一点一点的完善,补充的内容也一点一点的在时间轴中详细描述,工程具体的流程和代码逻辑可以 7 | 自行 review,代码中有相应的注释,如有需要也可以联系我, 8 | 希望你能通过这个工程获得一些你想获取的东西。 9 | 10 | ## 对于实施 UI 自动化前的技术选型考虑角度 11 | 12 | >不限于此 13 | 14 | 1. 脚本录制与回放 15 | 2. 脚本语言(是否支持团队技术基础上的语言) 16 | 3. 资源修改 17 | 4. 数据驱动 18 | 5. 数据驱动脚本自动转换 19 | 6. 组件自动同步 20 | 7. 模糊识别 21 | 8. 组件识别扩展 22 | 9. 动态检查(页面动态渲染) 23 | 10. 脚本扩展 24 | 11. 检查点 25 | 12. 识别组件对位置的依赖 26 | 13. 调试功能 27 | 14. 关键字驱动 28 | 29 | ## 项目依赖及环境准备 30 | 31 | 1. Python (3.7) 32 | 2. selenium (4.4.3) 33 | 3. [Appium-Python-Client (2.6.1)](https://appium.io/docs/en/about-appium/api/) 34 | 35 | - 环境准备 36 | 37 | 1. [appium (1.22.3-4)](https://github.com/appium/appium-desktop/releases/tag/v1.22.3-4) 38 | 2. miniconda ( 4.9.0) 39 | 3. adb 40 | 41 | ## 内容更新 42 | 43 | ### 2017.01.31 44 | 45 | 1.关于 appium 的通信服务如可通过代码进行开启、关闭、重启? 46 | 2.如何监控设备的链接状态?容错处理获取到的手机可用信息 47 | 3.app 版本迭代是否会影响到 Uiautomator Viewer的控件ID的获取? 48 | 49 | ### 2017.02.28 50 | 51 | 1.appium设计主链路功能UI测试的框架(数据驱动、关键字驱动) 52 | 2.实现测试过程中的性能数据收集 53 | 54 | ### 20170321 55 | 56 | - 需求:添加 adb cmd 的api 57 | - 项目下路径:framework/ui_test_api/adb/commond.py 58 | - 需求概述:将adb 调试命令 59 | 60 | ### 20170329 61 | 62 | 需求:封装 appium 基础的底层api, 在整个测试用例的编写过程 63 | 项目下路径: 64 | 需求概述: 65 | 66 | 1. 超时处理 67 | 2. 异常处理 68 | 3. 日志记录 69 | 4. 代码弱耦合 70 | 5. 逻辑强内聚 71 | 6. 减小创建的次数 72 | 7. APP监控了常用的men,cpu,fps 73 | 8. 设备重连机制 74 | 9. 邮件发送excel的测试报告 75 | 10. 支持多设备 andoird 并行 76 | 77 | ### 配置文件化-关于路径的操作 78 | 79 | pathconfig.ini中的配置所需项的相对路径,通过 getallpath 调用 configcommonctl 来解析拿到数据数据,getallpath作为对外接口 80 | 提供最直接的操作 81 | 82 | ### 20170405 83 | 84 | 1. 第一封装层的api,不应该有超过3复杂度的设计 85 | 2. 上层如果存在单一的逻辑直接写入底并提供调用方法 86 | 87 | ### 20170508 88 | 89 | 1. 使用 mysqldb 操作数据库 90 | 2. 使用 xlrd、xlwt和xlutils操作Excel文件 91 | 92 | ### 20170513 93 | 94 | 1. 如何进行多台设备进行同时执行?通过命令启动服务 95 | 96 | - 命令行参数: 97 | 98 | ```text 99 | -p: 是指定监听的端口(也可写成 --port),也可以修改为你需要的端口; 100 | 101 | -bp: (Android-only) 连接设备的端口号是连接Android设备bootstrap的端口号,默认是4724(也可写成--bootstrap-port) 102 | 103 | -U: 连接物理设备的唯一设备标识符,是连接的设备名称,如"adb devices"获取的设备标识(也可写成--udid) 104 | 105 | --chromedriver-port: 是chromedriver运行需要指定的端口号,默认是9515 106 | 107 | -a: 是指定监听的ip(也可写成 --address),后面“127.0.0.1”可以改为你需要的ip地址; 108 | 109 | --session-override: 是指覆盖之前的session; 110 | ``` 111 | 112 | - 启动对各服务端: 113 | 114 | ```text 115 | appium -p 4492 -bp 2251 -U udid_num 116 | appium -p 4493 -bp 2252 -U udid_num2 117 | ...... 118 | ``` 119 | 120 | - 客户端多个连接: 121 | 122 | ```text 123 | 在脚本的 capabilities.setCapability("udid","udid_num") 124 | driver.remote("http://127.0.0.1:4492/wd/hub",cpabilities") 125 | udid和对应启动的服务器的端口保持一致 126 | 端口生成、doc命令执行、获取设备列表、启动多服务器 127 | ``` 128 | 129 | 2. 现在做的逻辑就是:获取当前连接的设备数,启动相同数量的服务器并分配好未被占用的端口,同时要确认每个设备连接的是独立的服务端口 130 | 那么脚本必须做到多线程执行,不然会报错 131 | 3. 编写实际的自动化脚本时,记得先实力化server服务,然后进行多线程的设计(待完成) 132 | 133 | ### 20170728 20170730 (计划) 134 | 135 | 获取app的启动和首页的activity,可以通过查看apk包和已安装的app 136 | 137 | **aapt dump badging a.apk** 138 | **adb logcat -c && adb logcat -s ActivityManager** 139 | 140 | 1. 启动参数配置化(run.ini),在配置文件中读取驱动app启动的desired capabilities参数、还有关于是否重新安装的开关值 141 | apk的安装包路径、app的启动activity、app的首页activity 142 | 2. 是否重新安装的开关值,为0时,检查是否安装,若安装了先卸载,再安装;为1时,检查是否安装,没有安装就先安装再执行后续操作 143 | ,若安装了,就直接继续后续操作 144 | 3. 根据udid来过去设备对应的port(当然多设备的时候,需要将配置文件中的内容设置为list,逗号隔开) 145 | 4. 在脚本获取appium driver时,在线程中实例化一个线程,并返回这个driver 146 | 147 | ### 20170801 148 | 149 | 1. 如果进行渠道包验证(指定目录下的所有apk,一部或多部手机,多线程数据共享) (待完成) 150 | 2. 修改command.py中的app的安装、卸载和是否安装等方法; 151 | 3. run.ini配置文件中的内容,不在进行代码设计,因为通过代码来获取app的启动activity和首页activity,没有多少现实意义,后续有时间可以考虑添加该功能; 152 | 4. 修改initappiumdriver中的数据获取方式; 153 | 154 | ### 20170802 155 | 156 | 1. APPIUM DRIVER已经设计完成,需要后续再多线程实例化和初始化上做一个详细的流程。 157 | 2. 初次启动服务并实例化driver,会出现 urllib.error.URLError,因为服务启动占用了端口,但是正式的服务内容还没有启动完成,在此时去Remote(url)就报错了, 158 | 放弃之前使用的超时、校验端口的方式,使用异常处理的方式来建立driver。 159 | 3. 同时解除InitDriverOption与ServicePort的耦合。 160 | 4. 改造InitService.generate_service_command中的数据形式,使用dict代替list。 161 | 162 | ### 20170804 163 | 164 | 1. 做了个实验,python自己的日志模块做的很好,就像自己做一下封装,当前的日志模块可以有两种实现方式: 165 | - 1.将所有日志等级的handler在类的__init__方法中实例化 166 | - 2.在类的之前以普通方式实例handler 167 | - 3.使用配置文件的方式设置logging,其中一个是fileconfig,一个是dictconfig 168 | 2. 第一种方法会出现同一个日志level的logger,会有个handler,也就是会重复打印n个日志内容。而第二中方法不会出现这种情况,如果想使用第一种方法,也可以,就是在 169 | 打印日志后,将当前的handler关闭并移除当前的logger.handlers[i] 170 | 3. fileconfig和dictconfig比较方便的使用,但是日志的格式无法自定义,但是可以自己进行封装。 171 | 4. 现在项目中已经demo好了四种日志模板,可以用到任意项目中。 172 | 173 | ### 20171023 174 | 175 | 1. 通过ServicePort进行初始化的服务并生成的ini配置文件中添加一个run字段,如果为0:未执行;为1:执行过 176 | 2. InitDriverOption中初始化appiumdriver时,首先读取第一步生成的配置文件如果有 177 | 178 | ### 20171027 179 | 180 | 1. 优化一下项目管理 181 | 2. 测试脚本设计的一个建议,在创建线程前,先实例化appium服务,这时候通过配置文件来获取sno和port, 182 | 随后有了唯一设备的driver,然后就去执行脚本 183 | 184 | ### 20171221 185 | 186 | 1. 优化了启动后台appium服务的逻辑,及采用了线程方式来执行启动服务的命令。 187 | 2. 多机执行将会使用multiprocessing.Pool.map_async(caseFunc, driverList) 188 | 3. 启动服务后将bootstrap对应的端口也写入配置文件中,以便关闭appium服务的时候同时关闭该端口 189 | -------------------------------------------------------------------------------- /configs/allpath.ini: -------------------------------------------------------------------------------- 1 | [dumpxmlPath] 2 | dumpxmlPath=test_result\dumpxml\ 3 | #html报告路径 4 | [htmlreportPath] 5 | htmlreportPath=test_result\htmlreports\ 6 | [logsPath] 7 | logsPath=test_result\logs4script\ 8 | [appiumlogPath] 9 | appiumlogPath=test_result\logs4appium\ 10 | [capturePath] 11 | capturePath=test_result\screenshots\ 12 | #浏览器初始化界面URL 13 | [permissionPath] 14 | permissionPath=configs\ 15 | [baseURL] 16 | baseURL=http://localhost:8080/Lab_linux/stusign 17 | -------------------------------------------------------------------------------- /configs/appiumService.ini: -------------------------------------------------------------------------------- 1 | [90d1894b7d62] 2 | 90d1894b7d62 = 4490 3 | bp = 2233 4 | run = 0 5 | 6 | [8DF6R17327000259] 7 | 8df6r17327000259 = 4490 8 | bp = 2233 9 | run = 0 10 | 11 | [4db89e4a] 12 | 4db89e4a = 4491 13 | bp = 2234 14 | run = 0 15 | 16 | [933733967ce4] 17 | 933733967ce4 = 4490 18 | bp = 2233 19 | run = 0 20 | 21 | -------------------------------------------------------------------------------- /configs/db.ini: -------------------------------------------------------------------------------- 1 | [dbset] 2 | #服务器地址 3 | host=127.0.0.1 4 | #host192.168.38.129 5 | #端口号,默认是3306 6 | port=3306 7 | #要登陆的用户名 8 | #user=root 9 | user=root 10 | #所要登录用户的秘密 11 | #passwd=root 12 | passwd=root 13 | #所要链接的数据库:lab_digital_platform 14 | db=lab 15 | #设置链接的编码 16 | charset=utf8 -------------------------------------------------------------------------------- /configs/email.ini: -------------------------------------------------------------------------------- 1 | [emails] 2 | # smtp.163.com smtp.qq.com smtp.sina.com 3 | smtp_host=smtp.sina.com 4 | pop3_host=pop.163.com 5 | # 间隔是因为逗号 6 | receiver=jayzhen_testing@163.com 7 | receiver_pa= 8 | sender=jayzhen_oops@sina.com 9 | sender_pa= -------------------------------------------------------------------------------- /configs/framework.ini: -------------------------------------------------------------------------------- 1 | [TimeSet] 2 | #页面加载等待时间,单位:秒 3 | pageLoadTimeout=10 4 | #定位元素等待时间,单位:秒 5 | waitTimeout=1 6 | #异步加载等待时间 7 | scriptTimeout=10 8 | #单位:毫秒 9 | pauseTime=1000 10 | #截图保存的路径 11 | 12 | -------------------------------------------------------------------------------- /configs/logging.conf: -------------------------------------------------------------------------------- 1 | #logger.conf 2 | ############################################### 3 | [loggers] 4 | keys=root, debug, info, warning, error 5 | 6 | [logger_root] 7 | level=DEBUG 8 | handlers = h_debug, h_info, h_warning, h_error 9 | 10 | [logger_debug] 11 | handlers = h_debug 12 | qualname = debug 13 | propagate= 0 14 | 15 | [logger_info] 16 | handlers = h_info 17 | qualname = info 18 | propagate= 0 19 | 20 | [logger_warning] 21 | handlers = h_warning 22 | qualname = warning 23 | propagate= 0 24 | 25 | [logger_error] 26 | handlers = h_error 27 | qualname = error 28 | propagate= 0 29 | 30 | ############################################### 31 | [handlers] 32 | keys = h_debug, h_info, h_warning, h_error 33 | 34 | [handler_h_debug] 35 | class = FileHandler 36 | level = DEBUG 37 | formatter= format01 38 | args= ('../testresult/log4appium/logging_debug.log', 'a') 39 | 40 | [handler_h_info] 41 | class = FileHandler 42 | level = INFO 43 | formatter= format01 44 | args= ('../testresult/log4appium/logging_info.log', 'a') 45 | 46 | [handler_h_warning] 47 | class = FileHandler 48 | level = WARNING 49 | formatter= format01 50 | args= ('../testresult/log4appium/logging_warning.log', 'a') 51 | 52 | [handler_h_error] 53 | class = FileHandler 54 | level = ERROR 55 | formatter= format01 56 | args= ('../testresult/log4appium/logging_error.log', 'a') 57 | 58 | [handler_rotat_hand] 59 | class=handlers.RotatingFileHandler 60 | level=INFO 61 | formatter=format01 62 | args=('myapp3.log', 'a', 10*1024*1024, 5) 63 | 64 | ############################################### 65 | 66 | [formatters] 67 | keys = format01 68 | 69 | [formatter_format01] 70 | # [%(name)s] %(module)s %(filename)s %(processName)s - %(threadName)s [%(pathname)s] [%(funcName)s-line:%(lineno)d]: 71 | format = [%(asctime)-10s] [%(levelname)s] %(message)s 72 | # 如果配置了datefmt会覆盖掉asctime的格式 73 | # datefmt = %a, %d %b %Y %H:%M:%S 74 | 75 | 76 | -------------------------------------------------------------------------------- /configs/permission.json: -------------------------------------------------------------------------------- 1 | { 2 | "PermissList": [ 3 | { 4 | "Key": "android.permission.ACCESS_CHECKIN_PROPERTIES", 5 | "Title": "访问检入属性", 6 | "Memo": "允许对检入服务上传的属性进行读/写访问。普通应用程序不能使用此权限。", 7 | "Level": 0 8 | }, 9 | { 10 | "Key": "android.permission.ACCESS_COARSE_LOCATION", 11 | "Title": "大概位置", 12 | "Memo": "访问大概的位置源(例如蜂窝网络数据库)以确定手机的大概位置(如果可以)。恶意应用程序可借此确定您所处的大概位置。", 13 | "Level": 1 14 | }, 15 | { 16 | "Key": "android.permission.ACCESS_FINE_LOCATION", 17 | "Title": "精准的(GPS)位置", 18 | "Memo": "访问精准的位置源,例如手机上的全球定位系统(如果有)。恶意应用程序可能会借此确定您所处的位置,并可能消耗额外的电池电量。", 19 | "Level": 1 20 | }, 21 | { 22 | "Key": "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS", 23 | "Title": "访问额外的位置信息提供程序命令", 24 | "Memo": "访问额外的位置信息提供程序命令。恶意应用程序可借此干扰 GPS 或其他位置源的正常工作。", 25 | "Level": 1 26 | }, 27 | { 28 | "Key": "android.permission.ACCESS_MOCK_LOCATION", 29 | "Title": "使用模拟地点来源进行测试", 30 | "Memo": "创建模拟地点来源进行测试。恶意应用程序可能利用此选项覆盖由真实地点来源(如 GPS 或网络提供商)传回的地点和/或状态。", 31 | "Level": 1 32 | }, 33 | { 34 | "Key": "android.permission.ACCESS_NETWORK_STATE", 35 | "Title": "查看网络状态", 36 | "Memo": "允许应用程序查看所有网络的状态。", 37 | "Level": 0 38 | }, 39 | { 40 | "Key": "android.permission.ACCESS_SURFACE_FLINGER", 41 | "Title": "访问 SurfaceFlinger", 42 | "Memo": "允许应用程序使用 SurfaceFlinger 低级别功能。", 43 | "Level": 0 44 | }, 45 | { 46 | "Key": "android.permission.ACCESS_WIFI_STATE", 47 | "Title": "查看 WLAN 状态", 48 | "Memo": "允许应用程序查看有关 WLAN 状态的信息。", 49 | "Level": 0 50 | }, 51 | { 52 | "Key": "android.permission.ADD_SYSTEM_SERVICE", 53 | "Title": "系统级服务", 54 | "Memo": "允许程序发布系统级服务", 55 | "Level": 0 56 | }, 57 | { 58 | "Key": "android.permission.BATTERY_STATS", 59 | "Title": "修改电池统计信息", 60 | "Memo": "允许修改收集的电池使用情况统计信息。普通应用程序不能使用此权限。", 61 | "Level": 0 62 | }, 63 | { 64 | "Key": "android.permission.BLUETOOTH", 65 | "Title": "创建蓝牙连接", 66 | "Memo": "允许应用程序查看本地蓝牙手机的配置,以及建立或接受与配对设备的连接。", 67 | "Level": 1 68 | }, 69 | { 70 | "Key": "android.permission.BLUETOOTH_ADMIN", 71 | "Title": "蓝牙管理", 72 | "Memo": "允许应用程序配置本地蓝牙手机,以及发现远程设备并与其配对。", 73 | "Level": 0 74 | }, 75 | { 76 | "Key": "android.permission.BRICK", 77 | "Title": "永久停用手机", 78 | "Memo": "允许应用程序永久停用整个手机,这非常危险。", 79 | "Level": 2 80 | }, 81 | { 82 | "Key": "android.permission.BROADCAST_PACKAGE_REMOVED", 83 | "Title": "发送包删除的广播", 84 | "Memo": "允许应用程序广播已删除某应用程序包的通知。恶意应用程序可借此终止任何正在运行的其他应用程序。", 85 | "Level": 1 86 | }, 87 | { 88 | "Key": "android.permission.BROADCAST_STICKY", 89 | "Title": "发送置顶广播", 90 | "Memo": "允许应用程序发送顽固广播,这些广播在结束后仍会保留。恶意应用程序可能会借此使手机耗用太多内存,从而降低其速度或稳定性。", 91 | "Level": 1 92 | }, 93 | { 94 | "Key": "android.permission.CALL_PHONE", 95 | "Title": "直接拨打电话号码", 96 | "Memo": "允许应用程序在您不介入的情况下拨打电话。恶意应用程序可借此在您的话费单上产生意外通话费。请注意,此权限不允许应用程序拨打紧急呼救电话。", 97 | "Level": 2 98 | }, 99 | { 100 | "Key": "android.permission.CALL_PRIVILEGED", 101 | "Title": "直接呼叫任何电话号码", 102 | "Memo": "允许应用程序在您不介入的情况下拨打任何电话(包括紧急呼救)。恶意应用程序可借此向应急服务机构拨打骚扰电话甚至非法电话。", 103 | "Level": 2 104 | }, 105 | { 106 | "Key": "android.permission.CAMERA", 107 | "Title": "拍照", 108 | "Memo": "允许应用程序使用相机拍照,这样应用程序可随时收集进入相机镜头的图像。", 109 | "Level": 0 110 | }, 111 | { 112 | "Key": "android.permission.CHANGE_COMPONENT_ENABLED_STATE", 113 | "Title": "启用或停用应用程序组件", 114 | "Memo": "允许应用程序更改是否启用其他应用程序的组件。恶意应用程序可借此停用重要的手机功能。使用此权限时务必谨慎,因为这可能导致应用程序组件进入不可用、不一致或不稳定的状态。", 115 | "Level": 1 116 | }, 117 | { 118 | "Key": "android.permission.CHANGE_CONFIGURATION", 119 | "Title": "更改用户界面设置", 120 | "Memo": "允许应用程序更改当前配置,例如语言设置或整体的字体大小。", 121 | "Level": 0 122 | }, 123 | { 124 | "Key": "android.permission.CHANGE_NETWORK_STATE", 125 | "Title": "更改网络连接性", 126 | "Memo": "允许应用程序更改网络连接的状态。", 127 | "Level": 0 128 | }, 129 | { 130 | "Key": "android.permission.CHANGE_WIFI_STATE", 131 | "Title": "更改 WLAN 状态", 132 | "Memo": "允许应用程序连接到 WLAN 接入点以及与 WLAN 接入点断开连接,并对配置的 WLAN 网络进行更改。", 133 | "Level": 0 134 | }, 135 | { 136 | "Key": "android.permission.CLEAR_APP_CACHE", 137 | "Title": "删除所有应用程序缓存数据", 138 | "Memo": "允许应用程序通过删除应用程序缓存目录中的文件释放手机存储空间。通常此权限只适用于系统进程。", 139 | "Level": 0 140 | }, 141 | { 142 | "Key": "android.permission.CLEAR_APP_USER_DATA", 143 | "Title": "删除其他应用程序的数据", 144 | "Memo": "允许应用程序清除用户数据。", 145 | "Level": 1 146 | }, 147 | { 148 | "Key": "android.permission.CONTROL_LOCATION_UPDATES", 149 | "Title": "控制位置更新通知", 150 | "Memo": "允许启用/停用来自收音机的位置更新通知。普通应用程序不能使用此权限。", 151 | "Level": 0 152 | }, 153 | { 154 | "Key": "android.permission.DELETE_CACHE_FILES", 155 | "Title": "删除其他应用程序的缓存", 156 | "Memo": "允许应用程序删除缓存文件。", 157 | "Level": 0 158 | }, 159 | { 160 | "Key": "android.permission.DELETE_PACKAGES", 161 | "Title": "删除应用程序", 162 | "Memo": "允许应用程序删除 Android 包。恶意应用程序可借此删除重要的应用程序。", 163 | "Level": 0 164 | }, 165 | { 166 | "Key": "android.permission.DEVICE_POWER", 167 | "Title": "开机或关机", 168 | "Memo": "允许应用程序打开或关闭手机。", 169 | "Level": 0 170 | }, 171 | { 172 | "Key": "android.permission.DIAGNOSTIC", 173 | "Title": "读取/写入诊断所拥有的资源", 174 | "Memo": "允许应用程序读取/写入诊断组所拥有的任何资源(例如,/dev 中的文件)。这可能会影响系统稳定性和安全性。此权限仅供制造商或运营商诊断硬件问题。", 175 | "Level": 2 176 | }, 177 | { 178 | "Key": "android.permission.DISABLE_KEYGUARD", 179 | "Title": "停用键锁", 180 | "Memo": "允许应用程序停用键锁和任何关联的密码安全设置。例如,在手机上接听电话时停用键锁,在通话结束后重新启用键锁。", 181 | "Level": 0 182 | }, 183 | { 184 | "Key": "android.permission.DUMP", 185 | "Title": "检索系统内部状态", 186 | "Memo": "允许应用程序检索系统的内部状态。恶意应用程序可借此检索它们本不需要的各种保密信息和安全信息。", 187 | "Level": 0 188 | }, 189 | { 190 | "Key": "android.permission.EXPAND_STATUS_BAR", 191 | "Title": "展开/收拢状态栏", 192 | "Memo": "允许应用程序展开或收拢状态栏。", 193 | "Level": 0 194 | }, 195 | { 196 | "Key": "android.permission.FACTORY_TEST", 197 | "Title": "在出厂测试模式下运行", 198 | "Memo": "作为一项低级制造商测试来运行,从而允许对手机硬件进行完全访问。此权限仅当手机在制造商测试模式下运行时才可用。", 199 | "Level": 0 200 | }, 201 | { 202 | "Key": "android.permission.FLASHLIGHT", 203 | "Title": "控制闪光灯", 204 | "Memo": "允许应用程序控制闪光灯。", 205 | "Level": 0 206 | }, 207 | { 208 | "Key": "android.permission.FORCE_BACK", 209 | "Title": "强制应用程序关闭", 210 | "Memo": "允许应用程序强制前端的任何活动关闭并重新开始。普通应用程序从不需要使用此权限。", 211 | "Level": 0 212 | }, 213 | { 214 | "Key": "android.permission.FOTA_UPDATE", 215 | "Title": "系统升级", 216 | "Memo": "运行应用程序使用空中升级系统", 217 | "Level": 0 218 | }, 219 | { 220 | "Key": "android.permission.GET_ACCOUNTS", 221 | "Title": "发现已知帐户", 222 | "Memo": "允许应用程序获取手机已知的帐户列表。", 223 | "Level": 0 224 | }, 225 | { 226 | "Key": "android.permission.GET_PACKAGE_SIZE", 227 | "Title": "计算应用程序存储空间", 228 | "Memo": "允许应用程序检索其代码、数据和缓存大小", 229 | "Level": 0 230 | }, 231 | { 232 | "Key": "android.permission.GET_TASKS", 233 | "Title": "检索当前运行的应用程序", 234 | "Memo": "允许应用程序检索有关当前和最近运行的任务的信息。恶意应用程序可借此发现有关其他应用程序的保密信息。", 235 | "Level": 1 236 | }, 237 | { 238 | "Key": "android.permission.HARDWARE_TEST", 239 | "Title": "测试硬件", 240 | "Memo": "允许应用程序控制各外围设备以进行硬件测试。", 241 | "Level": 0 242 | }, 243 | { 244 | "Key": "android.permission.INJECT_EVENTS", 245 | "Title": "按键和控制按钮", 246 | "Memo": "允许应用程序将其自己的输入活动(按键等)提供给其他应用程序。恶意应用程序可借此掌控手机。", 247 | "Level": 2 248 | }, 249 | { 250 | "Key": "android.permission.INSTALL_PACKAGES", 251 | "Title": "直接安装应用程序", 252 | "Memo": "允许应用程序安装全新的或更新的 Android 包。恶意应用程序可能会借此添加其具有任意权限的新应用程序。", 253 | "Level": 1 254 | }, 255 | { 256 | "Key": "android.permission.INTERNAL_SYSTEM_WINDOW", 257 | "Title": "显示未授权的窗口", 258 | "Memo": "允许创建专用于内部系统用户界面的窗口。普通应用程序不能使用此权限。", 259 | "Level": 0 260 | }, 261 | { 262 | "Key": "android.permission.INTERNET", 263 | "Title": "访问网络", 264 | "Memo": "允许程序访问网络.", 265 | "Level": 0 266 | }, 267 | { 268 | "Key": "android.permission.MANAGE_APP_TOKENS", 269 | "Title": "管理应用程序令牌", 270 | "Memo": "允许应用程序创建和管理自己的令牌,从而绕开其常规的 Z 方向。普通应用程序从不需要使用此权限。", 271 | "Level": 0 272 | }, 273 | { 274 | "Key": "android.permission.MASTER_CLEAR", 275 | "Title": "恢复出厂设置", 276 | "Memo": "允许应用程序将系统恢复为出厂设置,即清除所有数据、配置以及所安装的应用程序。", 277 | "Level": 2 278 | }, 279 | { 280 | "Key": "android.permission.MODIFY_AUDIO_SETTINGS", 281 | "Title": "更改您的音频设置", 282 | "Memo": "允许应用程序修改整个系统的音频设置,如音量和路由。", 283 | "Level": 0 284 | }, 285 | { 286 | "Key": "android.permission.MODIFY_PHONE_STATE", 287 | "Title": "修改手机状态", 288 | "Memo": "允许应用程序控制设备的电话功能。拥有此权限的应用程序可自行切换网络、打开和关闭无线通信等,而不会通知您。", 289 | "Level": 1 290 | }, 291 | { 292 | "Key": "android.permission.MOUNT_UNMOUNT_FILESYSTEMS", 293 | "Title": "装载和卸载文件系统", 294 | "Memo": "允许应用程序装载和卸载可移动存储器的文件系统。", 295 | "Level": 0 296 | }, 297 | { 298 | "Key": "android.permission.PERSISTENT_ACTIVITY", 299 | "Title": "让应用程序始终运行", 300 | "Memo": "允许应用程序部分持续运行,这样系统便不能将其用于其他应用程序。", 301 | "Level": 0 302 | }, 303 | { 304 | "Key": "android.permission.PROCESS_OUTGOING_CALLS", 305 | "Title": "拦截外拨电话", 306 | "Memo": "允许应用程序处理外拨电话或更改要拨打的号码。恶意应用程序可能会借此监视、另行转接甚至阻止外拨电话。", 307 | "Level": 2 308 | }, 309 | { 310 | "Key": "android.permission.READ_CALENDAR", 311 | "Title": "读取日历活动", 312 | "Memo": "允许应用程序读取您手机上存储的所有日历活动。恶意应用程序可借此将您的日历活动发送给其他人。", 313 | "Level": 1 314 | }, 315 | { 316 | "Key": "android.permission.READ_CONTACTS", 317 | "Title": "读取联系人数据", 318 | "Memo": "允许应用程序读取您手机上存储的所有联系人(地址)数据。恶意应用程序可借此将您的数据发送给其他人。", 319 | "Level": 1 320 | }, 321 | { 322 | "Key": "android.permission.READ_FRAME_BUFFER", 323 | "Title": "读取帧缓冲区", 324 | "Memo": "允许应用程序读取帧缓冲区中的内容,比如抓屏程序.", 325 | "Level": 0 326 | }, 327 | { 328 | "Key": "android.permission.READ_INPUT_STATE", 329 | "Title": "记录您键入的内容和执行的操作", 330 | "Memo": "允许应用程序查看您按的键,即使在与其他应用程序交互(例如输入密码)时也不例外。普通应用程序从不需要使用此权限。", 331 | "Level": 2 332 | }, 333 | { 334 | "Key": "android.permission.READ_LOGS", 335 | "Title": "读取系统日志文件", 336 | "Memo": "允许应用程序从系统的各日志文件中读取信息。这样应用程序可以发现您的手机使用情况,但这些信息不应包含任何个人信息或保密信息。", 337 | "Level": 0 338 | }, 339 | { 340 | "Key": "android.permission.READ_OWNER_DATA", 341 | "Title": "读取所有者数据", 342 | "Memo": "允许应用程序读取您手机上存储的手机所有者数据。恶意应用程序可借此读取手机所有者数据。", 343 | "Level": 1 344 | }, 345 | { 346 | "Key": "android.permission.READ_SMS", 347 | "Title": "读取短信或彩信", 348 | "Memo": "允许应用程序读取您的手机或 SIM 卡中存储的短信。恶意应用程序可借此读取您的机密信息。", 349 | "Level": 2 350 | }, 351 | { 352 | "Key": "android.permission.READ_SYNC_SETTINGS", 353 | "Title": "读取同步设置", 354 | "Memo": "允许应用程序读取同步设置,例如是否为(联系人)启用同步。", 355 | "Level": 0 356 | }, 357 | { 358 | "Key": "android.permission.READ_SYNC_STATS", 359 | "Title": "读取同步统计信息", 360 | "Memo": "允许应用程序读取同步统计信息;例如已发生的同步历史记录。", 361 | "Level": 0 362 | }, 363 | { 364 | "Key": "android.permission.REBOOT", 365 | "Title": "强行重新启动手机", 366 | "Memo": "允许应用程序强行重新启动手机。", 367 | "Level": 1 368 | }, 369 | { 370 | "Key": "android.permission.RECEIVE_BOOT_COMPLETED", 371 | "Title": "开机时自动启动", 372 | "Memo": "允许应用程序在系统完成启动后即自行启动。这样会延长手机的启动时间,而且如果应用程序一直运行,会降低手机的整体速度。", 373 | "Level": 1 374 | }, 375 | { 376 | "Key": "android.permission.RECEIVE_MMS", 377 | "Title": "接收彩信", 378 | "Memo": "允许应用程序接收和处理彩信。恶意应用程序可借此监视您的信息,或者将信息删除而不向您显示。", 379 | "Level": 1 380 | }, 381 | { 382 | "Key": "android.permission.RECEIVE_SMS", 383 | "Title": "接收短信", 384 | "Memo": "允许应用程序接收和处理短信。恶意应用程序可借此监视您的信息,或者将信息删除而不向您显示。", 385 | "Level": 1 386 | }, 387 | { 388 | "Key": "android.permission.RECEIVE_WAP_PUSH", 389 | "Title": "接收 WAP", 390 | "Memo": "允许应用程序接收和处理 WAP 信息。恶意应用程序可借此监视您的信息,或者将信息删除而不向您显示。", 391 | "Level": 1 392 | }, 393 | { 394 | "Key": "android.permission.RECORD_AUDIO", 395 | "Title": "录音", 396 | "Memo": "允许应用程序访问录音路径。", 397 | "Level": 1 398 | }, 399 | { 400 | "Key": "android.permission.REORDER_TASKS", 401 | "Title": "对正在运行的应用程序重新排序", 402 | "Memo": "允许应用程序将任务移至前端和后台。恶意应用程序可借此强行进入前端,而不受您的控制。", 403 | "Level": 1 404 | }, 405 | { 406 | "Key": "android.permission.RESTART_PACKAGES", 407 | "Title": "重启程序", 408 | "Memo": "允许程序自己重启或重启其他程序", 409 | "Level": 0 410 | }, 411 | { 412 | "Key": "android.permission.SEND_SMS", 413 | "Title": "发送短信", 414 | "Memo": "允许应用程序发送短信。恶意应用程序可能会不经您的确认就发送信息,给您带来费用。", 415 | "Level": 2 416 | }, 417 | { 418 | "Key": "android.permission.SET_ACTIVITY_WATCHER", 419 | "Title": "监控所有应用程序的启动", 420 | "Memo": "允许应用程序监控系统启动活动的方式。恶意应用程序可借此彻底损坏系统。此权限仅在开发时才需要,普通的手机应用不需要。", 421 | "Level": 1 422 | }, 423 | { 424 | "Key": "android.permission.SET_ALWAYS_FINISH", 425 | "Title": "关闭所有后台应用程序", 426 | "Memo": "允许应用程序控制活动是否始终是一转至后台就完成。普通应用程序从不需要使用此权限。", 427 | "Level": 0 428 | }, 429 | { 430 | "Key": "android.permission.SET_ANIMATION_SCALE", 431 | "Title": "修改全局动画速度", 432 | "Memo": "允许应用程序随时更改全局动画速度(加快或放慢动画)。", 433 | "Level": 0 434 | }, 435 | { 436 | "Key": "android.permission.SET_DEBUG_APP", 437 | "Title": "启用应用程序调试", 438 | "Memo": "允许应用程序启动对其他应用程序的调试。恶意应用程序可借此终止其他应用程序。", 439 | "Level": 1 440 | }, 441 | { 442 | "Key": "android.permission.SET_ORIENTATION", 443 | "Title": "更改屏幕显示方向", 444 | "Memo": "允许应用程序随时更改屏幕的旋转方向。普通应用程序从不需要使用此权限。", 445 | "Level": 0 446 | }, 447 | { 448 | "Key": "android.permission.SET_PREFERRED_APPLICATIONS", 449 | "Title": "设置首选应用程序", 450 | "Memo": "允许应用程序修改首选的应用程序。这样恶意应用程序可能会暗中更改运行的应用程序,从而骗过您的现有应用程序来收集您的保密数据。", 451 | "Level": 1 452 | }, 453 | { 454 | "Key": "android.permission.SET_PROCESS_FOREGROUND", 455 | "Title": "强制前台运行", 456 | "Memo": "允许程序强制前台运行", 457 | "Level": 0 458 | }, 459 | { 460 | "Key": "android.permission.SET_PROCESS_LIMIT", 461 | "Title": "限制运行的进程个数", 462 | "Memo": "允许应用程序控制将运行的进程数上限。普通应用程序从不需要使用此权限。", 463 | "Level": 0 464 | }, 465 | { 466 | "Key": "android.permission.SET_TIME_ZONE", 467 | "Title": "设置时区", 468 | "Memo": "允许应用程序更改手机的时区。", 469 | "Level": 0 470 | }, 471 | { 472 | "Key": "android.permission.SET_WALLPAPER", 473 | "Title": "设置壁纸", 474 | "Memo": "允许应用程序设置系统壁纸。", 475 | "Level": 0 476 | }, 477 | { 478 | "Key": "android.permission.SET_WALLPAPER_HINTS", 479 | "Title": "设置壁纸大小提示", 480 | "Memo": "允许应用程序设置有关壁纸大小的提示。", 481 | "Level": 0 482 | }, 483 | { 484 | "Key": "android.permission.SIGNAL_PERSISTENT_PROCESSES", 485 | "Title": "向应用程序发送 Linux 信号", 486 | "Memo": "允许应用程序请求将所提供的信号发送给所有持久进程。", 487 | "Level": 0 488 | }, 489 | { 490 | "Key": "android.permission.STATUS_BAR", 491 | "Title": "停用或修改状态栏", 492 | "Memo": "允许应用程序停用状态栏或者增删系统图标。", 493 | "Level": 0 494 | }, 495 | { 496 | "Key": "android.permission.SUBSCRIBED_FEEDS_READ", 497 | "Title": "读取订阅的供稿", 498 | "Memo": "允许应用程序获取有关当前同步的供稿的详细信息。", 499 | "Level": 0 500 | }, 501 | { 502 | "Key": "android.permission.SUBSCRIBED_FEEDS_WRITE", 503 | "Title": "写入订阅的供稿", 504 | "Memo": "允许应用程序修改您当前同步的供稿。恶意应用程序可借此更改您同步的供稿。", 505 | "Level": 1 506 | }, 507 | { 508 | "Key": "android.permission.SYSTEM_ALERT_WINDOW", 509 | "Title": "显示系统级警报", 510 | "Memo": "允许应用程序显示系统警报窗口。恶意应用程序可借此掌控整个手机屏幕。", 511 | "Level": 1 512 | }, 513 | { 514 | "Key": "android.permission.VIBRATE", 515 | "Title": "控制振动器", 516 | "Memo": "允许应用程序控制振动器。", 517 | "Level": 0 518 | }, 519 | { 520 | "Key": "android.permission.WAKE_LOCK", 521 | "Title": "防止手机休眠", 522 | "Memo": "允许应用程序防止手机进入休眠状态。", 523 | "Level": 0 524 | }, 525 | { 526 | "Key": "android.permission.WRITE_APN_SETTINGS", 527 | "Title": "写入(接入点名称)设置", 528 | "Memo": "允许应用程序修改 APN 设置,例如任何 APN 的代理和端口。", 529 | "Level": 0 530 | }, 531 | { 532 | "Key": "android.permission.WRITE_CALENDAR", 533 | "Title": "添加或修改日历活动以及向邀请对象发送电子邮件", 534 | "Memo": "允许应用程序添加或更改日历中的活动,这可能会向邀请对象发送电子邮件。恶意应用程序可能会借此清除或修改您的日历活动,或者向邀请对象发送电子邮件。", 535 | "Level": 1 536 | }, 537 | { 538 | "Key": "android.permission.WRITE_CONTACTS", 539 | "Title": "写入联系数据", 540 | "Memo": "允许应用程序修改您手机上存储的联系人(地址)数据。恶意应用程序可借此清除或修改您的联系人数据。", 541 | "Level": 1 542 | }, 543 | { 544 | "Key": "android.permission.WRITE_GSERVICES", 545 | "Title": "修改 Google 地图", 546 | "Memo": "允许应用程序修改 Google 服务地图。普通应用程序不能使用此权限。", 547 | "Level": 0 548 | }, 549 | { 550 | "Key": "android.permission.WRITE_OWNER_DATA", 551 | "Title": "写入所有者数据", 552 | "Memo": "允许应用程序修改您手机上存储的手机所有者数据。恶意应用程序可借此清除或修改所有者数据。", 553 | "Level": 1 554 | }, 555 | { 556 | "Key": "android.permission.WRITE_SETTINGS", 557 | "Title": "修改全局系统设置", 558 | "Memo": "允许应用程序修改系统设置方面的数据。恶意应用程序可借此破坏您的系统配置。", 559 | "Level": 1 560 | }, 561 | { 562 | "Key": "android.permission.WRITE_SMS", 563 | "Title": "编辑短信或彩信", 564 | "Memo": "允许应用程序写入手机或 SIM 卡中存储的短信。恶意应用程序可借此删除您的信息。", 565 | "Level": 1 566 | }, 567 | { 568 | "Key": "android.permission.WRITE_SYNC_SETTINGS", 569 | "Title": "写入同步设置", 570 | "Memo": "允许应用程序修改同步设置,例如是否为(联系人)启用同步。", 571 | "Level": 1 572 | }, 573 | { 574 | "Key": "android.permission.ACCESS_CACHE_FILESYSTEM", 575 | "Title": "访问缓存文件系统", 576 | "Memo": "允许应用程序读取和写入缓存文件系统。", 577 | "Level": 0 578 | }, 579 | { 580 | "Key": "android.permission.ACCOUNT_MANAGER", 581 | "Title": "作为帐户身份验证程序", 582 | "Memo": "允许应用程序使用 AccountManager 的帐户身份验证程序功能,包括创建帐户以及获取和设置其密码。", 583 | "Level": 0 584 | }, 585 | { 586 | "Key": "android.permission.ASEC_ACCESS", 587 | "Title": "获取有关安全存储的信息", 588 | "Memo": "允许应用程序获取有关安全存储的信息。", 589 | "Level": 0 590 | }, 591 | { 592 | "Key": "android.permission.ASEC_CREATE", 593 | "Title": "创建安全存储", 594 | "Memo": "允许应用程序创建安全存储。", 595 | "Level": 0 596 | }, 597 | { 598 | "Key": "android.permission.ASEC_DESTROY", 599 | "Title": "清除安全存储", 600 | "Memo": "允许应用程序清除安全存储。", 601 | "Level": 0 602 | }, 603 | { 604 | "Key": "android.permission.ASEC_MOUNT_UNMOUNT", 605 | "Title": "安装/卸载安全存储", 606 | "Memo": "允许应用程序安装/卸载安全存储。", 607 | "Level": 0 608 | }, 609 | { 610 | "Key": "android.permission.ASEC_RENAME", 611 | "Title": "重命名安全存储", 612 | "Memo": "允许应用程序重命名安全存储。", 613 | "Level": 0 614 | }, 615 | { 616 | "Key": "android.permission.AUTHENTICATE_ACCOUNTS", 617 | "Title": "作为帐户身份验证程序", 618 | "Memo": "允许应用程序使用 AccountManager 的帐户身份验证程序功能,包括创建帐户以及获取和设置其密码。", 619 | "Level": 0 620 | }, 621 | { 622 | "Key": "android.permission.BACKUP", 623 | "Title": "控制系统备份和还原", 624 | "Memo": "允许应用程序控制系统的备份和还原机制。普通应用程序不能使用此权限。", 625 | "Level": 0 626 | }, 627 | { 628 | "Key": "android.permission.BIND_APPWIDGET", 629 | "Title": "选择窗口小部件", 630 | "Memo": "允许应用程序告诉系统哪个应用程序可以使用哪些窗口小部件。具有该权限的应用程序可以允许其他应用程序访问个人数据。普通应用程序不能使用此权限。", 631 | "Level": 0 632 | }, 633 | { 634 | "Key": "android.permission.BIND_DEVICE_ADMIN", 635 | "Title": "与设备管理器交互", 636 | "Memo": "允许持有对象将意向发送到设备管理器。普通的应用程序一律无需此权限。", 637 | "Level": 0 638 | }, 639 | { 640 | "Key": "android.permission.BIND_INPUT_METHOD", 641 | "Title": "绑定至输入法", 642 | "Memo": "允许手机用户绑定至输入法的顶级界面。普通应用程序从不需要使用此权限。", 643 | "Level": 0 644 | }, 645 | { 646 | "Key": "android.permission.BIND_WALLPAPER", 647 | "Title": "绑定到壁纸", 648 | "Memo": "允许手机用户绑定到壁纸的顶级界面。应该从不需要将此权限授予普通应用程序。", 649 | "Level": 0 650 | }, 651 | { 652 | "Key": "android.permission.BROADCAST_SMS", 653 | "Title": "发送短信收到的广播", 654 | "Memo": "允许应用程序广播已收到短信的通知。恶意应用程序可借此伪造收到的短信。", 655 | "Level": 1 656 | }, 657 | { 658 | "Key": "android.intent.category.MASTER_CLEAR.permission.C2D_MESSAGE", 659 | "Title": "C2DM权限(云端)", 660 | "Memo": "C2DM允许第三方开发者开发相关的应用来推送少量数据消息到用户的手机上。", 661 | "Level": 1 662 | }, 663 | { 664 | "Key": "android.permission.CHANGE_BACKGROUND_DATA_SETTING", 665 | "Title": "更改背景数据使用设置", 666 | "Memo": "允许应用程序更改背景数据使用设置。", 667 | "Level": 0 668 | }, 669 | { 670 | "Key": "android.permission.CHANGE_WIFI_MULTICAST_STATE", 671 | "Title": "允许接收WLAN多播", 672 | "Memo": "允许应用程序接收并非直接向您的设备发送的数据包。这样在查找附近提供的服务时很有用。这种操作所耗电量大于非多播模式。", 673 | "Level": 1 674 | }, 675 | { 676 | "Key": "android.permission.COPY_PROTECTED_DATA", 677 | "Title": "复制保护数据", 678 | "Memo": "允许调用默认的容器服务复制内容。不适用于普通应用程序使用。", 679 | "Level": 1 680 | }, 681 | { 682 | "Key": "android.permission.FORCE_STOP_PACKAGES", 683 | "Title": "强行停止其他应用程序", 684 | "Memo": "允许应用程序强行停止其他应用程序。", 685 | "Level": 0 686 | }, 687 | { 688 | "Key": "android.permission.GLOBAL_SEARCH", 689 | "Title": "全局搜索", 690 | "Memo": "允许应用程序使用全局搜索。", 691 | "Level": 0 692 | }, 693 | { 694 | "Key": "android.permission.GLOBAL_SEARCH_CONTROL", 695 | "Title": "全局搜索控制", 696 | "Memo": "允许应用程序控制全局搜索。", 697 | "Level": 0 698 | }, 699 | { 700 | "Key": "android.permission.INSTALL_LOCATION_PROVIDER", 701 | "Title": "允许安装位置信息提供程序", 702 | "Memo": "创建模拟地点来源进行测试。恶意应用程序可能利用此选项覆盖由真实地点来源(如 GPS 或网络提供商)所传回的地点和/或状态,或者监视您的位置并将其提供给外部来源。", 703 | "Level": 1 704 | }, 705 | { 706 | "Key": "android.permission.KILL_BACKGROUND_PROCESSES", 707 | "Title": "结束后台进程", 708 | "Memo": "无论内存资源是否紧张,都允许应用程序结束其他应用程序的后台进程。", 709 | "Level": 0 710 | }, 711 | { 712 | "Key": "android.permission.LEDS", 713 | "Title": "控制键盘灯", 714 | "Memo": "允许应用程序控制键盘灯。", 715 | "Level": 0 716 | }, 717 | { 718 | "Key": "android.permission.MANAGE_ACCOUNTS", 719 | "Title": "管理帐户列表", 720 | "Memo": "允许应用程序执行添加、删除帐户及删除其密码之类的操作。", 721 | "Level": 0 722 | }, 723 | { 724 | "Key": "android.permission.MEIZU_SYS_PHONE_FUNC", 725 | "Title": "魅族手机系统程序", 726 | "Memo": "魅族手机系统程序。不做解释.", 727 | "Level": 0 728 | }, 729 | { 730 | "Key": "android.permission.MOUNT_FORMAT_FILESYSTEMS", 731 | "Title": "格式化外部存储设备", 732 | "Memo": "允许应用程序格式化可移除的存储设备。", 733 | "Level": 2 734 | }, 735 | { 736 | "Key": "android.permission.MOVE_PACKAGE", 737 | "Title": "移动应用程序资源", 738 | "Memo": "允许应用程序在内部介质和外部介质之间移动应用程序资源。", 739 | "Level": 0 740 | }, 741 | { 742 | "Key": "android.permission.PACKAGE_USAGE_STATS", 743 | "Title": "更新组件使用统计", 744 | "Memo": "允许使用统计资料的收集组件修改。普通应用程序不适合使用。", 745 | "Level": 0 746 | }, 747 | { 748 | "Key": "android.permission.PERFORM_CDMA_PROVISIONING", 749 | "Title": "直接启动CDMA电话设置", 750 | "Memo": "允许应用程序启动 CDMA 服务。恶意应用程序可能会无端启动 CDMA 服务", 751 | "Level": 1 752 | }, 753 | { 754 | "Key": "android.permission.READ_CONTACTS_SUPER", 755 | "Title": "读取联系人", 756 | "Memo": "允许应用程序读取联系人数据(超级权限).", 757 | "Level": 2 758 | }, 759 | { 760 | "Key": "android.permission.READ_HISTORY_BOOKMARKS", 761 | "Title": "读取历史记录", 762 | "Memo": "允许应用程序读取浏览器历史记录.", 763 | "Level": 1 764 | }, 765 | { 766 | "Key": "android.permission.READ_PHONE_STATE", 767 | "Title": "读取手机状态和身份", 768 | "Memo": "允许应用程序访问设备的手机功能。有此权限的应用程序可确定此手机的号码和序列号,是否正在通话,以及对方的号码等。", 769 | "Level": 1 770 | }, 771 | { 772 | "Key": "android.permission.READ_USER_DICTIONARY", 773 | "Title": "读取用户定义的词典", 774 | "Memo": "允许应用程序读取用户在用户词典中存储的任意私有字词、名称和短语。", 775 | "Level": 0 776 | }, 777 | { 778 | "Key": "android.permission.SET_TIME", 779 | "Title": "设置时间", 780 | "Memo": "允许应用程序更改手机的时间。", 781 | "Level": 0 782 | }, 783 | { 784 | "Key": "android.permission.SET_WALLPAPER_COMPONENT", 785 | "Title": "设置壁纸组件", 786 | "Memo": "允许应用程序设置壁纸组件。", 787 | "Level": 0 788 | }, 789 | { 790 | "Key": "android.permission.SHUTDOWN", 791 | "Title": "部分关机", 792 | "Memo": "使活动管理器进入关闭状态。不执行彻底关机。", 793 | "Level": 0 794 | }, 795 | { 796 | "Key": "android.permission.STOP_APP_SWITCHES", 797 | "Title": "禁止切换应用程序", 798 | "Memo": "禁止用户切换到另一应用程序。", 799 | "Level": 0 800 | }, 801 | { 802 | "Key": "android.permission.UPDATE_DEVICE_STATS", 803 | "Title": "更新设备状态", 804 | "Memo": "允许应用程序更新设备状态。", 805 | "Level": 0 806 | }, 807 | { 808 | "Key": "android.permission.USE_CREDENTIALS", 809 | "Title": "使用帐户的身份验证凭据", 810 | "Memo": "允许应用程序请求身份验证标记。", 811 | "Level": 0 812 | }, 813 | { 814 | "Key": "android.permission.WRITE_CONTACTS_SUPER", 815 | "Title": "写入联系人数据", 816 | "Memo": "允许应用程序写入联系人数据(超级权限)。", 817 | "Level": 0 818 | }, 819 | { 820 | "Key": "android.permission.WRITE_EXTERNAL_STORAGE", 821 | "Title": "修改/删除SD卡中的内容", 822 | "Memo": "允许应用程序写入SD卡。", 823 | "Level": 0 824 | }, 825 | { 826 | "Key": "com.android.browser.permission.WRITE_HISTORY_BOOKMARKS", 827 | "Title": "写入浏览器历史和书签记录", 828 | "Memo": "允许应用程序写入浏览器历史和书签记录。", 829 | "Level": 0 830 | }, 831 | { 832 | "Key": "android.permission.WRITE_SECURE_SETTINGS", 833 | "Title": "修改安全系统设置", 834 | "Memo": "允许应用程序修改系统的安全设置数据。普通应用程序不能使用此权限。", 835 | "Level": 1 836 | }, 837 | { 838 | "Key": "android.permission.WRITE_USER_DICTIONARY", 839 | "Title": "写入用户定义的词典", 840 | "Memo": "允许应用程序向用户词典中写入新词。", 841 | "Level": 1 842 | } 843 | ] 844 | } -------------------------------------------------------------------------------- /configs/run.ini: -------------------------------------------------------------------------------- 1 | [run] 2 | # 是否是第一次跑,或者是重新跑,为0时会重新安装指定apk,并执行任务;为1时直接启动安装的app进行任务操作 3 | isFirst = 1 4 | # app的包名 5 | pkgName = com.test.paydayloan 6 | # 启动app的main activity: 手动获取 -- aapt dump badging a.apk 或者 adb shell dumpsys package pkg_name -> android.intent.action.MAIN: 7 | launchActivity = 8 | # 自动化启动app时,需要这个等待来做缓冲,避免启动页面挡住操作: 手动获取 -- adb logcat -c && adb logcat -s ActivityManager 9 | # 该自动可以为空 10 | waitActivity = com.test.paydayloan/.module.main.WelcomeActivity 11 | # 到isFirst为0时,就进行安装操作 12 | apkFilePath = ~/paydayloan_debug_v1.0(20170727100310).apk 13 | 14 | appiumPath = D:\DevTools\Appium\node_modules\appium\lib\server\main.js 15 | 16 | [desired_caps] 17 | # 这些参数都是启动app时需要的,但是在代码读取参数的时候,不一定都读取,因为有些参数不是固定的 18 | automationName=Appium 19 | platformName=Android 20 | # platformVersion=2.3 21 | # deviceName=Android Devices 22 | # udid = 23 | # app=houmi 24 | appPackage=com.test.paydayloan 25 | appActivity=.module.main.WelcomeActivity 26 | # 不用考虑apk的签名问题,有些需要重新签名才能进行操作,比如:robotium 27 | noSign = False 28 | # 是否支持中文 29 | unicodeKeyboard = False 30 | resetKeyboard = False -------------------------------------------------------------------------------- /framework/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/__init__.py -------------------------------------------------------------------------------- /framework/base/AppFileCtrl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding: utf8 -*- 3 | 4 | """ 5 | @version: v1.0 6 | @author: jayzhen 7 | @license: Apache Licence 8 | @contact: jayzhen_testing@163.com 9 | @site: http://blog.csdn.net/u013948858 10 | @software: PyCharm 11 | """ 12 | 13 | import os 14 | import re 15 | 16 | 17 | class ApkController(object): 18 | 19 | """ 20 | 初始化就先确认存放apk文件的路径,通过config目录的中apk path文件来获取配置文件中path。 21 | """ 22 | def __init__(self): 23 | # absp = os.getcwd() 24 | absp = "C:\\" 25 | apkp = os.path.join(absp,"apks") 26 | if not os.path.exists(apkp): 27 | os.mkdir(apkp) 28 | self.result_dir = apkp 29 | 30 | ''' 31 | 获取当前文件夹下的最新apk文件,并返回该文件的绝对路径和文件名。 32 | ''' 33 | def get_latest_apk(self,apklist): 34 | if apklist is None: 35 | return None 36 | st = apklist.sort(key=lambda fn: os.path.getmtime(self.result_dir+"\\"+fn) if not os.path.isdir(self.result_dir+"\\"+fn) else 0) 37 | # d=datetime.datetime.fromtimestamp(os.path.getmtime(result_dir+"\\"+apklist[-1])) 38 | # print d 39 | fname = apklist[-1] 40 | fpath = os.path.join(self.result_dir,fname) 41 | return fpath,fname 42 | 43 | ''' 44 | 获取当前文件夹下的所有apk文件,返回一个list。 45 | ''' 46 | def apk_list(self): 47 | filelist = os.listdir(self.result_dir) 48 | apklist = [] 49 | for fapk in filelist: 50 | if re.search(r'\.apk$',fapk): 51 | apklist.append(fapk) 52 | return apklist 53 | 54 | ''' 55 | 因为该模块会与apk在同一级文件夹下,所以知道文件名后,通过追加路径的方式,返回绝对路径。 56 | ''' 57 | def apk_abs_path(self,apkName): 58 | try: 59 | abspath = os.path.join(self.result_dir,apkName) 60 | if not os.path.exists(abspath): 61 | return None 62 | except TypeError as e: 63 | return None 64 | return abspath 65 | 66 | ''' 67 | 参数apk是apk的绝对路径,使用aapt命令来获取apk的包名,当然需要配置好aapt的环境变量。 68 | ''' 69 | def get_apk_package_name(self,apk): 70 | try: 71 | if apk is not None: 72 | res = os.popen("aapt dump badging %s"%apk).read() 73 | if res is None or len(res)<0: 74 | return None 75 | # reg = "package\: name\=\'(.*?)'" 76 | reg = "package: name='(.*?)'" 77 | regc = re.compile(reg) 78 | res = re.findall(regc,res) 79 | if res is not None and len(res) >0: 80 | pname = str(res[0]) 81 | print(">>> the apk's package name is [%s]"%pname) 82 | return pname 83 | else: 84 | return None 85 | except Exception as e: 86 | print("An error occurred environment variable on aapt") 87 | ''' 88 | 使用python的os中的remove方法来删除指定路径的文件,删除之前先判断是否存在该文件。 89 | ''' 90 | def delete_apk(self, apkpath): 91 | ap = apkpath 92 | if os.path.exists(ap): 93 | os.remove(ap) 94 | if not os.path.exists(ap): 95 | return True 96 | -------------------------------------------------------------------------------- /framework/base/DeviceInfoCtrl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | """ 11 | import re 12 | from framework.core.adb.AdbCommand import AdbCmder 13 | from framework.utils.reporterutils.LoggingUtil import LoggingController 14 | 15 | 16 | class DeviceController: 17 | def __init__(self): 18 | self.android = AdbCmder() 19 | self.log4py = LoggingController() 20 | ''' 21 | 获取连接上电脑的手机设备,返回一个设备名的list 22 | ''' 23 | def get_devices(self): 24 | sno_list = self.android.get_device_list() 25 | return sno_list 26 | 27 | ''' 28 | 根据不同的需求,设计了返回dict和list格式的两个function。 29 | ''' 30 | def get_infos_as_dict(self): 31 | try: 32 | info = {} 33 | lists = self.get_devices() 34 | if not lists or len(lists) <= 0: 35 | self.log4py.info("NO Device connected") 36 | return None 37 | for sno in lists: 38 | sno,phone_brand,phone_model,os_version,ram,dpi,image_resolution,ip = self.get_info(sno) 39 | info[sno] = {"phone_brand":phone_brand,"phone_model":phone_model,"ram":ram,"os_version":os_version,"dpi":dpi,"image_resolution":image_resolution,"ip":ip} 40 | return info 41 | except TypeError as e: 42 | self.log4py.error(e) 43 | return None 44 | 45 | def get_infos_as_list(self): 46 | info_list = self.get_devices_as_dict() 47 | devices_as_lsit = ["All"] 48 | for i in info_list: 49 | a = info_list[i]["phone_brand"] 50 | b = info_list[i]["phone_model"] 51 | c = info_list[i]["os_version"] 52 | d = info_list[i]["dpi"] 53 | e = info_list[i]["image_resolution"] 54 | f = info_list[i]["ip"] 55 | t = a+" :: "+b+" :: "+c+" :: "+d+" :: "+e+" :: "+f 56 | devices_as_lsit.append(t) 57 | return devices_as_lsit 58 | 59 | ''' 60 | 通过adb命令来获取连接上电脑的设备的信息。 61 | ''' 62 | def get_info(self,sno): 63 | phone_brand = None 64 | phone_model = None 65 | os_version = None 66 | ram = None 67 | dpi = None 68 | image_resolution = None 69 | ip = None 70 | try: 71 | result = self.android.shell("cat /system/build.prop").stdout.readlines() 72 | for res in result: 73 | #系统版本 74 | if re.search(r"ro\.build\.version\.release",res): 75 | os_version = res.split('=')[-1].strip() 76 | #手机型号 77 | elif re.search(r"ro\.product\.model",res): 78 | phone_model = res.split('=')[-1].strip() 79 | #手机品牌 80 | elif re.search(r"ro\.product\.brand",res): 81 | phone_brand = res.split('=')[-1].strip() 82 | ip = self.android.shell("getprop dhcp.wlan0.ipaddress").stdout.read() 83 | dpi = self.android.shell("getprop ro.sf.lcd_density").stdout.read() 84 | proc_meninfo = self.android.shell("cat /proc/meminfo").stdout.readline() 85 | ram = (int(proc_meninfo.split(" ")[-2])//1000000) 86 | if int(proc_meninfo.split(" ")[-2])%1000000 >= 500000: 87 | ram += 1 88 | res_4_2 = self.android.shell("dumpsys window").stdout.read() 89 | res_4_4 = self.android.shell("wm size").stdout.read() 90 | r_4_2 = "init=(\d*x\d*)" 91 | r_4_4 = "Physical size: (\d*x\d*)" 92 | reg_4_4 = re.compile(r_4_4) 93 | reg_4_2 = re.compile(r_4_2) 94 | image_list_4_4 = re.findall(reg_4_4,res_4_4) 95 | image_list_4_2 = re.findall(reg_4_2,res_4_2) 96 | if len(image_list_4_4) > 0: 97 | image_resolution = image_list_4_4[0] 98 | elif len(image_list_4_2) > 0: 99 | image_resolution = image_list_4_2[0] 100 | else: 101 | image_resolution = "NULL" 102 | return sno,phone_brand,phone_model,os_version,str(ram)+"GB",dpi.strip(),image_resolution,ip.strip() 103 | except Exception as e: 104 | self.log4py.error("Get device info happend ERROR :"+ str(e)) 105 | return None 106 | -------------------------------------------------------------------------------- /framework/base/DriverBaseCase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | """ 11 | 12 | from framework import LogObj 13 | from framework import DateTimeManager 14 | from framework import InitBrowser 15 | from framework import FileChecK 16 | from framework import Config 17 | import time 18 | import os 19 | 20 | 21 | class WebDriverDoBeforeTest(): 22 | 23 | def __init__(self): 24 | self.driver = None 25 | self.className = None 26 | self.beforeSuiteStarts = 0 27 | self.afterSuiteStops = 0 28 | self.beforeClassStarts = 0 29 | self.afterClassStops = 0 30 | self.beforeTestStarts = 0 31 | self.afterTestStops = 0 32 | fc = FileChecK() 33 | boolean = fc.is_has_file("framework.ini") 34 | if boolean: 35 | self.projectpath = fc.getProjectPath() 36 | self.fwInipath = fc.get_fileabspath() 37 | self.logger = LogObj() 38 | self.capturePath = os.path.join(self.projectpath,Config(self.fwInipath).get("capturePath", "capturePath")) 39 | 40 | def getDriverTooler(self,initbrowsername,baseURL): 41 | initbrowser = InitBrowser() 42 | initbrowser.beforeTestInitBrowser(initbrowsername,baseURL) 43 | self.driver = initbrowser.getWebDriver() 44 | return self.driver 45 | 46 | def beforeSuite(self): 47 | begins = DateTimeManager().formatedTime("%Y-%m-%d %H:%M:%S:%f") 48 | self.beforeSuiteStarts = time.time() 49 | self.logger.info("======" + begins + ":测试集开始======") 50 | 51 | def afterSuite(self): 52 | ends = DateTimeManager().formatedTime("%Y-%m-%d %H:%M:%S:%f") 53 | self.afterSuiteStops = time.time() 54 | self.logger.info("======" + ends + ":测试集结束======") 55 | self.logger.info("======本次测试集运行消耗时间 "+str(self.afterSuiteStops - self.beforeSuiteStarts) + " 秒!======"); 56 | 57 | def beforeClass(self): 58 | begins = DateTimeManager().formatedTime("%Y-%m-%d %H:%M:%S:%f") 59 | self.beforeClassStarts = time.time() 60 | self.logger.info("======" + str(begins) + ":测试【" + str(self.className) + "】开始======"); 61 | 62 | def afterClass(self): 63 | ends = DateTimeManager().formatedTime("%Y-%m-%d %H:%M:%S:%f") 64 | self.afterClassStops = time.time() 65 | self.logger.info("======" + str(ends) + ":测试【" + str(self.className) + "】结束======"); 66 | self.logger.info("======本次测试运行消耗时间 " + str(self.afterClassStops-self.beforeClassStarts)+ " 秒!======") 67 | 68 | def beforeTest(self, methodName) : 69 | begins = DateTimeManager().formatedTime("%Y-%m-%d %H:%M:%S:%f"); 70 | self.beforeTestStarts = time.time() 71 | self.logger.info("======" + begins + ":案例【" + str(self.className) + "." + methodName+ "】开始======") 72 | 73 | def afterTest(self,methodName, isSucceed): 74 | ends = DateTimeManager().formatedTime("%Y-%m-%d %H:%M:%S:%f") 75 | captureName = "" 76 | if (isSucceed): 77 | self.logger.info("案例 【" + str(self.className) + "." + methodName + "】 运行通过!") 78 | else: 79 | dateTime = DateTimeManager().formatedTime("-%Y%m%d-%H%M%S%f") 80 | captureName = self.capturePath+ str(self.className)+"."+methodName+str(dateTime)+".png" 81 | self.captureScreenshot(captureName) 82 | self.logger.error("案例 【" + str(self.className) + "." + methodName+ "】 运行失败,请查看截图快照:" + captureName) 83 | self.logger.info("======" + ends + ":案例【" + str(self.className) + "." + methodName+ "】结束======") 84 | afterTestStops = time.time() 85 | self.logger.info("======本次案例运行消耗时间 " + str(afterTestStops - self.beforeTestStarts) + " 秒!======"); 86 | return captureName; 87 | 88 | ''' 89 | * 截取屏幕截图并保存到指定路径 90 | * @param name:保存屏幕截图名称 91 | * @return 无 92 | ''' 93 | def capture(self,name): 94 | time.sleep(3) 95 | dateTime = DateTimeManager().formatedTime("-%Y%m%d-%H%M%S-%f") 96 | captureName = self.capturePath+name+dateTime+".png" 97 | self.captureScreenshot(captureName) 98 | self.logger.debug("请查看截图快照:" + captureName) 99 | 100 | ''' 101 | * 截取屏幕截图并保存到指定路径 102 | * @param filepath:保存屏幕截图完整文件名称及路径 103 | * @return 无 104 | ''' 105 | def captureScreenshot(self, filepath): 106 | try: 107 | self.driver.get_screenshot_as_file(filepath) 108 | except Exception as e: 109 | self.logger.error("保存屏幕截图失败,失败信息:"+str(e)) 110 | 111 | ''' 112 | * public method for handle assertions and screenshot. 113 | * @param isSucceed:if your operation success 114 | * @throws RuntimeException 115 | ''' 116 | def operationCheck(self, methodName, isSucceed): 117 | if (isSucceed): 118 | self.logger.info("method 【" + methodName + "】 运行通过!"); 119 | else: 120 | self.logger.error("method 【" + methodName + "】 运行失败!"); 121 | -------------------------------------------------------------------------------- /framework/base/GetAllPathCtrl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘dell‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @file: GetAllPathCtrl.py 10 | @time: 2017/3/29 13:12 11 | """ 12 | from framework.utils.fileutils.ConfigCommonUtil import ConfigController 13 | from framework.utils.fileutils.FileCheckAndGetPath import FileChecKController 14 | from framework.utils.reporterutils.LoggingUtil import LoggingController 15 | import os 16 | 17 | PATH = lambda a: os.path.abspath(a) 18 | 19 | 20 | class GetAllPathController(object): 21 | def __init__(self): 22 | self.fkctl = FileChecKController() 23 | if self.fkctl.is_has_file("allpath.ini"): 24 | fp = self.fkctl.get_fileabspath() 25 | self.cfgctl = ConfigController(fp) 26 | self.log4py = LoggingController() 27 | self.pro_path = self.fkctl.get_project_path() 28 | 29 | def get_dumpxml_path(self): 30 | self.log4py.info("executive -get_dumpxml_path- function ") 31 | path = os.path.join(self.pro_path, self.cfgctl.get("dumpxmlPath", "dumpxmlPath")) 32 | if PATH(path): 33 | self.log4py.info("获取 %s"%path) 34 | return path 35 | return None 36 | 37 | def get_htmlreport_path(self): 38 | self.log4py.info("executive -get_htmlreport_path- function ") 39 | path = os.path.join(self.pro_path, self.cfgctl.get("htmlreportPath", "htmlreportPath")) 40 | if PATH(path): 41 | self.log4py.info("获取 %s" % path) 42 | return path 43 | return None 44 | 45 | def get_logs_path(self): 46 | self.log4py.info("executive -get_logs_path- function ") 47 | path = os.path.join(self.pro_path, self.cfgctl.get("logsPath", "logsPath")) 48 | if PATH(path): 49 | if not os.path.exists(path): 50 | os.makedirs(path) 51 | self.log4py.info("获取 %s" % path) 52 | return path 53 | return None 54 | 55 | def get_capture_path(self): 56 | self.log4py.info("executive get_logs_path function ") 57 | path = os.path.join(self.pro_path, self.cfgctl.get("capturePath", "capturePath")) 58 | if PATH(path): 59 | self.log4py.info("获取 %s" % path) 60 | return path 61 | return None 62 | 63 | def get_appium_logs_path(self): 64 | self.log4py.info("executive get_logs_path function ") 65 | path = os.path.join(self.pro_path, self.cfgctl.get("appiumlogPath", "appiumlogPath")) 66 | if PATH(path): 67 | if not os.path.exists(path): 68 | os.makedirs(path) 69 | self.log4py.info("获取到appium服务的日志路径 %s" % path) 70 | return path.replace("\\", "/") 71 | return None -------------------------------------------------------------------------------- /framework/base/PackageCtrl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | """ 11 | import os 12 | import re 13 | from DeviceInfoCtrl import DeviceController 14 | from framework.core.adb.AdbCommand import AdbCmder 15 | ''' 16 | 主要处理安装和卸载手机上的应用 17 | ''' 18 | class PackageController(): 19 | def __init__(self): 20 | self.sno_list = DeviceController().get_devices() 21 | self.android = AdbCmder() 22 | ''' 23 | uninstall_All参数指定要卸载的包名,该方法会调用uninstall_One卸载所有链接在电脑上的手机中的应用 24 | ''' 25 | def uninstall_all(self,package_name): 26 | devices = self.sno_list 27 | if devices is None: 28 | print(">>>No device is connected") 29 | else: 30 | for sno in devices: 31 | self.uninstall_one(sno,package_name) 32 | ''' 33 | 指定设备,并指定包名进行应用的卸载 34 | ''' 35 | def uninstall_one(self,sno,package_name): 36 | uninstall_result = self.android.adb(sno,'uninstall %s'%package_name).stdout.read() 37 | if re.findall(r'Success',uninstall_result): 38 | print('>>>[%s] uninstall [%s] [SUCCESS]' %(sno,package_name)) 39 | else: 40 | print('>>>no assign package') 41 | ''' 42 | apk_name为apk的绝对路径,该方法会调用install_OneDevice方法,向所有设备安装该应用 43 | ''' 44 | def install_all_devices(self,apk_name,apk_package_name): 45 | print(">>>Install all devices") 46 | device_list = self.sno_list 47 | if device_list is None: 48 | print(">>>No device is connected") 49 | else: 50 | for sno in device_list: 51 | self.install_one_device(sno,apk_name,apk_package_name) 52 | 53 | ''' 54 | 指定设备名,并指定apk进行安装,安装前会检测手机是否已经安装了该应用,如果有,先卸载 55 | ''' 56 | def install_one_device(self,sno,apk_name,apk_package_name): 57 | had_package = self.android.shell(sno,'pm list packages |findstr "%s"'%apk_package_name).stdout.read() 58 | if re.search(apk_package_name,had_package): 59 | self.uninstall_one(sno,apk_package_name) 60 | install_result = self.android.adb(sno,'install %s'%apk_name).stdout.read() 61 | boolean = self.is_has_package(sno,apk_package_name) 62 | if re.findall(r'Success',install_result) or boolean: 63 | print('>>>[%s] adb install %s [SUCCESS]' %(sno,os.path.basename(apk_name))) 64 | else: 65 | print('>>>[%s] install %s [FALSE]'%(sno,os.path.basename(apk_name))) 66 | 67 | def cover_install(self,sno,apk_name,apk_package_name): 68 | install_result = self.android.adb(sno,'install -r %s'%apk_name).stdout.read() 69 | boolean = self.is_has_package(sno,apk_package_name) 70 | if re.findall(r'Success',install_result) or boolean: 71 | print('>>>[%s] adb install %s [SUCCESS]' %(sno,os.path.basename(apk_name))) 72 | else: 73 | print('>>>[%s] install %s [FALSE]'%(sno,os.path.basename(apk_name))) 74 | 75 | def is_has_package(self,sno,package_name): 76 | had_package = self.android.shell(sno,'pm list packages |findstr "%s"'%package_name).stdout.read() 77 | if re.search(package_name,had_package): 78 | return True 79 | else: 80 | return False 81 | 82 | def clear_app_data(self,sno,package_name): 83 | b = self.is_has_package(sno, package_name) 84 | if b: 85 | res = self.android.shell(sno,"pm clear %s"%package_name).stdout.read() 86 | if re.search(r'Success',res): 87 | print(">>> Clear data Success with [%s]"%package_name) 88 | else: 89 | print(">>> Clear work ERROR") 90 | else: 91 | print(">>> NO Package :",package_name) 92 | 93 | 94 | -------------------------------------------------------------------------------- /framework/base/PerformanceCtrl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | """ 11 | # 需要安装pychartdir模块 12 | import string 13 | import os 14 | from framework.core.adb.AdbCommand import AdbCmder 15 | from pychartdir import * 16 | 17 | 18 | class AppPerformanceMonitor: 19 | 20 | def __init__(self,sno,times,pkg_name): 21 | # 打开待测应用,运行脚本,默认times为30次(可自己手动修改次数),获取该应用cpu、memory占用率的曲线图,图表保存至chart目录下 22 | self.utils = AdbCmder() 23 | self.sno = sno 24 | if times is None or times == "": 25 | self.times = 30 #top次数 26 | else: 27 | self.times = string.atoi(times) 28 | if 15 > self.times > 0: 29 | self.times = 20 30 | if pkg_name is None or pkg_name == "": 31 | self.pak_name = self.utils.get_current_package_name(sno) 32 | else: 33 | self.pkg_name = pkg_name # 设备当前运行应用的包名 34 | 35 | # 获取cpu、mem占用 36 | def top(self): 37 | cpu = [] 38 | mem = [] 39 | top_info = self.utils.shell(self.sno, "top -n %s | findstr %s$" %(str(self.times), self.pkg_name)).stdout.readlines() 40 | # PID PR CPU% S #THR VSS RSS PCY UID Name 41 | for info in top_info: 42 | # temp_list = del_space(info) 43 | temp_list = info.split() 44 | cpu.append(temp_list[2]) 45 | mem.append(temp_list[6]) 46 | return cpu, mem 47 | 48 | # 绘制线性图表,具体接口的用法查看ChartDirecto的帮助文档 49 | def line_chart(self, data): 50 | PATH = lambda p: os.path.abspath(p) 51 | cpu_data = [] 52 | mem_data = [] 53 | # 去掉cpu占用率中的百分号,并转换为int型 54 | for cpu in data[0]: 55 | cpu_data.append(string.atoi(cpu.split("%")[0])) 56 | # 去掉内存占用中的单位K,并转换为int型,以M为单位 57 | for mem in data[1]: 58 | mem_data.append(string.atof(mem.split("K")[0])/1024) 59 | 60 | # 横坐标 61 | labels = [] 62 | for i in range(1, self.times + 1): 63 | labels.append(str(i)) 64 | 65 | # 自动设置图表区域宽度 66 | if self.times <= 50: 67 | xArea = self.times * 40 68 | elif 50 < self.times <= 90: 69 | xArea = self.times * 20 70 | else: 71 | xArea = 1800 72 | 73 | c = XYChart(xArea, 800, 0xCCEEFF, 0x000000, 1) 74 | c.setPlotArea(60, 100, xArea - 100, 650) 75 | c.addLegend(50, 30, 0, "arialbd.ttf", 15).setBackground(Transparent) 76 | 77 | c.addTitle("cpu and memery info(%s)" %self.pkg_name, "timesbi.ttf", 15).setBackground(0xCCEEFF, 0x000000, glassEffect()) 78 | c.yAxis().setTitle("The numerical", "arialbd.ttf", 12) 79 | c.xAxis().setTitle("Times", "arialbd.ttf", 12) 80 | 81 | c.xAxis().setLabels(labels) 82 | 83 | # 自动设置X轴步长 84 | if self.times <= 50: 85 | step = 1 86 | else: 87 | step = self.times / 50 + 1 88 | 89 | c.xAxis().setLabelStep(step) 90 | 91 | layer = c.addLineLayer() 92 | layer.setLineWidth(2) 93 | layer.addDataSet(cpu_data, 0xff0000, "cpu(%)") 94 | layer.addDataSet(mem_data, 0x008800, "mem(M)") 95 | 96 | path = PATH("%s/chart" %os.getcwd()) 97 | if not os.path.isdir(path): 98 | os.makedirs(path) 99 | 100 | # 图片保存至脚本当前目录的chart目录下 101 | c.makeChart(PATH("%s/%s.png" %(path, self.utils.timestamp()))) 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /framework/base/PycFileCtrl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | """ 11 | 12 | import os 13 | import time 14 | import re 15 | 16 | 17 | # 返回函数式方法 18 | def file_end_with(*endstring): 19 | ends = endstring 20 | 21 | def run(s): 22 | f = map(s.endswith, ends) 23 | if True in f: 24 | return s 25 | return run 26 | 27 | 28 | def san_path(abs_path, end_string): 29 | backfunc= file_end_with(end_string) 30 | for filepath, dirs, filelist in os.walk(abs_path): 31 | if not re.search("\.git", filepath): 32 | f_file = filter(backfunc, filelist) 33 | for i in f_file: 34 | print(os.path.join(filepath, i)) 35 | 36 | 37 | if __name__ == '__main__': 38 | # path = os.getcwd().split("AppiumTestProject")[0] 39 | # san_path(path, '.py') 40 | f = file_end_with("json") 41 | print(f("str.json")) 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /framework/base/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | @version: python3.7 6 | @author: ‘jayzhen‘ 7 | @license: Apache Licence 8 | @contact: 2431236868@qq.com 9 | @site: http://www.jayzhen.com 10 | @software: PyCharm 11 | @file: __init__.py.py 12 | @time: 2017/3/21 23:13 13 | """ -------------------------------------------------------------------------------- /framework/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/core/__init__.py -------------------------------------------------------------------------------- /framework/core/adb/AdbCommand.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | """ 11 | import os 12 | import platform 13 | import re 14 | import subprocess 15 | import sys 16 | import time 17 | import string 18 | import EventKeys 19 | import json 20 | 21 | class AdbCmder(object): 22 | """ 23 | 利用可变参数来初始化*(tuple),**(dict):约定参数中的key只能是sno 24 | a(1,2,3,4,4,Z=8,k=2) : *接受k=v之前的内容,**接受k=v 25 | """ 26 | 27 | def __init__(self, **serialno_num): 28 | self.system = None 29 | self.find_type = None 30 | self.command = "adb" 31 | self.__serialno_num = "" 32 | if "sno" in serialno_num: 33 | self.__serialno_num = serialno_num.get("sno") 34 | 35 | def get_serialno_num(self): 36 | return self.__serialno_num 37 | 38 | def set_serialno_num(self, sno): 39 | self.__serialno_num = sno 40 | 41 | def judgment_system_type(self): 42 | # 判断系统类型,windows使用findstr,linux使用grep 43 | self.system = platform.system() 44 | if self.system is "Windows": 45 | self.find_type = "findstr" 46 | else: 47 | self.find_type = "grep" 48 | 49 | def judgment_system_environment_variables(self): 50 | self.judgment_system_type() 51 | # 判断是否设置环境变量ANDROID_HOME 52 | if "ANDROID_HOME" in os.environ: 53 | if self.system == "Windows": 54 | self.command = "adb" # os.path.join(os.environ["ANDROID_HOME"], "platform-tools", "adb.exe") 55 | else: 56 | self.command = os.path.join(os.environ["ANDROID_HOME"], "platform-tools", "adb") 57 | else: 58 | raise EnvironmentError("Adb not found in $ANDROID_HOME path: %s." % os.environ["ANDROID_HOME"]) 59 | 60 | # adb命令 61 | 62 | def adb(self, args): 63 | if self.__serialno_num == "" or self.__serialno_num is None: 64 | cmd = "%s %s" % (self.command, str(args)) 65 | else: 66 | cmd = "%s -s %s %s" % (self.command, self.__serialno_num, str(args)) 67 | return subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 68 | 69 | def shell(self, args): 70 | if self.__serialno_num == "" or self.__serialno_num is None: 71 | cmd = "%s shell %s" % (self.command, str(args)) 72 | else: 73 | cmd = "%s -s %s shell %s" % (self.command, self.__serialno_num, str(args)) 74 | return subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 75 | 76 | def get_device_state(self): 77 | """ 78 | 获取设备状态: offline | bootloader | device 等 79 | """ 80 | return self.adb("get-state").stdout.read().strip() 81 | 82 | def get_device_sno(self): 83 | """ 84 | 只有一个设备,获取设备id号,return serialNo 85 | """ 86 | return self.adb("get-serialno").stdout.read().strip() 87 | 88 | def get_device_list(self): 89 | devices = [] 90 | result = subprocess.Popen("adb devices", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.readlines() 91 | result.reverse() # 将readlines结果反向排序 92 | for line in result[1:]: 93 | if "attached" not in line.strip() and "daemon" not in line.strip(): 94 | devices.append(line.split()[0]) 95 | else: 96 | break 97 | return devices 98 | 99 | def get_android_os_version(self): 100 | """ 101 | 获取设备中的Android版本号,如4.2.2 102 | """ 103 | return self.shell("getprop ro.build.version.release").stdout.read().strip() 104 | 105 | def get_sdk_version(self): 106 | """ 107 | 获取设备SDK版本号 108 | """ 109 | return self.shell("getprop ro.build.version.sdk").stdout.read().strip() 110 | 111 | def get_device_model(self): 112 | """ 113 | 获取设备型号 114 | """ 115 | return self.shell("getprop ro.product.model").stdout.read().strip() 116 | 117 | def get_app_pid(self, packageName): 118 | """ 119 | 获取进程pid 120 | args: 121 | - packageName -: 应用包名 122 | usage: getPid("com.android.settings") 123 | """ 124 | if self.system is "Windows": 125 | pidinfo = self.shell("ps | findstr %s$" % packageName).stdout.read() 126 | else: 127 | pidinfo = self.shell("ps | grep -w %s" % packageName).stdout.read() 128 | 129 | if pidinfo == '': 130 | return "the process doesn't exist." 131 | 132 | pattern = re.compile(r"\d+") 133 | result = pidinfo.split(" ") 134 | result.remove(result[0]) 135 | 136 | return pattern.findall(" ".join(result))[0] 137 | 138 | def get_focused_package_and_activity_2(self): 139 | pattern = re.compile(r"[a-zA-Z0-9\.]+/.[a-zA-Z0-9\.]+") 140 | out = self.shell("dumpsys window w | %s \/ | %s name=" % (self.find_util, self.find_util)).stdout.read() 141 | return pattern.findall(out)[0] 142 | 143 | def get_focused_package_and_activity(self): 144 | """ 145 | 获取当前应用界面的包名和Activity,返回的字符串格式为:packageName/activityName 146 | """ 147 | return self.shell("dumpsys activity | findstr mFocusedActivity").stdout.read().split()[-2] 148 | 149 | def get_current_package_name(self): 150 | """ 151 | 获取当前运行应用的activity 152 | """ 153 | return self.get_focused_package_and_activity().split("/")[0] 154 | 155 | def get_current_activity(self): 156 | """ 157 | 获取当前设备的activity 158 | """ 159 | return self.get_focused_package_and_activity().split("/")[-1] 160 | 161 | def get_battery_level(self): 162 | """ 163 | 获取电池电量 164 | """ 165 | level = self.shell("dumpsys battery | %s level" %self.find_type).stdout.read().split(": ")[-1] 166 | return int(level) 167 | 168 | def get_battery_status(self): 169 | """ 170 | 获取电池充电状态 171 | BATTERY_STATUS_UNKNOWN:未知状态 172 | BATTERY_STATUS_CHARGING: 充电状态 173 | BATTERY_STATUS_DISCHARGING: 放电状态 174 | BATTERY_STATUS_NOT_CHARGING:未充电 175 | BATTERY_STATUS_FULL: 充电已满 176 | """ 177 | statusDict = {1 : "BATTERY_STATUS_UNKNOWN", 178 | 2 : "BATTERY_STATUS_CHARGING", 179 | 3 : "BATTERY_STATUS_DISCHARGING", 180 | 4 : "BATTERY_STATUS_NOT_CHARGING", 181 | 5 : "BATTERY_STATUS_FULL"} 182 | status = self.shell("dumpsys battery | %s status" %self.find_type).stdout.read().split(": ")[-1] 183 | 184 | return statusDict[int(status)] 185 | 186 | def get_battery_temp(self): 187 | """ 188 | 获取电池温度 189 | """ 190 | temp = self.shell("dumpsys battery | %s temperature" % self.find_type).stdout.read().split(": ")[-1] 191 | return int(temp) / 10.0 192 | 193 | def get_screen_resolution(self): 194 | """ 195 | 获取设备屏幕分辨率,return (width, high) 196 | """ 197 | pattern = re.compile(r"\d+") 198 | out = self.shell("dumpsys display | %s PhysicalDisplayInfo" % self.find_type).stdout.read() 199 | display = pattern.findall(out) 200 | 201 | return (int(display[0]), int(display[1])) 202 | 203 | def get_system_app_list(self): 204 | """ 205 | 获取设备中安装的系统应用包名列表 206 | """ 207 | sysApp = [] 208 | for packages in self.shell("pm list packages -s").stdout.readlines(): 209 | sysApp.append(packages.split(":")[-1].splitlines()[0]) 210 | 211 | return sysApp 212 | 213 | def get_third_app_list(self): 214 | """ 215 | 获取设备中安装的第三方应用包名列表 216 | """ 217 | thirdApp = [] 218 | for packages in self.shell("pm list packages -3").stdout.readlines(): 219 | thirdApp.append(packages.split(":")[-1].splitlines()[0]) 220 | 221 | return thirdApp 222 | 223 | def get_matching_app_list(self, keyword): 224 | """ 225 | 模糊查询与keyword匹配的应用包名列表 226 | usage: getMatchingAppList("qq") 227 | """ 228 | matApp = [] 229 | for packages in self.shell("pm list packages %s" % keyword).stdout.readlines(): 230 | matApp.append(packages.split(":")[-1].splitlines()[0]) 231 | return matApp 232 | 233 | def get_app_start_total_time(self, component): 234 | """ 235 | 获取启动应用所花时间 236 | usage: getAppStartTotalTime("com.android.settings/.Settings") 237 | """ 238 | time = self.shell("am start -W %s | %s TotalTime" % (component, self.find_type)) \ 239 | .stdout.read().split(": ")[-1] 240 | return int(time) 241 | 242 | def do_install_app(self, appFile, pkg_name): 243 | """ 244 | 安装app,app名字不能含中文字符 245 | args:- appFile -: app路径 246 | usage: install("d:\\apps\\Weico.apk") 247 | """ 248 | self.adb("install %s" % appFile) 249 | if not self.is_install_app(pkg_name): 250 | return True 251 | else: 252 | return False 253 | 254 | def do_uninstall_app(self, pkg_name): 255 | """ 256 | 卸载应用args:- packageName -:应用包名,非apk名 257 | """ 258 | self.adb(" uninstall %s" % pkg_name) 259 | if not self.is_install_app(pkg_name): 260 | return True 261 | else: 262 | return False 263 | 264 | def is_install_app(self, packageName): 265 | """ 266 | 判断应用是否安装,已安装返回True,否则返回False 267 | usage: isInstall("com.example.apidemo") 268 | """ 269 | flag = False 270 | result = self.get_third_app_list() 271 | if result is None or len(result) < 0: 272 | return None 273 | for i in result: 274 | if re.search(packageName, i.strip()): 275 | flag = True 276 | return flag 277 | 278 | def do_clear_app_data(self, packageName): 279 | """ 280 | 清除应用用户数据 281 | usage: clearAppData("com.android.contacts") 282 | """ 283 | if "Success" in self.shell("pm clear %s" % packageName).stdout.read().splitlines(): 284 | return "clear user data success " 285 | else: 286 | return "make sure package exist" 287 | 288 | def do_reset_current_app(self): 289 | """ 290 | 重置当前应用 291 | """ 292 | packageName = self.get_current_package_name() 293 | component = self.get_current_activity() 294 | self.do_clear_app_data(packageName) 295 | self.do_start_activity(component) 296 | 297 | def do_start_activity(self, component): 298 | """ 299 | 启动一个Activity 300 | usage: startActivity(component = "com.android.settinrs/.Settings") 301 | """ 302 | self.shell("am start -n %s" % component) 303 | 304 | def do_start_webpage(self, url): 305 | """ 306 | 使用系统默认浏览器打开一个网页 307 | usage: startWebpage("http://www.baidu.com") 308 | """ 309 | self.shell("am start -a android.intent.action.VIEW -d %s" % url) 310 | 311 | def do_call_phone(self, number): 312 | """ 313 | 启动拨号器拨打电话 314 | usage: callPhone(10086) 315 | """ 316 | self.shell("am start -a android.intent.action.CALL -d tel:%s" % str(number)) 317 | 318 | def do_send_key_event(self, event_keys): 319 | """ 320 | 发送一个按键事件 321 | args: 322 | - event_keys -: 323 | http://developer.android.com/reference/android/view/KeyEvent.html 324 | usage: sendKeyEvent(event_keys.HOME) 325 | """ 326 | self.shell("input keyevent %s" % str(event_keys)) 327 | time.sleep(0.5) 328 | 329 | def do_long_press_key(self, event_keys): 330 | """ 331 | 发送一个按键长按事件,Android 4.4以上 332 | usage: longPressKey(event_keys.HOME) 333 | """ 334 | self.shell("input keyevent --longpress %s" % str(event_keys)) 335 | time.sleep(0.5) 336 | 337 | def do_touch(self, e=None, x=None, y=None): 338 | """ 339 | 触摸事件 340 | usage: touch(e), touch(x=0.5,y=0.5) 341 | """ 342 | if(e != None): 343 | x = e[0] 344 | y = e[1] 345 | if(0 < x < 1): 346 | x = x * self.width 347 | if(0 < y < 1): 348 | y = y * self.high 349 | 350 | self.shell("input tap %s %s" % (str(x), str(y))) 351 | time.sleep(0.5) 352 | 353 | def do_touch_by_element(self, element): 354 | """ 355 | 点击元素 356 | usage: touchByElement(Element().findElementByName(u"计算器")) 357 | """ 358 | self.shell("input tap %s %s" % (str(element[0]), str(element[1]))) 359 | time.sleep(0.5) 360 | 361 | def do_touch_by_ratio(self, ratioWidth, ratioHigh): 362 | """ 363 | 通过比例发送触摸事件 364 | args: 365 | - ratioWidth -:width占比, 0tasklist /FI "PID eq 10200" 515 | # Image Name PID Session Name Session# Mem Usage 516 | # ========================= ======== ================ =========== ============ 517 | # adb.exe 10200 Console 1 6,152 K 518 | 519 | process_name = os.popen('tasklist /FI "PID eq %s"' %pid).read().split()[-6] 520 | process_path = os.popen('wmic process where name="%s" get executablepath' %process_name).read().split("\r\n")[1] 521 | 522 | # #分割路径,得到进程所在文件夹名 523 | # name_list = process_path.split("\\") 524 | # del name_list[-1] 525 | # directory = "\\".join(name_list) 526 | # #打开进程所在文件夹 527 | # os.system("explorer.exe %s" %directory) 528 | # 杀死该进程 529 | os.system("taskkill /F /PID %s" %pid) 530 | os.system("adb start-server") 531 | 532 | def do_input_text(self,text): 533 | text_list = list(text) 534 | specific_symbol = set(['&','@','#','$','^','*']) 535 | for i in range(len(text_list)): 536 | if text_list[i] in specific_symbol: 537 | if i-1 < 0: 538 | text_list.append(text_list[i]) 539 | text_list[0] = "\\" 540 | else: 541 | text_list[i-1] = text_list[i-1] + "\\" 542 | seed = ''.join(text_list) 543 | self.shell('input text "%s"'%seed) 544 | 545 | def do_capture_window(self): 546 | self.shell("rm /sdcard/screenshot.png").wait() 547 | self.shell("/system/bin/screencap -p /sdcard/screenshot.png").wait() 548 | print(">>>截取屏幕成功,在桌面查看文件。") 549 | c_time = time.strftime("%Y_%m_%d_%H-%M-%S") 550 | self.adb('pull /sdcard/screenshot.png T:\\%s.png"'%c_time).wait() 551 | 552 | def get_srceenrecord(self,times, path): 553 | PATH = lambda p: os.path.abspath(p) 554 | sdk = string.atoi(self.shell("getprop ro.build.version.sdk").stdout.read()) 555 | try: 556 | times = string.atoi(times) 557 | except ValueError as e: 558 | print(">>>Value error because you enter value is not int type, use default 'times=20s'") 559 | times = int(20) 560 | if sdk >= 19: 561 | self.shell("screenrecord --time-limit %d /data/local/tmp/screenrecord.mp4" % times).wait() 562 | print(">>>Get Video file...") 563 | time.sleep(1.5) 564 | path = PATH(path) 565 | if not os.path.isdir(path): 566 | os.makedirs(path) 567 | self.adb("pull /data/local/tmp/screenrecord.mp4 %s" % PATH("%s/%s.mp4" % (path, self.timestamp()))).wait() 568 | self.shell("rm /data/local/tmp/screenrecord.mp4") 569 | print(">>>ok") 570 | else: 571 | print("sdk version is %d, less than 19!" % sdk) 572 | sys.exit(0) 573 | 574 | def get_crash_log(self): 575 | # 获取app发生crash的时间列表 576 | time_list = [] 577 | result_list = self.shell("dumpsys dropbox | findstr data_app_crash").stdout.readlines() 578 | for time in result_list: 579 | temp_list = time.split(" ") 580 | temp_time= [] 581 | temp_time.append(temp_list[0]) 582 | temp_time.append(temp_list[1]) 583 | time_list.append(" ".join(temp_time)) 584 | 585 | if time_list is None or len(time_list) <= 0: 586 | print(">>>No crash log to get") 587 | return None 588 | log_file = "T://Exception_log_%s.txt" % self.timestamp() 589 | f = open(log_file, "wb") 590 | for timel in time_list: 591 | cash_log = self.shell(timel).stdout.read() 592 | f.write(cash_log) 593 | f.close() 594 | print(">>>check local file") 595 | 596 | def get_permission_list(self, package_name): 597 | PATH = lambda p: os.path.abspath(p) 598 | permission_list = [] 599 | result_list = self.shell("dumpsys package %s | findstr android.permission" %package_name).stdout.readlines() 600 | for permission in result_list: 601 | permission_list.append(permission.strip()) 602 | pwd = os.path.join(os.getcwd(),"gui_controller\\scriptUtils") 603 | permission_json_file = open("%s\\permission.json"%pwd, "r") 604 | file_content = json.load(permission_json_file)["PermissList"] 605 | permission_json_file.close() 606 | name = "_".join(package_name.split(".")) 607 | f = open(PATH("%s\\%s_permission.txt" %(pwd,name)), "w") 608 | f.write("package: %s\n\n" %package_name) 609 | for permission in permission_list: 610 | for permission_dict in file_content: 611 | if permission == permission_dict["Key"]: 612 | f.write(permission_dict["Key"] + ":\n " + permission_dict["Memo"] + "\n") 613 | f.close() 614 | 615 | def timestamp(self): 616 | return time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time())) 617 | 618 | def get_ui_dump_xml(self, xml_path): 619 | """ 620 | 获取当前Activity的控件树 621 | """ 622 | print(xml_path) 623 | PATH = lambda a: os.path.abspath(a) 624 | if int(self.get_sdk_version()) >= 19: 625 | self.shell("uiautomator dump --compressed /data/local/tmp/uidump.xml").wait() 626 | else: 627 | self.shell("uiautomator dump /data/local/tmp/uidump.xml").wait() 628 | path = PATH(xml_path) 629 | if not os.path.isdir(path): 630 | os.makedirs(path) 631 | self.adb("pull /data/local/tmp/uidump.xml %s" % PATH(path)).wait() 632 | self.shell("rm /data/local/tmp/uidump.xml").wait() 633 | if os.path.exists(os.path.join(path, "uidump.xml")): 634 | return True 635 | else: 636 | return False -------------------------------------------------------------------------------- /framework/core/adb/EventKeys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | POWER = 26 5 | BACK = 4 6 | HOME = 3 7 | MENU = 82 8 | VOLUME_UP = 24 9 | VOLUME_DOWN = 25 10 | SPACE = 62 11 | BACKSPACE = 67 12 | ENTER = 66 13 | MOVE_HOME = 122 14 | MOVE_END = 123 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /framework/core/adb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/core/adb/__init__.py -------------------------------------------------------------------------------- /framework/core/appiumapi/AppiumBaseApi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | @version: python2.7 6 | @author: ‘jayzhen‘ 7 | @license: Apache Licence 8 | @contact: 2431236868@qq.com 9 | @site: http://www.jayzhen.com 10 | @software: PyCharm 11 | @file: AppiumBaseApi.py 12 | @time: 2017/3/28 23:32 13 | """ 14 | from appium.webdriver.common.touch_action import TouchAction 15 | from framework.core.adb.AdbCommand import AdbCmder 16 | from appium.webdriver.common.mobileby import MobileBy as By 17 | from framework.base.GetAllPathCtrl import GetAllPathController 18 | from framework.utils.reporterutils.LoggingUtil import LoggingController 19 | from framework.utils.formatutils.DateTimeUtil import DateTimeManager 20 | import os 21 | import re 22 | import time 23 | import random 24 | 25 | 26 | PATH = lambda a: os.path.abspath(a) 27 | 28 | 29 | class AppiumDriver(object): 30 | 31 | def __init__(self, driver): 32 | self.android = AdbCmder() 33 | self.log4py = LoggingController() 34 | self.driver = driver 35 | self.taction = TouchAction(self.driver) 36 | self.path_get = GetAllPathController() 37 | self.actions = [] 38 | self.xml_file_path = self.path_get.get_dumpxml_path() 39 | self.pattern = re.compile(r"\d+") 40 | self.capturePath = self.path_get.get_capture_path() 41 | 42 | def is_displayed(self, by, value): 43 | is_displayed = False 44 | try: 45 | is_displayed = self.driver.find_element(by, value).is_displayed() 46 | self.log4py.debug("element [ " + str(value) + " ] displayed? " + str(is_displayed)) 47 | except Exception as e: 48 | self.log4py.error("element元素没有点位到"+str(e)) 49 | return is_displayed 50 | 51 | def is_enabled(self, by, value): 52 | ''' 53 | * rewrite the isEnabled method, the element to be find
54 | * 与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 55 | * @param :the locator you want to find the element 56 | * @return the bool value of whether is the WebElement enabled ''' 57 | isEnabled = self.driver.find_element(by, value).is_enabled() 58 | self.log4py.debug("element [ " + str(by) + " ] enabled? " 59 | + (isEnabled)) 60 | return isEnabled 61 | 62 | def is_element_present(self, by, value, timeout): 63 | isSucceed = False 64 | self.log4py.debug("find element [" + value + "]") 65 | timeBegins = time.time() 66 | while(time.time() - timeBegins <= timeout): 67 | try: 68 | self.driver.find_element(by,value) 69 | isSucceed = True 70 | self.log4py.debug("find element [" + value+ "] success") 71 | break 72 | except Exception as e: 73 | self.log4py.error(e) 74 | self.pause(self.pauseTime) 75 | self.operationCheck("isElementPresent", isSucceed) 76 | return isSucceed 77 | 78 | def is_alert_exists(self,timeout): 79 | ''' 80 | * judge if the alert is present in specified seconds
81 | * 在指定的时间内判断弹出的对话框(Dialog)是否存在。 82 | * @param timeout:timeout in seconds 83 | ''' 84 | isSucceed = False 85 | timeBegins = time.time() 86 | while(time.time() - timeBegins <= timeout * 1000): 87 | try: 88 | self.driver.switch_to_alert() 89 | isSucceed = True 90 | break 91 | except Exception as e: 92 | self.log4py.error(e) 93 | self.operationCheck("isAlertExists", isSucceed) 94 | return isSucceed 95 | 96 | def find_element_by_want(self, by, value, timeout): 97 | """ 98 | 通过元素名称定位单个元素 99 | usage: findElementByName(u"设置") 100 | """ 101 | is_succeed = False 102 | element = None 103 | time_begins = time.time() 104 | if timeout is None or timeout == "": 105 | timeout = 3 106 | while time.time() - time_begins <= timeout: 107 | try: 108 | element = self.driver.find_element(by, value) 109 | is_succeed = True 110 | self.log4py.debug("find element [" + str(value) + "] success") 111 | break 112 | except Exception as e: 113 | self.log4py.error(str(e)) 114 | self.log4py.debug("find element [" + str(value) + "] failure") 115 | self.operation_check("find_element_by_want", is_succeed) 116 | return element 117 | 118 | def find_elements_by_want(self, by, value, timeout): 119 | """ 120 | 通过元素名称定位单个元素 121 | usage: findElementByName(u"设置") 122 | """ 123 | is_succeed = False 124 | self.log4py.debug("find elements [" + str(value) + "]") 125 | elements = None 126 | time_begins = time.time() 127 | while (time.time() - time_begins) <= timeout: 128 | try: 129 | elements = self.driver.find_elements(by, value) 130 | is_succeed = True 131 | self.log4py.debug("find elements [" + str(value) + "] success") 132 | break 133 | except Exception as e: 134 | self.log4py.error(e) 135 | self.operation_check("find_elements", is_succeed) 136 | return elements 137 | 138 | def is_element_checked_by_want(self, by, name): 139 | """ 140 | 通过元素名称判断checked的布尔值,返回布尔值列表 141 | """ 142 | element = self.driver.__getattribute__() 143 | return self.__checked("text", name) 144 | 145 | def is_elements_checked_by_want(self, id): 146 | """ 147 | 通过元素id判断checked的布尔值,返回布尔值列表 148 | """ 149 | return self.__checked("resource-id", id) 150 | 151 | def is_selected(self, by, value): 152 | ''' 153 | * rewrite the isSelected method, the element to be find
154 | * 与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 155 | * @param :the locator you want to find the element 156 | * @return the bool value of whether is the WebElement selected ''' 157 | is_selected = self.driver.find_element(by, value).is_selected() 158 | self.log4py.debug("element [ " + value + " ] selected? "+ str(is_selected)) 159 | return is_selected 160 | 161 | def do_pause(self, millisecond): 162 | try: 163 | time.sleep(millisecond) 164 | except Exception as e: 165 | self.log4py.error("pause error:"+str(e)) 166 | 167 | def get(self, url, actionCount): 168 | ''' 169 | * rewrite the get method, adding user defined log
170 | * 地址跳转方法,使用WebDriver原生get方法,加入失败重试的次数定义。 171 | * @param url: the url you want to open. 172 | * @param actionCount:retry: times when load timeout occuers. 173 | ''' 174 | isSucceed = False 175 | for i in range(actionCount): 176 | try: 177 | self.driver.get(url) 178 | self.__log4py.debug("navigate to url [ " + url + " ]") 179 | break 180 | except Exception as e: 181 | self.__log4py.error(e) 182 | self.operationCheck("get", isSucceed) 183 | 184 | def do_navigate_back(self): 185 | ''' 186 | * navigate back
地址跳转方法,与WebDriver原生navigate.back方法内容完全一致。 187 | ''' 188 | self.driver.back() 189 | self.log4py.debug("navigate back") 190 | 191 | def do_navigate_forward(self): 192 | ''' 193 | * navigate forward
地址跳转方法,与WebDriver原生navigate.forward方法内容完全一致。 194 | ''' 195 | self.driver.forward() 196 | self.log4py.debug("navigate forward") 197 | 198 | def get_current_activity(self): 199 | """ 200 | TypeError: 'unicode' object is not callable 是因为方法掉用加了括号 201 | """ 202 | status = self.driver.current_activity 203 | self.log4py.debug("current activity is :" + str(status)) 204 | return status 205 | 206 | def get_current_url(self): 207 | ''' 208 | * rewrite the getCurrentUrl method, adding user defined log
209 | * 与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 210 | * @return the url on your current session ''' 211 | url = self.driver.current_url() 212 | self.log4py.debug("current page url is :" + url) 213 | return url 214 | 215 | def get_window_handles(self): 216 | ''' 217 | * rewrite the getWindowHandles method, adding user defined log
218 | * 与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。' 219 | * @return the window handles set ''' 220 | handles = self.driver.window_handles() 221 | self.log4py.debug("window handles count are:" + len(handles)) 222 | self.log4py.debug("window handles are: " + handles) 223 | return handles 224 | 225 | def get_window_handle(self): 226 | ''' 227 | * rewrite the getWindowHandle method, adding user defined log
228 | * 与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 229 | * @return the window handle ''' 230 | handle = self.driver.current_window_handle() 231 | self.debug("current window handle is:" + handle) 232 | return handle 233 | 234 | def get_page_source(self): 235 | ''' 236 | * rewrite the getPageSource method, adding user defined log
237 | * 与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 238 | * @return the page source ''' 239 | source = self.driver.page_source() 240 | #log4py.debug("get PageSource : [ " + source + " ]") 241 | return source 242 | 243 | def get_tag_name(self, by, value): 244 | ''' 245 | * rewrite the getTagName method, find the element and get its tag 246 | * name
与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 247 | * @param 248 | * the locator you want to find the element 249 | * @return the tagname ''' 250 | tag_name = self.driver.find_element(by, value).tag_name 251 | self.log4py.debug("element [ " + str(by) + " ]'s TagName is: "+ tag_name) 252 | return tag_name 253 | 254 | def get_text(self, by, value): 255 | ''' * rewrite the getText method, find the element and get its own 256 | * text
与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 257 | * @param :the locator you want to find the element 258 | * @return the text ''' 259 | text = self.driver.find_element(by, value).text 260 | self.log4py.debug("element [ " + value + " ]'s text is: " + text) 261 | return text 262 | 263 | def get_attribute(self, by, value, attribute_name): 264 | ''' 265 | * rewrite the getAttribute method, find the element and get its 266 | * attribute value
与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 267 | * @param :the locator you want to find the element 268 | * @param attributeName:the name of the attribute you want to get 269 | * @return the attribute value ''' 270 | value = self.driver.find_element(by, value).get_attribute(attribute_name) 271 | self.log4py.debug("element [ " + str(by) + " ]'s " + attribute_name + "is: " + value) 272 | return value 273 | 274 | def webList_random_select_by_option(self,by,value,timeout): 275 | isSucceed = False 276 | timeBegins = time.time() 277 | while time.time() - timeBegins <= timeout: 278 | try: 279 | webselect = self.driver.find_element(by, value) 280 | selectElement = (webselect) 281 | ooptions = selectElement.options 282 | ooption = random.choice(ooptions) 283 | itemValue = ooption.get_attribute("value") 284 | selectElement.select_by_value(itemValue) 285 | isSucceed = True 286 | self.log4py.debug("item selected by item value [ " + itemValue+ " ] on [ " + str(by) + " ]") 287 | break 288 | except Exception as e: 289 | self.log4py.error(e) 290 | self.pause(self.pauseTime) 291 | self.operationCheck("webList_RandomSelectByOption", isSucceed) 292 | 293 | def select_by_value(self, by, value, itemValue, timeout): 294 | isSucceed = False 295 | try: 296 | if (self.isElementPresent(by,value, timeout)): 297 | element = self.driver.find_element(by, value) 298 | select = (element) 299 | select.select_by_value(itemValue) 300 | self.log4py.debug("item selected by item value [ " + itemValue+ " ] on [ " + value + " ]") 301 | isSucceed = True 302 | except Exception as e: 303 | self.log4py.error(e) 304 | self.operationCheck("selectByValue", isSucceed) 305 | 306 | def scrollbar_slide_to_bottom(self, element): 307 | '''将页面滚动条拖到底部 308 | js="var q=document.documentElement.scrollTop=10000" ''' 309 | js = "var q=document.getElementById('%s').scrollTop=10000" % element 310 | self.driver.execute_script(js) 311 | time.sleep(3) 312 | self.log4py.debug("将元素%s滑动到底部" %element) 313 | 314 | def accept_alert(self): 315 | try: 316 | self.driver.switch_to_alert().accept() 317 | self.log4py.debug("切换到弹窗,并点击确定按钮") 318 | except Exception as e: 319 | self.log4py.error("接受弹窗,出现异常:"+str(e)) 320 | 321 | def get_screen_size(self): 322 | x = self.driver.get_window_size()['width'] 323 | y = self.driver.get_window_size()['height'] 324 | return (x, y) 325 | 326 | def do_clear(self, element): 327 | ''' 328 | * rewrite the clear method, adding user defined log
329 | * 与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 330 | * @param element:the webelement you want to operate ''' 331 | element.clear() 332 | self.log4py.debug("element [ " + element + " ] cleared") 333 | 334 | def do_click(self, by, value, times=3): 335 | ''' 336 | * rewrite the click method, adding user defined log
337 | * 与工具原生API作用完全一致,只是增加了操作结果检查和日志记录。 338 | * @param element:the webelement you want to operate ''' 339 | element = self.find_element_by_want(by,value,times) 340 | if element is None: 341 | self.log4py.debug("没有找到对应的元素:{}".format(str(value))) 342 | return 343 | element.click() 344 | self.log4py.debug("click on element [ " + str(element) + " ] ") 345 | 346 | def do_sendkeys(self, by, value, txt): 347 | element = self.find_element_by_want(by, value, 3) 348 | if element is None: 349 | self.log4py.debug("没有找到对应的元素:{}".format(str(value))) 350 | return 351 | element.send_keys(txt) 352 | self.log4py.debug("send key to element [ " + str(element) + " ] ") 353 | 354 | def do_swipe_up(self, driver, duration_time): 355 | # 操作屏幕向上滑动 356 | size_screen = self.get_screen_size(driver) 357 | x_start = int(size_screen[0] * 0.5) 358 | x_end = x_start 359 | y_start = int(size_screen[1] * 0.75) 360 | y_end = int(size_screen[0] * 0.25) 361 | self.driver.swipe(x_start, y_start, x_end, y_end, duration_time) 362 | print("log:action swipe up:(%d,%d)-(%d,%d)" % (x_start, y_start, x_end, y_end)) 363 | 364 | # 操作屏幕向下滑动 365 | def do_swipe_down(self, driver, duration_time): 366 | size_screen = self.get_screen_size(driver) 367 | x_start = int(size_screen[0] * 0.5) 368 | x_end = x_start 369 | y_start = int(size_screen[1] * 0.25) 370 | y_end = int(size_screen[0] * 0.75) 371 | self.driver.swipe(x_start, y_start, x_end, y_end, duration_time) 372 | print("log:action swipe down:(%d,%d)-(%d,%d)" % (x_start, y_start, x_end, y_end)) 373 | 374 | # 操作屏幕向左←滑动 375 | def do_swipe_left(self, driver, duration_time): 376 | size_screen = self.get_screen_size(driver) 377 | y_start = int(size_screen[0] * 0.5) 378 | y_end = y_start 379 | x_start = int(size_screen[1] * 0.75) 380 | x_end = int(size_screen[0] * 0.25) 381 | self.driver.swipe(x_start, y_start, x_end, y_end, duration_time) 382 | print("log:action swipe left:(%d,%d)-(%d,%d)" % (x_start, y_start, x_end, y_end)) 383 | 384 | # 操作屏幕向右→滑动 385 | def do_swipe_right(self, driver, duration_time): 386 | size_screen = self.get_screen_size(driver) 387 | y_start = int(size_screen[0] * 0.5) 388 | y_end = y_start 389 | x_start = int(size_screen[1] * 0.25) 390 | x_end = int(size_screen[0] * 0.75) 391 | self.driver.swipe(x_start, y_start, x_end, y_end, duration_time) 392 | print("log:action swipe right:(%d,%d)-(%d,%d)" % (x_start, y_start, x_end, y_end)) 393 | 394 | def do_tap_element(self, elemnt): 395 | """Perform a tap action on the element 396 | :Args: 397 | - element - the element to tap 398 | - x - (optional) x coordinate to tap, relative to the top left corner of the element. 399 | - y - (optional) y coordinate. If y is used, x must also be set, and vice versa 400 | :Usage: 401 | """ 402 | self.taction.tap(elemnt, 10, 10).perform() 403 | self.log4py.info("appium driver do touch action at element:%s" % (str(elemnt))) 404 | 405 | def do_press(self, el=None, x=None, y=None): 406 | """Begin a chain with a press down action at a particular element or point 407 | """ 408 | return self 409 | 410 | def do_long_press(self, el, x, y, duration=1000): 411 | """Begin a chain with a press down that lasts `duration` milliseconds 412 | """ 413 | self.taction.press(el, x, y, duration).release().perform() 414 | 415 | def do_wait(self, ms=0): 416 | """Pause for `ms` milliseconds. 417 | """ 418 | return self 419 | 420 | def do_move_to(self, el, x, y): 421 | """Move the pointer from the previous point to the element or point specified 422 | press(el0).moveTo(el1).release() 423 | """ 424 | self.taction.move_to(el, x, y) 425 | 426 | def do_release(self): 427 | """End the action by lifting the pointer off the screen 428 | """ 429 | self.driver.release() 430 | 431 | def do_perform(self): 432 | """Perform the action by sending the commands to the server to be operated upon 433 | """ 434 | # get rid of actions so the object can be reused 435 | pass 436 | 437 | def do_pinch(self,el): 438 | ''' 439 | Places two fingers at the edges of the screen and brings them together. 在 0% 到 100% 内双指缩放屏幕, 440 | ''' 441 | self.driver.pinch(element=el) 442 | 443 | def do_zoom(self, el): 444 | ''' 445 | 放大屏幕 在 100% 以上放大屏幕 446 | ''' 447 | self.driver.zoom(element=el) 448 | 449 | def do_shake(self): 450 | ''' 451 | 模拟设备摇晃 452 | ''' 453 | self.driver.shake() 454 | 455 | # convenience method added to Appium (NOT Selenium 3) 456 | 457 | def do_scroll(self, origin_el, destination_el): 458 | """Scrolls from one element to another 459 | :Args: 460 | - originalEl - the element from which to being scrolling 461 | - destinationEl - the element to scroll to 462 | :Usage: 463 | appium_driver.scroll(el1, el2) 464 | """ 465 | return self 466 | 467 | # convenience method added to Appium (NOT Selenium 3) 468 | 469 | def do_drag_and_drop(self, origin_el, destination_el): 470 | """Drag the origin element to the destination element 471 | :Args: 472 | - originEl - the element to drag 473 | - destinationEl - the element to drag to 474 | """ 475 | self.driver.drag_and_drop(origin_el, destination_el) 476 | 477 | # convenience method added to Appium (NOT Selenium 3) 478 | 479 | def do_flick(self, start_x, start_y, end_x, end_y): 480 | """Flick from one point to another point. 481 | :Args: 482 | - start_x - x-coordinate at which to start 483 | - start_y - y-coordinate at which to start 484 | - end_x - x-coordinate at which to stop 485 | - end_y - y-coordinate at which to stop 486 | :Usage: 487 | appium_driver.flick(100, 100, 100, 400) 488 | """ 489 | return self.driver.flick(start_x, start_y, end_x, end_y) 490 | 491 | def capture_screenshot(self, filepath): 492 | '''* 截取屏幕截图并保存到指定路径 493 | * @param filepath:保存屏幕截图完整文件名称及路径 494 | * @return 无 ''' 495 | try: 496 | self.driver.get_screenshot_as_file(filepath) 497 | except Exception as e: 498 | self.log4py.error("保存屏幕截图失败,失败信息:"+ str(e)) 499 | 500 | def operation_check(self, method_name, is_succeed): 501 | ''' 502 | * public method for handle assertions and screenshot. 503 | * @param isSucceed:if your operation success ''' 504 | if is_succeed: 505 | self.log4py.info("method 【" + method_name + "】 运行通过!") 506 | else: 507 | date_time = DateTimeManager().formated_time("-%Y%m%d-%H%M%S%f") 508 | capture_name = self.capturePath+method_name+date_time+".png" 509 | self.capture_screenshot(capture_name) 510 | self.log4py.error("method 【" + method_name + "】 运行失败,请查看截图快照:"+ capture_name) 511 | 512 | @property 513 | def get_contexts(self): 514 | """ 515 | Returns the contexts within the current session. 516 | :Usage: 517 | driver.contexts 518 | """ 519 | return self.driver.contexts 520 | 521 | @property 522 | def get_current_context(self): 523 | """ 524 | Returns the current context of the current session. 525 | :Usage: 526 | driver.current_context 527 | """ 528 | return self.driver.current_context 529 | 530 | @property 531 | def get_context(self): 532 | """ 533 | Returns the current context of the current session. 534 | :Usage: 535 | driver.context 536 | """ 537 | return self.current_context 538 | 539 | def find_element_by_android_uiautomator(self, uia_string): 540 | """Finds element by uiautomator in Android. 541 | :Args: 542 | - uia_string - The element name in the Android UIAutomator library 543 | :Usage: 544 | driver.find_element_by_android_uiautomator('.elements()[1].cells()[2]') 545 | """ 546 | return self.find_element_by_want(by=By.ANDROID_UIAUTOMATOR, value=uia_string) 547 | 548 | def find_elements_by_android_uiautomator(self, uia_string): 549 | """Finds elements by uiautomator in Android. 550 | :Args: 551 | - uia_string - The element name in the Android UIAutomator library 552 | :Usage: 553 | driver.find_elements_by_android_uiautomator('.elements()[1].cells()[2]') 554 | """ 555 | return self.find_elements(by=By.ANDROID_UIAUTOMATOR, value=uia_string) 556 | 557 | def find_element_by_accessibility_id(self, id): 558 | """Finds an element by accessibility id. 559 | :Args: 560 | - id - a string corresponding to a recursive element search using the 561 | Id/Name that the native Accessibility options utilize 562 | :Usage: 563 | driver.find_element_by_accessibility_id() 564 | """ 565 | return self.find_element_by_want(by=By.ACCESSIBILITY_ID, value=id) 566 | 567 | def find_elements_by_accessibility_id(self, id): 568 | """Finds elements by accessibility id. 569 | :Args: 570 | - id - a string corresponding to a recursive element search using the 571 | Id/Name that the native Accessibility options utilize 572 | 573 | :Usage: 574 | driver.find_elements_by_accessibility_id() 575 | """ 576 | return self.find_elements(by=By.ACCESSIBILITY_ID, value=id) 577 | 578 | def create_web_element(self, element_id): 579 | """ 580 | Creates a web element with the specified element_id. 581 | Overrides method in Selenium WebDriver in order to always give them 582 | Appium WebElement 583 | """ 584 | self.log4py.info("创建一个id为%s的元素" %element_id) 585 | return self.driver.create_web_element(element_id) 586 | 587 | def get_app_strings(self, language=None, string_file=None): 588 | """Returns the application strings from the device for the specified language. 589 | :Args: 590 | - language - strings language code 591 | - string_file - the name of the string file to query 592 | """ 593 | app_str = self.driver.app_strings 594 | self.log4py.info("获取app的strings" + str(app_str)) 595 | return app_str 596 | 597 | def do_reset(self): 598 | """Resets the current application on the device. 599 | """ 600 | self.driver.reset() 601 | 602 | def do_hide_keyboard(self, key_name=None, key=None, strategy=None): 603 | """Hides the software keyboard on the device. In iOS, use `key_name` to press 604 | a particular key, or `strategy`. In Android, no parameters are used. 605 | :Args: 606 | - key_name - key to press 607 | - strategy - strategy for closing the keyboard (e.g., `tapOutside`) 608 | """ 609 | self.driver.hide_keyboard() 610 | 611 | # Needed for Selendroid 612 | def do_keyevent(self, keycode, metastate=None): 613 | """Sends a keycode to the device. Android only. Possible keycodes can be 614 | found in http://developer.android.com/reference/android/view/KeyEvent.html. 615 | 616 | :Args: 617 | - keycode - the keycode to be sent to the device 618 | - metastate - meta information about the keycode being sent 619 | """ 620 | pass 621 | 622 | def do_press_keycode(self, keycode, metastate=None): 623 | """Sends a keycode to the device. Android only. Possible keycodes can be 624 | found in http://developer.android.com/reference/android/view/KeyEvent.html. 625 | 626 | :Args: 627 | - keycode - the keycode to be sent to the device 628 | - metastate - meta information about the keycode being sent 629 | """ 630 | pass 631 | 632 | def do_long_press_keycode(self, keycode, metastate=None): 633 | """Sends a long press of keycode to the device. Android only. Possible keycodes can be 634 | found in http://developer.android.com/reference/android/view/KeyEvent.html. 635 | 636 | :Args: 637 | - keycode - the keycode to be sent to the device 638 | - metastate - meta information about the keycode being sent 639 | """ 640 | pass 641 | 642 | def set_value(self, element, value): 643 | """Set the value on an element in the application. 644 | 645 | :Args: 646 | - element - the element whose value will be set 647 | - Value - the value to set on the element 648 | """ 649 | self.driver.set_value(element, value) 650 | 651 | def do_pull_file(self, path): 652 | """Retrieves the file at `path`. Returns the file's content encoded as 653 | Base64. 654 | :Args: 655 | - path - the path to the file on the device 656 | """ 657 | pass 658 | 659 | def do_pull_folder(self, path): 660 | """Retrieves a folder at `path`. Returns the folder's contents zipped 661 | and encoded as Base64. 662 | :Args: 663 | - path - the path to the folder on the device 664 | """ 665 | pass 666 | 667 | def do_push_file(self, path, base64data): 668 | """Puts the data, encoded as Base64, in the file specified as `path`. 669 | :Args: 670 | - path - the path on the device 671 | - base64data - data, encoded as Base64, to be written to the file 672 | """ 673 | pass 674 | 675 | def do_background_app(self, seconds): 676 | """Puts the application in the background on the device for a certain duration. 677 | :Args: 678 | - seconds - the duration for the application to remain in the background 679 | """ 680 | self.driver.background_app(seconds) 681 | self.log4py.info("将app放置后台%s秒" %str(seconds)) 682 | return self 683 | 684 | def is_app_installed(self, bundle_id): 685 | """Checks whether the application specified by `bundle_id` is installed 686 | on the device. 687 | :Args: 688 | - bundle_id - the id of the application to query 689 | """ 690 | pass 691 | 692 | def do_install_app(self, app_path): 693 | """Install the application found at `app_path` on the device. 694 | :Args: 695 | - app_path - the local or remote path to the application to install 696 | """ 697 | self.driver.install_app(app_path) 698 | 699 | def do_remove_app(self, app_id): 700 | """Remove the specified application from the device. 701 | 702 | :Args: 703 | - app_id - the application id to be removed 704 | """ 705 | pass 706 | 707 | def do_launch_app(self): 708 | """Start on the device the application specified in the desired capabilities. 709 | """ 710 | pass 711 | 712 | def do_close_app(self): 713 | """Stop the running application, specified in the desired capabilities, on 714 | the device. 715 | """ 716 | self.driver.close_app() 717 | 718 | def start_activity(self, app_package, app_activity, **opts): 719 | """Opens an arbitrary activity during a test. If the activity belongs to 720 | another application, that application is started and the activity is opened. 721 | This is an Android-only method. 722 | :Args: 723 | - app_package - The package containing the activity to start. 724 | - app_activity - The activity to start. 725 | - app_wait_package - Begin automation after this package starts (optional). 726 | - app_wait_activity - Begin automation after this activity starts (optional). 727 | - intent_action - Intent to start (optional). 728 | - intent_category - Intent category to start (optional). 729 | - intent_flags - Flags to send to the intent (optional). 730 | - optional_intent_arguments - Optional arguments to the intent (optional). 731 | - dont_stop_app_on_reset - Should the app be stopped on reset (optional)? 732 | """ 733 | return self.driver.start_activity(app_package, app_activity) 734 | 735 | def end_test_coverage(self, intent, path): 736 | """Ends the coverage collection and pull the coverage.ec file from the device. 737 | Android only. 738 | See https://github.com/appium/appium/blob/master/docs/en/android_coverage.md 739 | :Args: 740 | - intent - description of operation to be performed 741 | - path - path to coverage.ec file to be pulled from the device 742 | """ 743 | pass 744 | 745 | def do_lock(self, seconds): 746 | """Lock the device for a certain period of time. iOS only. 747 | :Args: 748 | - the duration to lock the device, in seconds 749 | """ 750 | self.driver.lock(seconds) 751 | 752 | def do_open_notifications(self): 753 | """Open notification shade in Android (API Level 18 and above) 754 | """ 755 | self.driver.open_notifications() 756 | 757 | @property 758 | def do_network_connection(self): 759 | """Returns an integer bitmask specifying the network connection type. 760 | Android only. 761 | Possible values are available through the enumeration `appium.webdriver.ConnectionType` 762 | """ 763 | return self.driver.network_connection 764 | 765 | def set_network_connection(self, connectionType): 766 | """Sets the network connection type. Android only. 767 | Possible values: 768 | Value (Alias) | Data | Wifi | Airplane Mode 769 | ------------------------------------------------- 770 | 0 (None) | 0 | 0 | 0 771 | 1 (Airplane Mode) | 0 | 0 | 1 772 | 2 (Wifi only) | 0 | 1 | 0 773 | 4 (Data only) | 1 | 0 | 0 774 | 6 (All network on) | 1 | 1 | 0 775 | These are available through the enumeration `appium.webdriver.ConnectionType` 776 | 777 | :Args: 778 | - connectionType - a member of the enum appium.webdriver.ConnectionType 779 | """ 780 | pass 781 | 782 | @property 783 | def get_available_ime_engines(self): 784 | """Get the available input methods for an Android device. Package and 785 | activity are returned (e.g., ['com.android.inputmethod.latin/.LatinIME']) 786 | Android only. 787 | """ 788 | available_ime = self.driver.available_ime_engines 789 | self.log4py.info("可见的输入法:" + str(available_ime)) 790 | return available_ime 791 | 792 | def is_ime_active(self): 793 | """Checks whether the device has IME service active. Returns True/False. 794 | Android only. 795 | """ 796 | return self.driver.is_ime_active() 797 | 798 | def do_activate_ime_engine(self, engine): 799 | """激活输入法引擎Activates the given IME engine on the device.Android only. 800 | :Args: 801 | - engine - the package and activity of the IME engine to activate (e.g., 802 | 'com.android.inputmethod.latin/.LatinIME') 803 | """ 804 | self.log4py.info("激活输入法引擎:" + str(engine)) 805 | self.driver.activate_ime_engine(engine) 806 | 807 | def deactivate_ime_engine(self): 808 | """Deactivates the currently active IME engine on the device. 809 | Android only. 810 | """ 811 | self.driver.deactivate_ime_engine() 812 | self.log4py.info("将当前活跃的输入法引擎失效") 813 | 814 | @property 815 | def get_active_ime_engine(self): 816 | """Returns the activity and package of the currently active IME engine (e.g., 817 | 'com.android.inputmethod.latin/.LatinIME'). 818 | Android only. 819 | """ 820 | current_ime = self.driver.active_ime_engine 821 | self.log4py.info("获取到当前的输入法:" + str(current_ime)) 822 | return current_ime 823 | 824 | def device_time(self): 825 | """Returns the appium server Settings for the current session. 826 | Do not get Settings confused with Desired Capabilities, they are 827 | separate concepts. See https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md 828 | """ 829 | return self.driver.device_time() 830 | 831 | def update_settings(self, settings): 832 | """Set settings for the current session. 833 | For more on settings, see: https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md 834 | 835 | :Args: 836 | - settings - dictionary of settings to apply to the current test session 837 | """ 838 | pass 839 | 840 | def toggle_location_services(self): 841 | """Toggle the location services on the device. Android only. 842 | """ 843 | pass 844 | 845 | def set_location(self, latitude, longitude, altitude): 846 | """Set the location of the device 847 | :Args: 848 | - latitude - String or numeric value between -90.0 and 90.00 849 | - longitude - String or numeric value between -180.0 and 180.0 850 | - altitude - String or numeric value 851 | """ 852 | pass 853 | 854 | @property 855 | def get_device_time(self): 856 | """Returns the date and time fomr the device 857 | """ 858 | return self.driver.device_time 859 | 860 | -------------------------------------------------------------------------------- /framework/core/appiumapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/core/appiumapi/__init__.py -------------------------------------------------------------------------------- /framework/core/dos/DosCommand.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | 11 | 执行window下特定的dos命令:包括并发执行appium脚本后的关闭启动的端口。 12 | """ 13 | 14 | import subprocess 15 | from framework.utils.reporterutils.LoggingUtil import LoggingController 16 | 17 | 18 | class WindowCmder(object): 19 | 20 | def __init__(self): 21 | self.log4py = LoggingController() 22 | 23 | def is_port_used(self, port_num): 24 | """ 25 | 检查端口是否被占用 26 | netstat -aon | findstr port 能够获得到内容证明端口被占用 27 | :param port_num: 28 | :return: 29 | """ 30 | flog = True 31 | try: 32 | port_res = subprocess.Popen('netstat -ano | findstr %s' % port_num, shell=True, stdout=subprocess.PIPE, 33 | stderr=subprocess.PIPE).stdout.readlines() 34 | if len(port_res) <= 0: 35 | self.log4py.info(str(port_num) + " port unoccupied.") 36 | flog = False 37 | else: 38 | self.log4py.error(str(port_num) + " port has been occupied.") 39 | except Exception as e: 40 | self.log4py.error(str(port_num) + " port get occupied status failure: " + str(e)) 41 | return flog 42 | 43 | def exec_cmd(self, cmd_str): 44 | """ 45 | :param cmd_str: 46 | :return: 命令执行的成功与否 47 | """ 48 | subprocess.Popen(cmd_str, shell=True) 49 | return True 50 | 51 | def exec_cmd_console(self, cmd_str): 52 | """ 53 | :param cmd_str: 54 | :return: 将命令执行后的结果内容返回 55 | """ 56 | content = "" 57 | return content 58 | 59 | def kill_service_on_pid(self, pid): 60 | """ 61 | 杀死为pid进程号的进程:taskkill -F -PID pid_num 62 | :param pid: 63 | :return: 64 | """ 65 | return self.exec_cmd("taskkill -F -PID %s" % pid) 66 | 67 | def kill_service_on_name(self, service_name): 68 | """ 69 | 通过进程的名称来终结进程:taskkill -F -im service_name 70 | :param service_name: 71 | :return: 72 | """ 73 | return True 74 | -------------------------------------------------------------------------------- /framework/core/dos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/core/dos/__init__.py -------------------------------------------------------------------------------- /framework/core/exceptions/Exception.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | """ 11 | 12 | 13 | class ScriptException(Exception): 14 | 15 | def __init__(self, str_param): 16 | self.str_param = str_param 17 | 18 | def _str_(self): 19 | return self.str_param 20 | 21 | -------------------------------------------------------------------------------- /framework/core/exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/core/exceptions/__init__.py -------------------------------------------------------------------------------- /framework/domain/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | """ 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @version: v1.0 8 | @contact: jayzhen_testing@163.com 9 | @site: http://blog.csdn.net/u013948858 10 | @file: __init__.py.py 11 | @time: 2017/10/24 21:47 12 | """ 13 | -------------------------------------------------------------------------------- /framework/domain/mobile_infos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @version: v1.0 6 | @author: jayzhen 7 | @license: Apache Licence 8 | @email: jayzhen_testing@163.com 9 | @software: PyCharm 10 | @file: mobile_infos.py 11 | @time: 2017/10/22 17:52 12 | """ 13 | 14 | 15 | class MobileInfos(object): 16 | 17 | def __init__(self): 18 | self.sno = None 19 | self.phone_brand = None 20 | self.phone_model = None 21 | self.os_version = None 22 | self.ram = None 23 | self.dpi = None 24 | self.image_resolution = None 25 | self.ip = None 26 | -------------------------------------------------------------------------------- /framework/initdriver/InitAppiumDriver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | @time: 2017/8/3 17:22 当前只能是一个设备 11 | @TODO 多线程并发执行脚本时,需要将端口、手机设备的关联做好;多个设备时desired_capabilities属性只能是一个设备 12 | """ 13 | import os 14 | import re 15 | import subprocess 16 | from urllib.error import URLError 17 | 18 | from appium import webdriver 19 | from framework.initdriver.InitConfig import InitConfiger 20 | 21 | from framework.core.adb.AdbCommand import AdbCmder 22 | from framework.utils.fileutils.ConfigCommonUtil import ConfigController 23 | from framework.utils.reporterutils.LoggingUtil import LoggingController 24 | 25 | 26 | class InitDriverOption(object): 27 | def __init__(self): 28 | self.log4py = LoggingController() 29 | self.run_cfg = InitConfiger() 30 | self.android = AdbCmder() 31 | self.run_data = None 32 | 33 | def get_desired_capabilities(self, sno): 34 | device_info = {"udid": sno} 35 | try: 36 | result = subprocess.Popen("adb -s %s shell getprop" % sno, shell=True, stdout=subprocess.PIPE, 37 | stderr=subprocess.PIPE).stdout.readlines() 38 | for res in result: 39 | if re.search(r"ro\.build\.version\.release", res): 40 | device_info["platformVersion"] = (res.split(': ')[-1].strip())[1:-1] 41 | elif re.search(r"ro\.product\.model", res): 42 | device_info["deviceName"] = (res.split(': ')[-1].strip())[1:-1] 43 | if "platformVersion" in device_info.keys() and "deviceName" in device_info.keys(): 44 | break 45 | except Exception as e: 46 | self.log4py.error("获取手机信息时出错 :" + str(e)) 47 | return None 48 | desired_caps_conf = self.run_cfg.get_desired_caps_conf() 49 | desired_caps = device_info.copy() 50 | desired_caps.update(desired_caps_conf) 51 | return desired_caps 52 | 53 | def get_appium_port(self, sno): 54 | """ 55 | 这里读取启动服务时生成的那个ini配置文件,读取其中sno对应的状态及服务的port 56 | :param sno: 57 | :return: 58 | """ 59 | path = (os.getcwd()).split('src')[0] + "\\testconfig\\appiumService.ini" 60 | ff = ConfigController(path) 61 | try: 62 | port = ff.get(sno, sno) 63 | if port: 64 | self.log4py.info("获取到{}设备对应的appium服务端口{}".format(sno, port)) 65 | return port 66 | except Exception as e: 67 | self.log4py.debug("{}设备对应的appium未启动".format(sno)) 68 | return None 69 | 70 | def is_port_used(self, port_num): 71 | """ 72 | 检查端口是否被占用 73 | netstat -aon | findstr port 能够获得到内容证明端口被占用 74 | """ 75 | flag = False 76 | try: 77 | port_res = subprocess.Popen('netstat -ano | findstr %s | findstr LISTENING' % port_num, shell=True, stdout=subprocess.PIPE, 78 | stderr=subprocess.PIPE).stdout.readlines() 79 | reg = re.compile(str(port_num)) 80 | for i in range(len(port_res)): 81 | ip_port = port_res[i].strip().split(" ") 82 | if re.search(reg, ip_port[1]): 83 | flag = True 84 | self.log4py.info(str(port_num) + " 端口的服务已经启动." ) 85 | if not flag: 86 | self.log4py.info(str(port_num) + " 端口的服务未启动.") 87 | except Exception as e: 88 | self.log4py.error(str(port_num) + " port get occupied status failure: " + str(e)) 89 | return flag 90 | 91 | def before_create_driver(self, sno): 92 | """ 93 | 在实例appium driver前,进行设备的操作:安装、卸载 94 | :param sno: 95 | :return: 96 | """ 97 | self.android.set_serialno_num(sno) 98 | self.run_data = self.run_cfg.get_run_conf() 99 | if int(self.run_data["is_first"]) == 0: 100 | if self.android.is_install_app(self.run_data["pkg_name"]): 101 | self.android.do_uninstall_app(self.run_data["pkg_name"]) 102 | self.log4py.info("对测试设备进行卸载应用操作:{}".format(self.run_data["pkg_name"])) 103 | if self.android.do_install_app(self.run_data["apk_file_path"], self.run_data["pkg_name"]): 104 | self.log4py.info("重新安装应用成功") 105 | elif int(self.run_data["is_first"]) == 1: 106 | self.log4py.info("非首次执行,可以直接进行正常用例操作") 107 | 108 | def get_android_driver(self, sno): 109 | desired_caps = self.get_desired_capabilities(sno) 110 | self.before_create_driver(desired_caps['udid']) 111 | port = self.get_appium_port(desired_caps["udid"]) 112 | if not self.is_port_used(port): 113 | self.log4py.debug("设备号[{}]对应的appium服务没有启动".format(desired_caps['udid'])) 114 | return None 115 | url = 'http://127.0.0.1:%s/wd/hub' % (port.strip()) 116 | self.log4py.debug(url) 117 | self.log4py.debug(desired_caps) 118 | num = 0 119 | while num <= 5: 120 | try: 121 | driver = webdriver.Remote(url, desired_caps) 122 | except URLError as e: 123 | self.log4py.error("连接appium服务,实例化driver时出错,尝试重连...({})".format(num)) 124 | num = num + 1 125 | continue 126 | if self.run_data["wait_activity"] is not None: 127 | driver.wait_activity(self.run_data["wait_activity"], 10) 128 | else: 129 | driver.implicitly_wait(10) 130 | self.log4py.info("webdriver连接信息:{}:{}".format(url, str(desired_caps))) 131 | return driver 132 | return None 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /framework/initdriver/InitConfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | """ 5 | @version: v1.0 6 | @author: jayzhen 7 | @license: Apache Licence 8 | @contact: jayzhen_testing@163.com 9 | @site: http://blog.csdn.net/u013948858 10 | @software: PyCharm 11 | @license: Apache Licence 12 | @file: InitConfig.py 13 | @time: 2017/7/31 10:49 14 | """ 15 | from framework.utils.fileutils.ConfigCommonUtil import ConfigController 16 | import os 17 | 18 | 19 | class InitConfiger(object): 20 | 21 | def __init__(self): 22 | path = (os.getcwd()).split('src')[0] + "\\testconfig\\run.ini" 23 | self.cf = ConfigController(path) 24 | 25 | def get_run_conf(self): 26 | section = "run" 27 | try: 28 | # 是否是第一次跑,或者是重新跑,为0时会重新安装指定apk,并执行任务;为1时直接启动安装的app进行任务操作 29 | is_first = self.cf.get(section, "isFirst") 30 | # app的包名 31 | pkg_name = self.cf.get(section, "pkgName") 32 | # 启动app的main activity 33 | launch_activity = self.cf.get(section, "launchActivity") 34 | # 自动化启动app时,需要这个等待来做缓冲,避免启动页面挡住操作 35 | wait_activity = self.cf.get(section, "waitActivity") 36 | # 到isFirst为0时,就进行安装操作 37 | apk_file_path = self.cf.get(section, "apkFilePath") 38 | except Exception as e: 39 | return None 40 | return {"is_first": is_first, "pkg_name": pkg_name, "launch_activity": launch_activity, "wait_activity": wait_activity, "apk_file_path": apk_file_path} 41 | 42 | def set_run_conf(self, is_first, pkg_name, launch_activity, wait_activity, apk_file_path): 43 | flag = False 44 | section = "run" 45 | try: 46 | self.cf.set(section, "isFirst", is_first) 47 | self.cf.set(section, "pkgName", pkg_name) 48 | self.cf.set(section, "launchActivity", launch_activity) 49 | self.cf.set(section, "waitActivity", wait_activity) 50 | self.cf.get(section, "apkFilePath", apk_file_path) 51 | flag = True 52 | except Exception as e: 53 | return None 54 | return flag 55 | 56 | def get_desired_caps_conf(self): 57 | section = "desired_caps" 58 | # 这些参数都是启动app时需要的,但是在代码读取参数的时候,不一定都读取,因为有些参数不是固定的 59 | dc = {} 60 | try: 61 | dc["automationName"] = self.cf.get(section, "automationName") 62 | dc["platformName"] = self.cf.get(section, "platformName") 63 | # dc["app"] = self.cf.get(section, "app") 64 | dc["appPackage"] = self.cf.get(section, "appPackage") 65 | dc["appActivity"] = self.cf.get(section, "appActivity") 66 | dc["noSign"] = self.cf.get(section, "noSign") 67 | dc["unicodeKeyboard"] = self.cf.get(section, "unicodeKeyboard") 68 | dc["resetKeyboard"] = self.cf.get(section, "resetKeyboard") 69 | except Exception as e: 70 | return None 71 | return dc 72 | 73 | -------------------------------------------------------------------------------- /framework/initdriver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/initdriver/__init__.py -------------------------------------------------------------------------------- /framework/initservice/InitService.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | @time: 2017/5/13 11 | """ 12 | import subprocess 13 | import re 14 | import os 15 | import threading 16 | from multiprocessing import Process 17 | import time 18 | from framework.utils.reporterutils.LoggingUtil import LoggingController 19 | from framework.base.GetAllPathCtrl import GetAllPathController 20 | from framework.utils.fileutils.CreateConfigUtil import CreateConfigFile 21 | 22 | 23 | class RunServer(threading.Thread): 24 | def __init__(self, cmd): 25 | threading.Thread.__init__(self) 26 | self.cmd = cmd 27 | 28 | def run(self): 29 | # 20170802 尽可能使用subprocess代替os.system执行命令,避免一些错误 30 | # os.system(i) 31 | # fp = open("AppiumTestProject/testresult/logs4appium/933733961f382.txt", 'a') 32 | # 20171219 可以使用fp对象传给stdout 33 | p = subprocess.Popen(self.cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) 34 | p.wait() 35 | time.sleep(5) 36 | 37 | 38 | class ServicePort(object): 39 | def __init__(self): 40 | self.log4py = LoggingController() 41 | path_obj = GetAllPathController() 42 | self.appium_log_path = path_obj.get_appium_logs_path() 43 | self.appium_port_list = [] 44 | self.bootstrap_port_list = [] 45 | self.device_list = [] 46 | self.cfg = CreateConfigFile() 47 | self.tmp = {} 48 | 49 | def is_port_used(self, port_num): 50 | """ 51 | 检查端口是否被占用 52 | netstat -aon | findstr port 能够获得到内容证明端口被占用 53 | """ 54 | flag = False 55 | try: 56 | port_res = subprocess.Popen('netstat -ano | findstr %s | findstr LISTENING' % port_num, shell=True, stdout=subprocess.PIPE, 57 | stderr=subprocess.PIPE).stdout.readlines() 58 | reg = re.compile(str(port_num)) 59 | for i in range(len(port_res)): 60 | ip_port = port_res[i].strip().split(" ") 61 | if re.search(reg, ip_port[1]): 62 | flag = True 63 | self.log4py.info(str(port_num) + " 端口已经在使用,对应的进程是:" + str(ip_port[-1])) 64 | self.tmp[port_num] = ip_port[-1] 65 | if not flag: 66 | self.log4py.info(str(port_num) + " 端口没有被占用.") 67 | except Exception as e: 68 | self.log4py.error(str(port_num) + " port get occupied status failure: " + str(e)) 69 | return flag 70 | 71 | def is_live_service(self, port): 72 | """ 73 | 检查这个端口是否存在一个活动的service,就返回这个端口service的pid 74 | :param port: 这个port来自appiumservice.ini文件 75 | """ 76 | return self.is_port_used(port) 77 | 78 | def __generat_port_list(self, port_start, num): 79 | """ 80 | 根据链接电脑的设备来创建num个端口号(整形) 电脑有0-65535个端口 81 | """ 82 | new_port_list = [] 83 | while len(new_port_list) != num: 84 | if port_start >= 0 and port_start <= 65535: 85 | if not self.is_port_used(port_start): 86 | new_port_list.append(port_start) 87 | port_start = port_start + 1 88 | return new_port_list 89 | 90 | def __get_devices(self): 91 | """ 92 | 获取链接电脑的设备数 93 | """ 94 | self.device_list = [] 95 | try: 96 | result = subprocess.Popen("adb devices", shell=True, stdout=subprocess.PIPE, 97 | stderr=subprocess.PIPE).stdout.readlines() 98 | result.reverse() # 将readlines结果反向排序 99 | for line in result[1:]: 100 | """ 101 | List of devices attached 102 | * daemon not running. starting it now at tcp:5037 * 103 | * daemon started successfully * 104 | """ 105 | if "attached" not in line.strip() and "daemon" not in line.strip(): 106 | self.device_list.append(line.split()[0]) 107 | else: 108 | break 109 | except Exception as e: 110 | self.log4py.error("启动appium前查询连接的设备情况,发生错误:{}".format(str(e))) 111 | return self.device_list 112 | 113 | def __get_port_list(self, start): 114 | """ 115 | 只用传送一个开始值,就行了 116 | """ 117 | if self.device_list is not None: 118 | device_num = self.device_list 119 | else: 120 | device_num = self.__get_devices() 121 | port_list = self.__generat_port_list(start, len(device_num)) 122 | return port_list 123 | 124 | def __generate_service_command(self): 125 | """ 126 | generat_port_list (service_port, conn_port, udid)->command 127 | :return 是一个以端口号为key的dict 128 | """ 129 | self.appium_port_list = self.__get_port_list(4490) 130 | self.bootstrap_port_list = self.__get_port_list(2233) 131 | # 20170804 将service_cmd list类型换成dict --> {port:cmd,port1:cmd2} ,port留作执行cmd后的端口校验 132 | service_cmd = {} 133 | for i in range(len(self.device_list)): 134 | # 20170802 命令中如果带有路径尽量使用斜杠,不使用反斜杠(win环境中是单个),如使用记得变成双斜杠 135 | # appium_path = 'start /b node D:/Android/Appium/node_modules/appium/lib/server/main.js -p ' 136 | # 这两个方式都可以在后台启动一个appium的服务 137 | cmd = "start /b appium -p " + str(self.appium_port_list[i]) + " -a 127.0.0.1" + " -bp " + str(self.bootstrap_port_list[i]) + " -U " + str(self.device_list[i]) + " >" + str(self.appium_log_path) + str(self.device_list[i]) + ".txt" 138 | service_cmd[str(self.appium_port_list[i])] = cmd 139 | return service_cmd 140 | 141 | def kill_service_on_pid(self, pid): 142 | if pid is not None: 143 | os.system("taskkill -F -PID %s" % pid) 144 | self.log4py.info("PID:%s 关闭端口服务成功" % pid) 145 | 146 | def stop_all_appium_server(self): 147 | """ 148 | 20170802 149 | @auther jayzhen 150 | @pm 将service_port中启动的service进行关闭 151 | """ 152 | c = CreateConfigFile() 153 | server_list = c.get_all_appium_server_port() 154 | if len(server_list) <= 0: 155 | self.log4py.debug("请你确认是否有appium服务启动") 156 | return None 157 | 158 | for i in range(len(server_list)): 159 | self.log4py.info("准备关闭端口 %s 的服务" % server_list[i]) 160 | if self.is_live_service(server_list[i]): 161 | self.kill_service_on_pid(self.tmp[server_list[i]]) 162 | 163 | def check_service(self, times=5): 164 | # 检查服务是否已经启动 165 | begin = time.time() 166 | for i in range(len(self.appium_port_list)): 167 | p = self.appium_port_list[i] 168 | while time.time() - begin <= times: 169 | if self.is_live_service(p): 170 | self.log4py.info("appium server 端口为{}的服务已经启动,bootstrap监听的端口也已设置好".format(p)) 171 | # 服务启动正常,就写入配置文件 172 | self.cfg.set_appium_uuid_port(self.device_list[i], self.appium_port_list[i], self.bootstrap_port_list[i]) 173 | break 174 | self.log4py.info("appium server 端口为{}的服务未启动".format(p)) 175 | 176 | def start_services(self): 177 | """ 178 | 根据appium端口、链接手机端口、手机serialno表示,创建一个服务器;启动有些延迟 179 | 需要将appium和手机sno放到文件中供初始化driver使用,xml、ini、conf、json文件格式都行 180 | 20171218 现在考虑一个问题:是否在没有设备连接的时候就把这个服务启动起来? 181 | 如果启动了:写入配置的内容如何定义?后续有设备连接上了,如果刷新配置文件中的内容? 182 | 最终还是没有设备就不启动了(或者给个开关也行) 183 | """ 184 | self.device_list = self.__get_devices() 185 | if self.device_list is None or len(self.device_list) <= 0: 186 | self.log4py.debug("当前没有设备连接到pc,无法进行appium服务端口的映射,无法启动对应的服务") 187 | return None 188 | 189 | service_list = self.__generate_service_command() 190 | # 启动服务 191 | if len(service_list) > 0: 192 | for i in service_list: 193 | self.log4py.info("通过线程启动服务的命令:{}".format(service_list[i])) 194 | t1 = RunServer(service_list[i]) 195 | p = Process(target=t1.start()) 196 | p.start() 197 | # 20171221 等待5秒钟,等待一下进程 198 | time.sleep(5) 199 | -------------------------------------------------------------------------------- /framework/initservice/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | """ 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @version: v1.0 8 | @contact: jayzhen_testing@163.com 9 | @site: http://blog.csdn.net/u013948858 10 | @file: __init__.py.py 11 | @time: 2017/10/27 22:50 12 | """ 13 | -------------------------------------------------------------------------------- /framework/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/utils/__init__.py -------------------------------------------------------------------------------- /framework/utils/bat/startAppium.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | title startAppiumServer 3 | cmd /c "appium -a 127.0.0.1 -p 4723" -------------------------------------------------------------------------------- /framework/utils/bat/stopAppium.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | title stopAppiumServer 3 | tasklist /V|find "startAppiumServer">nul 4 | if %errorlevel%==0 ( 5 | ::关闭appium服务 6 | taskkill /F /IM node.exe 7 | taskkill /F /FI "WINDOWTITLE eq startAppiumServer" 8 | ) 9 | taskkill /F /FI "WINDOWTITLE eq stopAppiumServer" -------------------------------------------------------------------------------- /framework/utils/databaseutils/ExcelDataUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | 11 | python实现读取Excel文件中的内容 12 | 准备:环境中必须有相关的包 13 | 1.找到文件所在路径 14 | 2.打开文件 15 | 3.获取内容行数 16 | 4.读取指定位子的数据 17 | """ 18 | 19 | import xlrd 20 | from xlutils.copy import copy 21 | from framework.utils.reporterutils.LoggingUtil import LoggingController 22 | from framework.utils.fileutils.FileCheckAndGetPath import FileChecKController 23 | 24 | 25 | class ExcelManager(object): 26 | 27 | def __init__(self, excelfilename): 28 | self.excelfilename = excelfilename 29 | fc = FileChecKController() 30 | boolean = fc.is_has_file(excelfilename) 31 | if boolean: 32 | self.excel_path = fc.get_fileabspath() 33 | self.log4py = LoggingController() 34 | 35 | def read_excel(self, excel_sheet_name): 36 | 37 | """打开目标excel文件 r--读,w--写(覆盖),a--追加写""" 38 | xls_data = xlrd.open_workbook(self.excel_path, "rb") 39 | table = xls_data.sheet_by_name(excel_sheet_name) #打开sheet页 40 | self.log4py.debug("打开的%s文件中的sheet页" % self.excelfilename) 41 | return table # 将指定的sheet页对象返回给调用者 42 | 43 | def writ_excel(self, row, column, value): 44 | x_data = xlrd.open_workbook(self.excel_path, "rb") #只能是xls文件 45 | copy_sheet = copy(x_data) #copy,并对附件进行操作 46 | write_xls = copy_sheet.get_sheet(0) #得到附件中的sheet页 47 | write_xls.write(row, column, value) #将测试的结果追加到附件中sheet页中每一行的后面 48 | copy_sheet.save(self.excel_path) #覆盖保存(注意编码错误) 49 | -------------------------------------------------------------------------------- /framework/utils/databaseutils/SQLController.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding=utf-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘jayzhen‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @time: 2017/3/29 13:12 10 | 先抽象后具体,保证整体的流程,在细化过程中的操作 11 | """ 12 | 13 | import mysqldb 14 | from framework.utils.fileutils.ConfigCommonUtil import ConfigController 15 | from framework.utils.fileutils.FileCheckAndGetPath import FileChecKController 16 | from framework.utils.reporterutils.LoggingUtil import LoggingController 17 | 18 | 19 | class MySQLController(object): 20 | 21 | def __init__(self): 22 | self.logger = LoggingController() 23 | self.fc = FileChecKController() 24 | boolean = self.fc.is_has_file("db.ini") 25 | if boolean: 26 | self.inipath = self.fc.get_fileabspath() 27 | self.conf = ConfigController(self.inipath) 28 | self.host = str(self.conf.get("dbset", "host")) 29 | self.port = int(self.conf.get("dbset", "port")) 30 | self.user = str(self.conf.get("dbset", "user")) 31 | self.passwd = str(self.conf.get("dbset", "passwd")) 32 | self.db = str(self.conf.get("dbset", "db")) 33 | self.charset = str(self.conf.get("dbset", "charset")) 34 | self.conn = mysqldb.Connect(self.host, self.user, self.passwd, self.db, self.port, self.charset) 35 | self.logger.debug("数据库初始化完成"+self.host+str(self.port)+self.db+self.charset) 36 | 37 | def execute_select(self, sql): 38 | cursor = self.conn.cursor() 39 | try: 40 | cursor.execute(sql) 41 | self.logger.debug("check_acct_available :" +sql) 42 | res = cursor.fetchall() 43 | if len(res) < 1: 44 | self.logger.error("%s执行查询内容不存在"%sql) 45 | return res 46 | finally: 47 | cursor.close() 48 | return None 49 | 50 | def execute_add_one(self, sql): 51 | cursor = self.conn.cursor() #操作数据库的游标 52 | try: 53 | cursor.execute(sql) #执行sql语句 54 | if cursor.rowcount == 1: 55 | self.logger.debug("%s添加成功"%sql) 56 | self.conn.commit() #执行成功后向数据库进行提交 57 | return True 58 | except Exception as e: 59 | self.conn.rollback() 60 | self.logger.error("插入数据出现错误"+str(e)) 61 | finally: 62 | cursor.close() 63 | return False 64 | 65 | def execute_delete(self,sql): 66 | cursor = self.conn.cursor() 67 | try: 68 | cursor.execute(sql) 69 | if cursor.rowcount==1: 70 | self.logger.debug("%s删除成功"%sql) 71 | self.conn.commit() 72 | return True 73 | except Exception as e: 74 | self.conn.rollback() 75 | self.logger.error("删除数据出现错误"+str(e)) 76 | finally: 77 | cursor.close() 78 | return False 79 | 80 | def execute_update(self,sql): 81 | cursor = self.conn.cursor() 82 | try: 83 | cursor.execute(sql) 84 | if cursor.rowcount==1: 85 | self.logger.debug("%s更新失败" % sql ) 86 | self.conn.commit() 87 | return True 88 | except Exception as e: 89 | self.conn.rollback() 90 | self.logger.error("更新数据出现错误"+str(e)) 91 | finally: 92 | cursor.close() 93 | return False 94 | 95 | def execute_conn_close(self): 96 | try: 97 | self.conn.close() 98 | except Exception as e: 99 | self.logger.error("执行关闭数据库链接出现错误"+str(e)) 100 | -------------------------------------------------------------------------------- /framework/utils/databaseutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/utils/databaseutils/__init__.py -------------------------------------------------------------------------------- /framework/utils/emailutils/SendEmail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:UTF-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘jayzhen‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @time: 2017/3/29 13:12 10 | """ 11 | 12 | import smtplib 13 | from email.mime.text import MIMEText 14 | from email.header import Header 15 | from email.mime.multipart import MIMEMultipart 16 | from framework.utils.fileutils.FileCheckAndGetPath import FileChecKController 17 | from framework.utils.fileutils.ConfigCommonUtil import ConfigController 18 | from framework.utils.reporterutils.LoggingUtil import LoggingController 19 | 20 | 21 | class EmailController(object): 22 | 23 | def __init__(self): 24 | self.fc = FileChecKController() 25 | bools = self.fc.is_has_file("email.ini") 26 | if bools: 27 | fp = self.fc.get_fileabspath() 28 | conf = ConfigController(fp) 29 | self.smtp_host =conf.get("emails", "smtp_host") 30 | self.pop3_host =conf.get("emails", "pop3_host") 31 | self.receiver = conf.get("emails", "receiver").split(",") 32 | self.receiver_pa =conf.get("emails", "receiver_pa") 33 | self.sender =conf.get("emails", "sender") 34 | self.sender_pa =conf.get("emails", "sender_pa") 35 | self.log4py = LoggingController() 36 | 37 | def send_email_is_html(self): 38 | latestfpath,fname,currentfolder = self.fc.get_LatestFile() 39 | msgRoot = MIMEMultipart('related') 40 | ff = open(latestfpath, 'rb') 41 | message = MIMEText(ff.read(), 'html', 'utf-8') 42 | ff.close() 43 | message['From'] = self.sender 44 | #message['To'] = self.receiver 45 | subject = '实验室数字化平台-自动化测试报告' 46 | message['Subject'] = Header(subject, 'utf-8') 47 | msgRoot.attach(message) 48 | try: 49 | smtpObj = smtplib.SMTP() 50 | smtpObj.connect(self.smtp_host) 51 | smtpObj.login(self.sender, self.sender_pa) 52 | smtpObj.sendmail(self.sender, self.receiver, msgRoot.as_string()) 53 | self.log4py.debug("SendEmail_withFile邮件发送成功") 54 | smtpObj.close() 55 | except Exception as e: 56 | self.log4py.error("Error: 无法发送邮件::"+str(e)) 57 | 58 | def send_email_with_file(self): 59 | #创建一个带附件的实例 related alternative 60 | message = MIMEMultipart("related") 61 | #message['from'] = Header("QA jayzhen <%s>" %self.sender, 'utf-8') 62 | message['from'] = self.sender 63 | #message['To'] = Header("Leader <%s>" %self.receiver, 'utf-8') 64 | #message['To'] = self.receiver #群发邮件不能使用 65 | subject = '实验室数字化平台-自动化测试报告' 66 | message['Subject'] = Header(subject, 'utf-8') 67 | #邮件正文内容 68 | message.attach(MIMEText('

基于Spring MVC的实验室数字化平台-自动化测试 V1.0版本 -自动化测试报告


附件报告,请下载!(邮件为自动发送勿回)

', 'html', 'utf-8')) 69 | 70 | latestfpath,fname,currentfolder= self.fc.get_LatestFile() 71 | # 构造附件1,传送当前目录下的 test.txt 文件 72 | with open(latestfpath, 'rb') as f: 73 | att1 = MIMEText(f.read(), 'base64', 'utf-8') 74 | att1["Content-Type"] = 'application/octet-stream' 75 | # 这里的filename可以任意写,写什么名字,邮件中显示什么名字 76 | att1["Content-Disposition"] = 'attachment; filename=%s'%fname 77 | message.attach(att1) 78 | try: 79 | smtpObj = smtplib.SMTP() 80 | smtpObj.connect(self.smtp_host) 81 | smtpObj.login(self.sender, self.sender_pa) 82 | smtpObj.sendmail(self.sender, self.receiver, message.as_string()) 83 | self.log4py.debug("SendEmail_withFile邮件发送成功") 84 | smtpObj.close() 85 | except Exception as e: 86 | self.log4py.error("Error: 无法发送邮件::"+str(e)) 87 | 88 | -------------------------------------------------------------------------------- /framework/utils/emailutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/utils/emailutils/__init__.py -------------------------------------------------------------------------------- /framework/utils/fileutils/ConfigCommonUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘jayzhen‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @time: 2017/3/29 13:12 10 | 11 | 1.对ini配置文件进行读取操作 12 | """ 13 | import sys 14 | from configparser import ConfigParser 15 | 16 | 17 | class ConfigController(object): 18 | 19 | def __init__(self, path): 20 | self.path = path 21 | self.cf = ConfigParser.ConfigParser() 22 | self.cf.read(self.path) 23 | 24 | def get(self, field, key): 25 | result = "" 26 | try: 27 | result = self.cf.get(field, key) 28 | except Exception as e: 29 | result = "" 30 | return result 31 | 32 | def set(self, filed, key, value): 33 | try: 34 | self.cf.set(filed, key, value) 35 | self.cf.write(open(self.path, 'w')) 36 | except Exception as e: 37 | return False 38 | return True 39 | 40 | ''' 41 | 备用的 42 | ''' 43 | def read_config(self,config_file_path, field, key): 44 | cf = ConfigParser.ConfigParser() 45 | try: 46 | cf.read(config_file_path) 47 | result = cf.get(field, key) 48 | except: 49 | sys.exit(1) 50 | return result 51 | 52 | def write_config(self,config_file_path, field, key, value): 53 | cf = ConfigParser.ConfigParser() 54 | try: 55 | cf.read(config_file_path) 56 | cf.add_section(field) 57 | cf.set(field, key, value) 58 | cf.write(open(config_file_path,'w')) 59 | except: 60 | sys.exit(1) 61 | return True 62 | 63 | 64 | -------------------------------------------------------------------------------- /framework/utils/fileutils/CreateConfigUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘jayzhen‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | 1.该类主要处理appium多机并发测试场景下生成的端口与机器的设备号映射。 10 | a.接受初始化service中接受到device_list和service_list 11 | b.先判断是否存在固定的config文件,如果没有就创建;有,就覆盖模式进行写入 12 | c.循环遍历这个list并写入文件中 13 | """ 14 | import os 15 | from configparser import ConfigParser 16 | from framework.utils.reporterutils.LoggingUtil import LoggingController 17 | 18 | 19 | class CreateConfigFile(object): 20 | 21 | def __init__(self): 22 | self.cfg = ConfigParser.ConfigParser() 23 | self.log4py = LoggingController() 24 | self.path = (os.getcwd()).split('src')[0] + "\\testconfig" 25 | 26 | def set_appium_uuids_ports(self, device_list, port_list): 27 | """ 28 | 遍历list,按照下表进行对应映射 29 | :param device_lsit: 手机uuid 30 | :param port_list: pc启动的appium服务端口 31 | """ 32 | f_path = self.create_config_file(self.path) 33 | 34 | if len(device_list) > 0 and len(port_list) > 0: 35 | self.cfg.read(f_path) 36 | for i in range(len(device_list)): 37 | filed = device_list[i] 38 | key = filed 39 | value = port_list[i] 40 | # 因为是覆盖写入,没有section,需要先添加再设置, 初始化的服务都加一个run的标识 41 | self.cfg.add_section(filed) 42 | self.cfg.set(filed, key, value) 43 | self.cfg.set(filed, "run", "0") 44 | self.cfg.write(open(f_path, 'wb')) 45 | self.log4py.debug("设备sno与appium服务端口映射已写入appiumService.ini配置文件:{}--{}".format(key, value)) 46 | 47 | def set_appium_uuid_port(self, device, port, bp): 48 | """ 49 | 如果这样一个一个的写入到配置文件中,是追加还是覆盖?如果是覆盖的,服务启动完成后就剩一个配置,所以不行 50 | 如果是追加,需要判断配置文件中是否已经有了相同的section,有就更新,没有就添加 51 | :param device: 手机uuid 52 | :param port pc启动的appium服务端口 53 | """ 54 | f_path = os.path.join(self.path, 'appiumService.ini') 55 | if not os.path.exists(f_path): 56 | os.makedirs(f_path) 57 | if device is not None and port is not None: 58 | self.cfg.read(f_path) 59 | sec = device 60 | key = sec 61 | value = port 62 | if sec in self.cfg.sections(): 63 | self.cfg.set(sec, key, value) 64 | self.cfg.set(sec, "bp", bp) 65 | self.cfg.set(sec, "run", "0") 66 | else: 67 | self.cfg.add_section(sec) 68 | self.cfg.set(sec, key, value) 69 | self.cfg.set(sec, "bp", bp) 70 | self.cfg.set(sec, "run", "0") 71 | self.cfg.write(open(f_path, 'wb')) 72 | self.log4py.debug("设备sno与appium服务端口映射已写入appiumService.ini配置文件:{}--{}".format(key, value)) 73 | 74 | def create_config_file(self, path): 75 | """ 76 | 如果path这个文件不存在,就创建这个文件;存在就清空文件 77 | :param path: 78 | :return: 79 | """ 80 | if not os.path.exists(path): 81 | os.makedirs(path) 82 | f_path = os.path.join(path, 'appiumService.ini') 83 | f = open(f_path, "wb") 84 | f.close() 85 | return f_path 86 | 87 | def get_all_appium_server_port(self): 88 | f_path = os.path.join(self.path, 'appiumService.ini') 89 | port_list = [] 90 | if os.path.exists(f_path): 91 | self.cfg.read(f_path) 92 | section_list = self.cfg.sections() 93 | for sl in section_list: 94 | port_list.append(self.cfg.get(sl, sl)) 95 | port_list.append(self.cfg.get(sl, "bp")) 96 | return port_list 97 | 98 | -------------------------------------------------------------------------------- /framework/utils/fileutils/FileCheckAndGetPath.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:UTF-8 -*- 3 | 4 | """ 5 | @version: python2.7 6 | @author: ‘jayzhen‘ 7 | @contact: jayzhen_testing@163.com 8 | @site: https://github.com/gitjayzhen 9 | @software: PyCharm Community Edition 10 | @time: 2017/3/29 13:12 11 | 12 | 1.通过filecheck来查看项目目录下是否有指定文件 13 | 2.确定有指定文件后,可以获取文件的绝对路径(一定要保证文件名是正确的) 14 | """ 15 | 16 | import datetime 17 | import filecmp 18 | import os 19 | import time 20 | from framework.utils.reporterutils.LoggingUtil import LoggingController 21 | 22 | 23 | class FileChecKController(): 24 | 25 | def __init__(self): 26 | self.__fileabspath = None #不可访问的 27 | self.__logger = LoggingController() 28 | ''' 29 | 是否存在指定的文件,路径默认为当前项目的目录 30 | ''' 31 | def is_has_file(self, filename): 32 | propath = self.get_project_path() 33 | boolean = self.is_path_has_file(propath, filename) 34 | return boolean 35 | 36 | ''' 37 | 指定目录下是否存在指定的文件 38 | ''' 39 | def is_path_has_file(self, path, filename): 40 | boolean = self.check_has_file(path, filename) 41 | return boolean 42 | 43 | ''' 44 | 扫描指定目录下的所有文件,找到所要找的文件,return True or False 45 | ''' 46 | def check_has_file(self, path, filename): 47 | try: 48 | for filep, dirs, filelist in os.walk(path): 49 | for fl in filelist: 50 | if filecmp.cmp(fl, filename) == 0: #这个字符串的比较存在风险,python3不支持,待修改 51 | self.__fileabspath = os.path.join(filep, fl) 52 | self.__logger.info("查找的%s文件存在" %filename) 53 | return True 54 | return False 55 | except Exception as e: 56 | self.__logger.error("check_has_file()方法出现异常"+ str(e)) 57 | 58 | ''' 59 | 获取文件的绝对路径之倩需要check文件是否存在 60 | ''' 61 | def get_fileabspath(self): 62 | return self.__fileabspath 63 | 64 | ''' 65 | 截取当前项目所有在的路径 66 | ''' 67 | def get_project_path(self): 68 | abspath = os.getcwd() 69 | project_path = abspath.split("src")[0] #当前项目的目录 70 | return project_path 71 | 72 | ''' 73 | 1.在指定文件下,获取所有文件 74 | 2.再获取每个文件的时间,对比后获取文件名(使用内置函数) 75 | ''' 76 | def get_LatestFile(self): 77 | pro_path = self.get_project_path() 78 | rpath = "TestResult\Reports" 79 | result_dir = os.path.join(pro_path,rpath) 80 | l = os.listdir(result_dir) #该目录下的文件list 81 | #对key进行升序排列(变量fn是每个文件或者文件夹的全称,如果fn是不是文件夹或者是0,那就获取该文件的创建时间,排序后的最后一个文件就是最新的文件了) 82 | st = l.sort(key=lambda fn: os.path.getmtime(result_dir+"\\"+fn) if not os.path.isdir(result_dir+"\\"+fn) else 0) #第二句 83 | d = datetime.datetime.fromtimestamp(os.path.getmtime(result_dir+"\\"+l[-1])) 84 | fname = l[-1] 85 | fpath = os.path.join(result_dir, fname) 86 | self.__logger.debug('last file is ::'+fpath) 87 | time_end = time.mktime(d.timetuple()) 88 | self.__logger.debug('time_end:%s'%time_end) 89 | return fpath, fname, result_dir #fpath:html文件的全目录,fname:最新html文件名,result_dir:html文件当前所处文件夹路径 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /framework/utils/fileutils/JsonUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘jayzhen‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @time: 2017/3/29 13:12 10 | """ 11 | import json 12 | from framework.utils.fileutils.FileCheckAndGetPath import FileChecKController 13 | from framework.utils.reporterutils.LoggingUtil import LoggingController 14 | 15 | 16 | class JsonParser(object): 17 | def __init__(self): 18 | self.json_obj = None 19 | self.fc = FileChecKController() 20 | if self.fc.is_has_file("android_devices_info.json"): 21 | self.json_file_path = self.fc.get_fileabspath() 22 | self.log4py = LoggingController() 23 | 24 | def load_json(self,json_file_path): 25 | fin = open(json_file_path,"r") 26 | try: 27 | json_obj = json.load(fin) 28 | self.log4py.info("加载了%s文件"%json_file_path) 29 | except ValueError as e: 30 | json_obj = {} 31 | fin.close() 32 | return json_obj 33 | 34 | def get_value_with_key(self, json_key): 35 | pass 36 | 37 | def put_key_value(self,dict_data): 38 | try: 39 | json_obj = self.load_json(self.json_file_path) 40 | n = 0 41 | for k in dict_data: 42 | if not json_obj.has_key(k): 43 | json_obj[k] = dict_data[k] 44 | n += 1 45 | if n == 0 : 46 | print("该设备的数据已存在") 47 | return None 48 | self.log4py.info(dict_data) 49 | with open(self.json_file_path,'w+') as json_f_obj: 50 | json_f_obj.write(json.dumps(json_obj,sort_keys=True,indent =4,separators=(',', ': '),encoding="gbk",ensure_ascii=True)) 51 | except Exception as e: 52 | self.log4py.error("JsomParser func happend error") 53 | else: 54 | self.log4py.info("device info collect work has done, go to check json file") 55 | 56 | -------------------------------------------------------------------------------- /framework/utils/fileutils/XMLCheckUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | """ 11 | from xml.etree import ElementTree as ET 12 | import os 13 | 14 | 15 | PATH = lambda a: os.path.abspath(a) 16 | 17 | 18 | class XMLNodeChecker(object): 19 | 20 | def __element(self, attrib, name): 21 | """ 22 | 同属性单个元素,返回单个坐标元组,(x, y) 23 | :args: 24 | - attrib - node节点中某个属性 25 | - name - node节点中某个属性对应的值 26 | """ 27 | Xpoint = None 28 | Ypoint = None 29 | 30 | if not self.android.get_ui_dump_xml(self.xml_file_path): 31 | return None 32 | tree = ET.ElementTree(file=PATH("%s/uidump.xml" % self.xml_file_path)) 33 | treeIter = tree.iter(tag="node") 34 | for elem in treeIter: 35 | if elem.attrib[attrib] == name: 36 | #获取元素所占区域坐标[x, y][x, y] 37 | bounds = elem.attrib["bounds"] 38 | 39 | #通过正则获取坐标列表 40 | coord = self.pattern.findall(bounds) 41 | 42 | #求取元素区域中心点坐标 43 | Xpoint = (int(coord[2]) - int(coord[0])) / 2.0 + int(coord[0]) 44 | Ypoint = (int(coord[3]) - int(coord[1])) / 2.0 + int(coord[1]) 45 | break 46 | 47 | if Xpoint is None or Ypoint is None: 48 | raise Exception("Not found this element(%s) in current activity"%name) 49 | 50 | return (Xpoint, Ypoint) 51 | 52 | def __elements(self, attrib, name): 53 | """ 54 | 同属性多个元素,返回坐标元组列表,[(x1, y1), (x2, y2)] 55 | """ 56 | pointList = [] 57 | self.android.get_ui_dump_xml(self.xml_file_path) 58 | tree = ET.ElementTree(file=PATH("%s/uidump.xml" % self.xml_file_path)) 59 | treeIter = tree.iter(tag="node") 60 | for elem in treeIter: 61 | if elem.attrib[attrib] == name: 62 | bounds = elem.attrib["bounds"] 63 | coord = self.pattern.findall(bounds) 64 | Xpoint = (int(coord[2]) - int(coord[0])) / 2.0 + int(coord[0]) 65 | Ypoint = (int(coord[3]) - int(coord[1])) / 2.0 + int(coord[1]) 66 | 67 | #将匹配的元素区域的中心点添加进pointList中 68 | pointList.append((Xpoint, Ypoint)) 69 | 70 | return pointList 71 | 72 | def __bound(self, attrib, name): 73 | """ 74 | 同属性单个元素,返回单个坐标区域元组,(x1, y1, x2, y2) 75 | """ 76 | coord = [] 77 | 78 | self.android.get_ui_dump_xml(self.xml_file_path) 79 | tree = ET.ElementTree(file=PATH("%s/uidump.xml" % self.xml_file_path)) 80 | treeIter = tree.iter(tag="node") 81 | for elem in treeIter: 82 | if elem.attrib[attrib] == name: 83 | bounds = elem.attrib["bounds"] 84 | coord = self.pattern.findall(bounds) 85 | 86 | if not coord: 87 | raise Exception("Not found this element(%s) in current activity"%name) 88 | 89 | return (int(coord[0]), int(coord[1]), int(coord[2]), int(coord[3])) 90 | 91 | def __bounds(self, attrib, name): 92 | """ 93 | 同属性多个元素,返回坐标区域列表,[(x1, y1, x2, y2), (x3, y3, x4, y4)] 94 | """ 95 | pointList = [] 96 | self.android.get_ui_dump_xml(self.xml_file_path) 97 | tree = ET.ElementTree(file=PATH("%s/uidump.xml" % self.xml_file_path)) 98 | treeIter = tree.iter(tag="node") 99 | for elem in treeIter: 100 | if elem.attrib[attrib] == name: 101 | bounds = elem.attrib["bounds"] 102 | coord = self.pattern.findall(bounds) 103 | pointList.append((int(coord[0]), int(coord[1]), int(coord[2]), int(coord[3]))) 104 | 105 | return pointList 106 | 107 | def __checked(self, attrib, name): 108 | """ 109 | 返回布尔值列表 110 | """ 111 | boolList = [] 112 | self.android.get_ui_dump_xml(self.xml_file_path) 113 | tree = ET.ElementTree(file=PATH("%s/uidump.xml" % self.xml_file_path)) 114 | treeIter = tree.iter(tag="node") 115 | for elem in treeIter: 116 | if elem.attrib[attrib] == name: 117 | checked = elem.attrib["checked"] 118 | if checked == "true": 119 | boolList.append(True) 120 | else: 121 | boolList.append(False) 122 | 123 | return boolList 124 | 125 | def find_elements_By_Name(self, name): 126 | """ 127 | 通过元素名称定位多个相同text的元素 128 | """ 129 | return self.__elements("text", name) 130 | 131 | def find_element_By_Class(self, className): 132 | """ 133 | 通过元素类名定位单个元素 134 | usage: findElementByClass("android.widget.TextView") 135 | """ 136 | return self.__element("class", className) 137 | 138 | def find_elements_By_Class(self, className): 139 | """ 140 | 通过元素类名定位多个相同class的元素 141 | """ 142 | return self.__elements("class", className) 143 | 144 | def find_element_By_Id(self, id): 145 | """ 146 | 通过元素的resource-id定位单个元素 147 | usage: findElementsById("com.android.deskclock:id/imageview") 148 | """ 149 | return self.__element("resource-id", id) 150 | 151 | def find_elements_By_Id(self, id): 152 | """ 153 | 通过元素的resource-id定位多个相同id的元素 154 | """ 155 | return self.__elements("resource-id", id) 156 | 157 | def find_element_By_Content_Desc(self, contentDesc): 158 | """ 159 | 通过元素的content-desc定位单个元素 160 | """ 161 | return self.__element("content-desc", contentDesc) 162 | 163 | def find_elements_By_Content_Desc(self, contentDesc): 164 | """ 165 | 通过元素的content-desc定位多个相同的元素 166 | """ 167 | return self.__elements("content-desc", contentDesc) 168 | 169 | def get_element_bound_By_Name(self, name): 170 | """ 171 | 通过元素名称获取单个元素的区域 172 | """ 173 | return self.__bound("text", name) 174 | 175 | def get_element_bounds_By_Name(self, name): 176 | """ 177 | 通过元素名称获取多个相同text元素的区域 178 | """ 179 | return self.__bounds("text", name) 180 | 181 | def get_element_bound_By_Class(self, className): 182 | """ 183 | 通过元素类名获取单个元素的区域 184 | """ 185 | return self.__bound("class", className) 186 | 187 | def get_element_bounds_By_Class(self, className): 188 | """ 189 | 通过元素类名获取多个相同class元素的区域 190 | """ 191 | return self.__bounds("class", className) 192 | 193 | def get_element_bound_By_Content_Desc(self, contentDesc): 194 | """ 195 | 通过元素content-desc获取单个元素的区域 196 | """ 197 | return self.__bound("content-desc", contentDesc) 198 | 199 | def get_element_bounds_By_Content_Desc(self, contentDesc): 200 | """ 201 | 通过元素content-desc获取多个相同元素的区域 202 | """ 203 | return self.__bounds("content-desc", contentDesc) 204 | 205 | def get_element_bound_By_Id(self, id): 206 | """ 207 | 通过元素id获取单个元素的区域 208 | """ 209 | return self.__bound("resource-id", id) 210 | 211 | def get_element_bounds_By_Id(self, id): 212 | """ 213 | 通过元素id获取多个相同resource-id元素的区域 214 | """ 215 | return self.__bounds("resource-id", id) 216 | 217 | def is_elements_checked_By_Name(self, name): 218 | """ 219 | 通过元素名称判断checked的布尔值,返回布尔值列表 220 | """ 221 | return self.__checked("text", name) 222 | 223 | def is_elements_checked_By_Id(self, id): 224 | """ 225 | 通过元素id判断checked的布尔值,返回布尔值列表 226 | """ 227 | return self.__checked("resource-id", id) 228 | 229 | def is_elements_checked_By_Class(self, className): 230 | """ 231 | 通过元素类名判断checked的布尔值,返回布尔值列表 232 | """ 233 | return self.__checked("class", className) -------------------------------------------------------------------------------- /framework/utils/fileutils/ZipUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘jayzhen‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @time: 2017/3/29 13:12 10 | """ 11 | import os 12 | import ZipUtil 13 | 14 | # 解压zip文件 15 | def unzip(): 16 | source_zip = "c:\\update\\SW_Servers_20120815.zip" 17 | target_dir = "c:\\update\\" 18 | myzip = ZipUtil(source_zip) 19 | myfilelist=myzip.namelist() 20 | for name in myfilelist: 21 | f_handle=open(target_dir+name,"wb") 22 | f_handle.write(myzip.read(name)) 23 | f_handle.close() 24 | myzip.close() 25 | 26 | #添加文件到已有的zip包中 27 | def addzip(currentfolder,ready2compression): 28 | zipfname = "AutoTesting-Reports.zip" 29 | absZIPpath = os.path.join(currentfolder,zipfname) 30 | absfpath = os.path.join(currentfolder,ready2compression) 31 | f = ZipUtil.ZipFile(absZIPpath, 'w', ZipUtil.ZIP_DEFLATED) 32 | f.write(absfpath) 33 | f.close() 34 | 35 | return absZIPpath,zipfname 36 | 37 | #把整个文件夹内的文件打包 38 | def adddirfile(): 39 | f = ZipUtil.ZipFile('archive.zip', 'w', ZipUtil.ZIP_DEFLATED) 40 | startdir = "c:\\mydirectory" 41 | for dirpath, dirnames, filenames in os.walk(startdir): 42 | for filename in filenames: 43 | f.write(os.path.join(dirpath,filename)) 44 | f.close() 45 | #latestfpath,fname,currentfolder= FileChecK().get_LatestFile() 46 | #absZIPpath,zipfname = addzip(currentfolder,fname) -------------------------------------------------------------------------------- /framework/utils/fileutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/utils/fileutils/__init__.py -------------------------------------------------------------------------------- /framework/utils/formatutils/DateTimeUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:UTF-8 -*- 3 | 4 | """ 5 | @version: python2.7 6 | @author: ‘jayzhen‘ 7 | @contact: jayzhen_testing@163.com 8 | @site: https://github.com/gitjayzhen 9 | @software: PyCharm Community Edition 10 | @time: 2017/3/29 13:12 11 | """ 12 | import time 13 | import datetime 14 | import calendar 15 | 16 | 17 | class DateTimeManager(object): 18 | 19 | ''' 20 | * 获取系统当前日期和时间并格式化为yyyyMMddHHmmss即类似20110810155638格式 21 | * @param 无 22 | * @return 系统当前日期和时间并格式化为yyyyMMddHHmmss即类似20110810155638格式 23 | ''' 24 | def getCurrentDateTime(self): 25 | return datetime.datetime.now().strftime("%Y%m%d%H%M%S") 26 | 27 | ''' 28 | * 获取系统当前日期和时间并格式化为yyyyMMddHHmmssSSS即类似20130526002728796格式 29 | * @param 无 30 | * @return 系统当前日期和时间并格式化为yyyyMMddHHmmssSSS即类似20130526002728796格式 31 | ''' 32 | def getDateTime(self): 33 | return datetime.datetime.now() 34 | 35 | ''' 36 | * 获取系统当前日期并格式化为yyyyMMdd即类似20110810格式 37 | * @param 无 38 | * @return 系统当前日期并格式化为yyyyMMdd即类似20110810格式 39 | ''' 40 | def getCurrentDate(self): 41 | return datetime.datetime.now().strftime("%Y%m%d") 42 | 43 | ''' 44 | * 获取系统当前时间并格式化为HHmmss即类似155638格式 45 | * @param 无 46 | * @return 系统当前时间并格式化为HHmmss即类似155638格式 47 | ''' 48 | def getCurrentTime(self): 49 | return datetime.datetime.now().strftime("%H%M%S") 50 | 51 | ''' 52 | * 获取系统当前时间并格式化为HHmmssSSS即类似155039527格式 53 | * @param 无 54 | * @return 系统当前时间并格式化为HHmmssSSS即类似155039527格式 55 | ''' 56 | def getTime(self): 57 | return datetime.datetime.now().strftime("%H%M%S%f") 58 | 59 | ''' 60 | * 根据自定义格式化获取系统当前时间 61 | * @param format:时间格式化如yyyy-MM-dd HH:mm:ss:SSS "%Y%m%d%H%M%S%f" 62 | * @return 根据自定义格式化返回系统当前时间 63 | ''' 64 | def formated_time(self, format_time): 65 | return datetime.datetime.now().strftime(format_time) 66 | ''' 67 | * get specified time string in specified date format. 68 | * @param days 69 | * days after or before current date, use + and - to add. 70 | * @param dateFormat 71 | * the formatter of date, such as:yyyy-MM-dd HH:mm:ss:SSS. 72 | ''' 73 | def addDaysByFormatter(self,adddays,dateFormat): 74 | afteraddtime = datetime.datetime.now() + datetime.timedelta(days=adddays) 75 | return time.strftime(afteraddtime,dateFormat) 76 | 77 | ''' 78 | * get specified time string in specified date format. 79 | * @param months: months after or before current date, use + and - to add. 80 | * @param dateFormat:the formatter of date, such as:yyyy-MM-dd HH:mm:ss:SSS. 81 | ''' 82 | def addMonthsByFormatter(self, months,dateFormat): 83 | d = datetime.datetime.now() 84 | c = calendar.Calendar() 85 | year = d.year 86 | month = d.month 87 | today = d.day 88 | if month+months > 12 : 89 | month = months 90 | year += 1 91 | else: 92 | month += months 93 | days = calendar.monthrange(year, month)[1] 94 | 95 | if today > days: 96 | afteraddday = days 97 | else: 98 | afteraddday = today 99 | return datetime.datetime(year,month,afteraddday).strftime(dateFormat) 100 | ''' 101 | * get specified time string in specified date format. 102 | * @param years:years after or before current date, use + and - to add. 103 | * @param dateFormat:the formatter of date, such as:yyyy-MM-dd HH:mm:ss:SSS. 104 | ''' 105 | def addYearsByFormatter(self, years,dateFormat): 106 | d = datetime.datetime.now() 107 | c = calendar.Calendar() 108 | year = d.year + years 109 | month = d.month 110 | today = d.day 111 | 112 | days = calendar.monthrange(year, month)[1] 113 | 114 | if today > days: 115 | afterday = days 116 | else: 117 | afterday = today 118 | return datetime.datetime(year,month,afterday).strftime(dateFormat) 119 | 120 | ''' 121 | * get first day of next month in specified date format. 122 | * @param dateFormat: the formatter of date, such as:yyyy-MM-dd HH:mm:ss:SSS. 123 | ''' 124 | def firstDayOfNextMonth(self, dateFormat): 125 | d = datetime.datetime.now() 126 | year = d.year 127 | month = d.month 128 | if month+1 > 12 : 129 | month = 1 130 | year += 1 131 | else : 132 | month += 1 133 | 134 | return datetime.datetime(year,month,1).strftime(dateFormat) 135 | 136 | ''' 137 | * get first day of specified month and specified year in specified date 138 | * format. 139 | * @param year: the year of the date. 140 | * @param month:the month of the date. 141 | * @param dateFormat:the formatter of date, such as:yyyy-MM-dd HH:mm:ss:SSS. 142 | ''' 143 | def firstDayOfMonth(self, year,month, dateFormat): 144 | return datetime.datetime(year,month,1).strftime(dateFormat) 145 | 146 | ''' 147 | get first day of specified month of current year in specified dateformat. 148 | @param month:the month of the date. 149 | @param dateFormat:the formatter of date, such as:yyyy-MM-dd HH:mm:ss:SSS. 150 | ''' 151 | def firstDayOfMonthThisYear(self,month,dateFormat): 152 | d = datetime.datetime.now() 153 | year = d.year 154 | return datetime.datetime(year,month,1).strftime(dateFormat) 155 | 156 | ''' 157 | get the system current milliseconds. 158 | ''' 159 | def getMilSecNow(self): 160 | return time.time() 161 | 162 | 163 | -------------------------------------------------------------------------------- /framework/utils/formatutils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘dell‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @file: __init__.py.py 10 | @time: 2017/3/30 19:49 11 | """ 12 | -------------------------------------------------------------------------------- /framework/utils/reporterutils/HtmlReportUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:UTF-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘jayzhen‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @time: 2017/3/29 13:12 10 | """ 11 | import os 12 | from framework.utils.reporterutils.LoggingUtil import LoggingController 13 | from framework.utils.fileutils.ConfigCommonUtil import ConfigController 14 | from framework.utils.fileutils.FileCheckAndGetPath import FileChecKController 15 | from framework.utils.formatutils.DateTimeUtil import DateTimeManager 16 | 17 | ''' 18 | 创建一个html文件,并返回文件的对象 19 | ''' 20 | def html_reporter(): 21 | logger = LoggingController() 22 | fc = FileChecKController() 23 | pro_path = fc.getProjectPath() 24 | boolean = fc.is_has_file("framework.ini") 25 | if boolean: 26 | inipath = fc.get_fileabspath() 27 | fw_conf = ConfigController(inipath) 28 | htmlrp_path = fw_conf.get("htmlreportPath", "htmlreportPath") 29 | htmreportl_abs_path = os.path.join(pro_path,htmlrp_path) 30 | timecurrent = DateTimeManager().formatedTime("%Y-%m-%d-%H-%M-%S") 31 | logger.debug("=====创建了一个html文件报告,路径是::"+htmreportl_abs_path) 32 | file_path = str(htmreportl_abs_path)+timecurrent+"-LDP-TestingRreporter.html" 33 | try: 34 | if os.path.exists(file_path): 35 | html_obj = open(file_path, "a") #打开文件 追加 36 | return html_obj 37 | else: 38 | html_obj = open(file_path, "wb+") 39 | return html_obj 40 | except Exception as e: 41 | logger.error("创建html_reporter出现错误"+str(e)) 42 | 43 | 44 | -------------------------------------------------------------------------------- /framework/utils/reporterutils/ImageUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘jayzhen‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @time: 2017/3/29 13:12 10 | """ 11 | 12 | #图片处理,需要PIL库 13 | 14 | import tempfile 15 | import os 16 | import shutil 17 | from functools import reduce 18 | 19 | from PIL import Image 20 | from framework.core.adb.AdbCommand import AdbCmder 21 | 22 | PATH = lambda p: os.path.abspath(p) 23 | 24 | class ImageController(object): 25 | 26 | def __init__(self, device_id=""): 27 | """ 28 | 初始化,获取系统临时文件存放目录 29 | """ 30 | self.utils = AdbCmder() 31 | self.tempFile = tempfile.gettempdir() 32 | 33 | def screenShot(self): 34 | """ 35 | 截取设备屏幕 36 | """ 37 | self.utils.shell("screencap -p /data/local/tmp/temp.png").wait() 38 | self.utils.adb("pull /data/local/tmp/iuniTemp.png %s" %self.tempFile).wait() 39 | 40 | return self 41 | 42 | def writeToFile(self, dirPath, imageName, form = "png"): 43 | """ 44 | 将截屏文件写到本地 45 | usage: screenShot().writeToFile("d:\\screen", "image") 46 | """ 47 | if not os.path.isdir(dirPath): 48 | os.makedirs(dirPath) 49 | shutil.copyfile(PATH("%s/temp.png" %self.tempFile), PATH("%s/%s.%s" %(dirPath, imageName, form))) 50 | self.utils.shell("rm /data/local/tmp/temp.png") 51 | 52 | def loadImage(self, imageName): 53 | """ 54 | 加载本地图片 55 | usage: lodImage("d:\\screen\\image.png") 56 | """ 57 | if os.path.isfile(imageName): 58 | load = Image.open(imageName) 59 | return load 60 | else: 61 | print("该设备的数据已存在") 62 | 63 | def subImage(self, box): 64 | """ 65 | 截取指定像素区域的图片 66 | usage: box = (100, 100, 600, 600) 67 | screenShot().subImage(box) 68 | """ 69 | image = Image.open(PATH("%s/temp.png" %self.tempFile)) 70 | newImage = image.crop(box) 71 | newImage.save(PATH("%s/temp.png" %self.tempFile)) 72 | 73 | return self 74 | 75 | #http://testerhome.com/topics/202 76 | def sameAs(self,loadImage): 77 | """ 78 | 比较两张截图的相似度,完全相似返回True 79 | usage: load = loadImage("d:\\screen\\image.png") 80 | screen().subImage(100, 100, 400, 400).sameAs(load) 81 | """ 82 | import math 83 | import operator 84 | 85 | image1 = Image.open(PATH("%s/temp.png" %self.tempFile)) 86 | image2 = loadImage 87 | 88 | 89 | histogram1 = image1.histogram() 90 | histogram2 = image2.histogram() 91 | 92 | differ = math.sqrt(reduce(operator.add, list(map(lambda a,b: (a-b)**2, \ 93 | histogram1, histogram2)))/len(histogram1)) 94 | if differ == 0: 95 | return True 96 | else: 97 | return False 98 | -------------------------------------------------------------------------------- /framework/utils/reporterutils/LogWithConfUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import os 6 | import inspect 7 | import logging 8 | import logging.config 9 | 10 | 11 | class LoggingController(object): 12 | 13 | def __init__(self): 14 | pro_root = os.getcwd().split("src")[0] 15 | f_path = os.path.join(pro_root, "testconfig\\logging.conf") 16 | logging.config.fileConfig(f_path) # 采用配置文件 17 | # create logger debug,info,warning,error 18 | self.D = logging.getLogger("debug") 19 | self.I = logging.getLogger("info") 20 | self.W = logging.getLogger("warning") 21 | self.E = logging.getLogger("error") 22 | 23 | def getLogMessage(self, message): 24 | frame, filename, lineNo, functionName, code, unknowField = inspect.stack()[2] 25 | '''日志格式:[时间] [类型] [记录代码] 信息''' 26 | return "[%s- %s -%s] %s" % (filename, lineNo, functionName, message) 27 | 28 | def debug(self, mag): 29 | mag = self.getLogMessage(mag) 30 | print(self.D.handlers) 31 | self.D.debug(mag) 32 | 33 | def info(self, mag): 34 | mag = self.getLogMessage(mag) 35 | print(self.I.handlers) 36 | self.I.info(mag) 37 | 38 | def warn(self, mag): 39 | mag = self.getLogMessage(mag) 40 | print(self.W.handlers) 41 | self.W.warning(mag) 42 | 43 | def error(self, mag): 44 | mag = self.getLogMessage(mag) 45 | print(self.E.handlers) 46 | self.E.error(mag) 47 | 48 | -------------------------------------------------------------------------------- /framework/utils/reporterutils/LoggingUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/usr python 2 | # -*- coding:utf8 -*- 3 | """ 4 | @version: python2.7 5 | @author: ‘jayzhen‘ 6 | @contact: jayzhen_testing@163.com 7 | @site: https://github.com/gitjayzhen 8 | @software: PyCharm Community Edition 9 | @time: 2017/3/29 13:12 10 | 该日志类可以把不同级别的日志输出到不同的日志文件中 11 | """ 12 | 13 | import os 14 | import datetime 15 | import logging 16 | import inspect 17 | 18 | abspath = os.getcwd() 19 | logfilepath = abspath.split("src")[0] + "testresult\\logs4script\\" 20 | if not os.path.exists(logfilepath): 21 | os.makedirs(logfilepath) 22 | 23 | # 将对应文件实例化成一个FileHandler对象,让不用级别的日志共用该Filehandler,这样做到日志打印到一个文件中 24 | hd = logging.FileHandler(os.path.abspath(os.path.join(logfilepath, "scripts.log"))) 25 | handlers = {logging.DEBUG: hd,logging.INFO: hd,logging.WARNING: hd, logging.ERROR: hd} 26 | 27 | 28 | class LoggingController(object): 29 | 30 | def __init__(self, level=logging.NOTSET): 31 | self.__loggers = {} 32 | log_levels = handlers.keys() 33 | for level in log_levels: 34 | logger = logging.getLogger(str(level)) 35 | logger.addHandler(handlers[level]) 36 | logger.setLevel(level) 37 | self.__loggers.update({level: logger}) 38 | 39 | def time_now_formate(self): 40 | return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f') 41 | 42 | def get_log_message(self, level, message): 43 | frame, filename, lineNo, functionName, code, unknowField = inspect.stack()[2] 44 | '''日志格式:[时间] [类型] [记录代码] 信息''' 45 | relative_path = filename.split("AppiumTestProject")[1] 46 | relative_path = relative_path.replace("/", ".") 47 | relative_path = relative_path.replace("\\", ".") 48 | relative_path = relative_path.replace(".", "", 1) 49 | return "%s %s %s %s - %s" % (self.time_now_formate(), level, relative_path, lineNo, message) 50 | 51 | def info(self, message): 52 | message = self.get_log_message("INFO", message) 53 | self.__loggers[logging.INFO].info(message) 54 | 55 | def error(self, message): 56 | message = self.get_log_message("ERROR", message) 57 | self.__loggers[logging.ERROR].warning(message) 58 | 59 | def warning(self, message): 60 | message = self.get_log_message("WARNING", message) 61 | self.__loggers[logging.WARNING].warning(message) 62 | 63 | def debug(self, message): 64 | message = self.get_log_message("DEBUG", message) 65 | self.__loggers[logging.DEBUG].debug(message) 66 | 67 | def critical(self, message): 68 | message = self.get_log_message("CRITICAL", message) 69 | self.__loggers[logging.CRITICAL].critical(message) 70 | 71 | -------------------------------------------------------------------------------- /framework/utils/reporterutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/framework/utils/reporterutils/__init__.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Appium-Python-Client==2.6.1 2 | pytest==7.1.2 -------------------------------------------------------------------------------- /test_case/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | @version: python2.7 6 | @author: ‘jayzhen‘ 7 | @license: Apache Licence 8 | @contact: 2431236868@qq.com 9 | @site: http://www.jayzhen.com 10 | @software: PyCharm 11 | @file: __init__.py.py 12 | @time: 2017/3/20 23:12 13 | """ -------------------------------------------------------------------------------- /test_case/test_atp_base_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding=utf8 -*- 3 | """ 4 | @version: v1.0 5 | @author: jayzhen 6 | @license: Apache Licence 7 | @contact: jayzhen_testing@163.com 8 | @site: http://blog.csdn.net/u013948858 9 | @software: PyCharm 10 | """ 11 | import unittest 12 | 13 | from appium.webdriver.common.mobileby import MobileBy 14 | 15 | from framework.core.appiumapi.AppiumBaseApi import AppiumDriver 16 | from framework.initdriver.InitAppiumDriver import InitDriverOption 17 | 18 | 19 | class TestAppiumBaseApi(unittest.TestCase): 20 | 21 | def setUp(self): 22 | self.driver = InitDriverOption().get_android_driver() 23 | self.appium_instances = AppiumDriver(self.driver) 24 | 25 | def tearDown(self): 26 | self.driver.quit() 27 | 28 | # @unittest.skip("skip 'test_is_displayed' func") 29 | def test_is_displayed(self): 30 | print(self.appium_instances.is_displayed(MobileBy.ID, "com.youku.phone:id/img_user")) 31 | 32 | @unittest.skip("skip 'test_find_element_by_want' func") 33 | def test_find_element_by_want(self): 34 | print(self.appium_instances.find_element_by_want(MobileBy.ID, "com.youku.phone:id/img_user", 5)) 35 | 36 | def test_get_current_activity(self): 37 | print(self.appium_instances.get_current_activity()) 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /test_result/other/android_devices_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "006f7d8760d5074b": { 3 | "dpi": "320", 4 | "image_resolution": "768x1280", 5 | "ip": "172.24.91.8", 6 | "os_version": "4.2.2", 7 | "phone_brand": "Android", 8 | "phone_model": "Full JellyBean on Mako", 9 | "ram": "2GB" 10 | }, 11 | "091504e9": { 12 | "dpi": "480", 13 | "image_resolution": "1080x1920", 14 | "ip": "30.96.97.216", 15 | "os_version": "4.4.4", 16 | "phone_brand": "Xiaomi", 17 | "phone_model": "MI 3", 18 | "ram": "2GB" 19 | }, 20 | "84534ad1": { 21 | "dpi": "480", 22 | "image_resolution": "1080x1920", 23 | "ip": "192.168.1.100", 24 | "os_version": "5.1.1", 25 | "phone_brand": "OPPO", 26 | "phone_model": "OPPO R9 Plusm A", 27 | "ram": "4GB" 28 | }, 29 | "90d1894b7d62": { 30 | "dpi": "320", 31 | "image_resolution": "720x1280", 32 | "ip": "192.168.1.101", 33 | "os_version": "5.1.1", 34 | "phone_brand": "Xiaomi", 35 | "phone_model": "wt86047", 36 | "ram": "2GB" 37 | }, 38 | "A3P4CE771775": { 39 | "dpi": "320", 40 | "image_resolution": "1536x2048", 41 | "ip": "30.96.93.160", 42 | "os_version": "5.1", 43 | "phone_brand": "Xiaomi", 44 | "phone_model": "MI PAD 2", 45 | "ram": "2GB" 46 | }, 47 | "APU0215C05001561": { 48 | "dpi": "480", 49 | "image_resolution": "1080x1920", 50 | "ip": "", 51 | "os_version": "6.0", 52 | "phone_brand": "HUAWEI", 53 | "phone_model": "NEXT", 54 | "ram": "3GB" 55 | }, 56 | "B2T7N16908000353": { 57 | "dpi": "480", 58 | "image_resolution": "1080x1920", 59 | "ip": "", 60 | "os_version": "7.0", 61 | "phone_brand": "Huawei", 62 | "phone_model": "generic_a15", 63 | "ram": "3GB" 64 | }, 65 | "BY3ETK1596002574": { 66 | "dpi": "320", 67 | "image_resolution": "720x1280", 68 | "ip": "172.24.91.9", 69 | "os_version": "4.4.2", 70 | "phone_brand": "hi6210sft", 71 | "phone_model": "Huawei Ascend", 72 | "ram": "1GB" 73 | }, 74 | "NX505J": { 75 | "dpi": "400", 76 | "image_resolution": "1080x1920", 77 | "ip": "172.28.112.3", 78 | "os_version": "4.4.2", 79 | "phone_brand": "nubia", 80 | "phone_model": "NX505J", 81 | "ram": "2GB" 82 | }, 83 | "TWP7LJ55OZTCTSVK": { 84 | "dpi": "480", 85 | "image_resolution": "1080x1920", 86 | "ip": "30.96.97.141", 87 | "os_version": "5.1", 88 | "phone_brand": "OPPO", 89 | "phone_model": "OPPO R9m", 90 | "ram": "4GB" 91 | }, 92 | "VSKVEMLZ79R4O78L": { 93 | "dpi": "480", 94 | "image_resolution": "1080x1920", 95 | "ip": "192.168.1.124", 96 | "os_version": "5.1", 97 | "phone_brand": "vivo", 98 | "phone_model": "PD1501D x6plus", 99 | "ram": "4GB" 100 | } 101 | } -------------------------------------------------------------------------------- /test_result/screenshots/find_element_by_want-20170417-223949366000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/1b143606a8c92eab32cbc4034a6031faa9954a20/test_result/screenshots/find_element_by_want-20170417-223949366000.png --------------------------------------------------------------------------------