├── framework
├── __init__.py
├── core
│ ├── __init__.py
│ ├── adb
│ │ ├── __init__.py
│ │ ├── EventKeys.py
│ │ └── AdbCommand.py
│ ├── dos
│ │ ├── __init__.py
│ │ └── DosCommand.py
│ ├── appiumapi
│ │ ├── __init__.py
│ │ └── AppiumBaseApi.py
│ └── exceptions
│ │ ├── __init__.py
│ │ └── Exception.py
├── utils
│ ├── __init__.py
│ ├── emailutils
│ │ ├── __init__.py
│ │ └── SendEmail.py
│ ├── fileutils
│ │ ├── __init__.py
│ │ ├── ZipUtil.py
│ │ ├── ConfigCommonUtil.py
│ │ ├── JsonUtil.py
│ │ ├── FileCheckAndGetPath.py
│ │ ├── CreateConfigUtil.py
│ │ └── XMLCheckUtil.py
│ ├── databaseutils
│ │ ├── __init__.py
│ │ ├── ExcelDataUtil.py
│ │ └── SQLController.py
│ ├── reporterutils
│ │ ├── __init__.py
│ │ ├── LogWithConfUtil.py
│ │ ├── HtmlReportUtil.py
│ │ ├── LoggingUtil.py
│ │ └── ImageUtil.py
│ ├── bat
│ │ ├── startAppium.bat
│ │ └── stopAppium.bat
│ └── formatutils
│ │ ├── __init__.py
│ │ └── DateTimeUtil.py
├── initdriver
│ ├── __init__.py
│ ├── InitConfig.py
│ └── InitAppiumDriver.py
├── base
│ ├── __init__.py
│ ├── PycFileCtrl.py
│ ├── GetAllPathCtrl.py
│ ├── AppFileCtrl.py
│ ├── PerformanceCtrl.py
│ ├── PackageCtrl.py
│ ├── DeviceInfoCtrl.py
│ └── DriverBaseCase.py
├── domain
│ ├── __init__.py
│ └── mobile_infos.py
└── initservice
│ ├── __init__.py
│ └── InitService.py
├── requirements.txt
├── test_result
├── screenshots
│ └── find_element_by_want-20170417-223949366000.png
└── other
│ └── android_devices_info.json
├── configs
├── framework.ini
├── email.ini
├── db.ini
├── appiumService.ini
├── allpath.ini
├── run.ini
├── logging.conf
└── permission.json
├── test_case
├── __init__.py
└── test_atp_base_api.py
├── .gitignore
├── README.md
└── LICENSE
/framework/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/core/adb/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/core/dos/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/initdriver/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/core/appiumapi/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/core/exceptions/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/utils/emailutils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/utils/fileutils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/utils/databaseutils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/framework/utils/reporterutils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Appium-Python-Client==2.6.1
2 | pytest==7.1.2
--------------------------------------------------------------------------------
/framework/utils/bat/startAppium.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | title startAppiumServer
3 | cmd /c "appium -a 127.0.0.1 -p 4723"
--------------------------------------------------------------------------------
/test_result/screenshots/find_element_by_want-20170417-223949366000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitjayzhen/AppiumTestProject/HEAD/test_result/screenshots/find_element_by_want-20170417-223949366000.png
--------------------------------------------------------------------------------
/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/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=
--------------------------------------------------------------------------------
/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/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"
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 | """
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------