├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── ABOUT.rst ├── CHANGELOG ├── DESIGN.md ├── HISTORY.md ├── LICENSE ├── README.md ├── README_ADVANCED.md ├── TODO.md ├── appveyor.yml ├── atx ├── __init__.py ├── __main__.py ├── adbkit │ ├── __init__.py │ ├── client.py │ ├── device.py │ ├── mixins.py │ └── openstf │ │ ├── __init__.py │ │ ├── keycode.py │ │ ├── service.py │ │ ├── stfwire.proto │ │ ├── stfwire_pb2.py │ │ ├── test_service.py │ │ └── tkinput.py ├── apkparse.py ├── base.py ├── cmds │ ├── __init__.py │ ├── doctor.py │ ├── info.py │ ├── install.py │ ├── iosdeveloper.py │ ├── minicap.py │ ├── monkey.py │ ├── record.py │ ├── run.py │ ├── screen.py │ ├── screencap.py │ ├── screenrecord.py │ ├── static │ │ ├── css │ │ │ ├── bootstrap-3.3.5.min.css │ │ │ ├── index.css │ │ │ └── jquery.fancybox.min.css │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── help.md │ │ ├── icons │ │ │ ├── back.ico │ │ │ ├── exit.ico │ │ │ ├── home.ico │ │ │ ├── menu.ico │ │ │ ├── power.ico │ │ │ ├── rotate.ico │ │ │ ├── rotate2.ico │ │ │ ├── save.ico │ │ │ ├── volume_down.ico │ │ │ └── volume_up.ico │ │ ├── index.html │ │ ├── js │ │ │ ├── bootstrap-3.3.5.min.js │ │ │ ├── index.js │ │ │ ├── jquery-1.11.3.min.js │ │ │ ├── jquery.fancybox.pack.js │ │ │ ├── jquery.mousewheel.min.js │ │ │ ├── notify.js │ │ │ ├── vue.js │ │ │ └── vue.min.js │ │ └── recorder.ico │ ├── tcpproxy.py │ ├── tkgui.py │ └── utils.py ├── comtools.py ├── consts.py ├── drivers │ ├── __init__.py │ ├── android.py │ ├── dummy.py │ ├── ios_uiautomation │ │ ├── __init__.py │ │ ├── bootstrap.sh │ │ ├── instruments-test.js │ │ ├── ios-models.yml │ │ └── mechanic.js │ ├── ios_webdriveragent.py │ ├── mixin.py │ ├── webdriver.py │ └── windows.py ├── errors.py ├── ext │ ├── __init__.py │ ├── chromedriver.py │ ├── gt.py │ └── report │ │ ├── README.md │ │ ├── __init__.py │ │ ├── index.tmpl.html │ │ ├── patch.py │ │ ├── report.png │ │ └── v2.html ├── imutils.py ├── ios │ ├── README.txt │ ├── __init__.py │ └── __main__.py ├── ioskit.py ├── logutils.py ├── patch.py ├── record │ ├── __init__.py │ ├── android.py │ ├── android_hooks.py │ ├── android_layout.py │ ├── base.py │ ├── draft_editor.py │ ├── monkey.py │ ├── scene_detector.py │ ├── site │ │ ├── index.html │ │ └── static │ │ │ ├── css │ │ │ └── app.css │ │ │ └── js │ │ │ ├── app.js │ │ │ ├── manifest.js │ │ │ └── vendor.js │ └── windows.py ├── strutils.py ├── taskqueue │ ├── DESIGN.md │ ├── __init__.py │ └── __main__.py └── vendor │ ├── RotationWatcher.apk │ └── STFService.apk ├── docs ├── API.md ├── Makefile ├── QUICKSTART.md ├── conf.py ├── index.rst ├── make.bat ├── multisize-button.png └── usage.rst ├── examples └── coc │ └── test_coc.py.ipynb ├── images ├── atx-gui.gif ├── demo.gif ├── logo.png ├── macmini.jpg └── tkide.png ├── publish.sh ├── requirements.txt ├── scripts ├── 100-lines-tcp-proxy.py ├── AXMLPrinter2.jar ├── README.md ├── adb_old.py ├── airtest_recoder.py ├── airtoolbox │ ├── Makefile │ ├── common.go │ ├── input.go │ ├── main.go │ └── stat.go ├── androaxml.py ├── androguard.zip ├── apkview │ ├── Makefile │ ├── apkview.go │ └── proto.go ├── compile.sh ├── gui1.py ├── image.py ├── install-minicap.py ├── install.py ├── jurassic_park_kitchen.jpg ├── mac_install.sh ├── monitor.py ├── monkey_playback.py ├── monkey_recorder.py ├── pixelmatch.py ├── py-interpreter.py ├── run.bat ├── runxinput.bat ├── simple-ide.py ├── simple-tcp-proxy.py ├── snow │ ├── README.txt │ ├── static │ │ └── js │ │ │ └── index.js │ ├── templates │ │ ├── index.html │ │ ├── layout.html │ │ └── runtest.html │ └── web.py ├── surfaceflinger-fps.py ├── test-insert-code.py ├── tkgui.py └── uiautomation │ ├── instruments-test.js │ ├── run_instruments.sh │ ├── runjs.sh │ └── write_pipe.sh ├── setup.cfg ├── setup.py └── tests ├── .fsw.yml ├── README.txt ├── media ├── dummy_screen.png ├── haima.png └── system-app.png ├── requirements.txt ├── runtest.sh ├── test.py ├── test_android.py ├── test_base.py ├── test_dummy.py ├── test_ext_gt.py ├── test_ext_report.py ├── test_imutils.py ├── test_mixin.py ├── test_monkey.py ├── test_record.py ├── test_scene_detector.py ├── test_strutils.py ├── test_windows.py └── testcase_examples ├── blockly.py └── blockly.xml /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Version information 2 | - ATX Version: You can get from `python -matx version` 3 | - Python Version: You can get from `python -V` 4 | 5 | ## What steps will reproduce the problem? 6 | 7 | ## What is the expected output? 8 | 9 | ## What do you see instead? 10 | 11 | ## Do you have any log or screenshots? 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | tmp/ 38 | log/ 39 | *.log 40 | test/match.png 41 | _site 42 | apps 43 | __cache__ 44 | *.png 45 | .eggs 46 | .cache 47 | AUTHORS 48 | *.trace/ 49 | *.tiff 50 | .ipynb_checkpoints 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | install: 7 | - pip install --upgrade pip wheel 8 | - pip install -r requirements.txt 9 | - pip install -r tests/requirements.txt 10 | - pip install opencv_python 11 | script: 12 | - bash tests/runtest.sh 13 | deploy: 14 | - provider: pypi 15 | user: codeskyblue 16 | password: 17 | secure: gP+Fo/jBmk32PW2p01wSX/qL6rj6Y7X+ZZ1XLHKUXtBCwVQtAvYtqlevVIKuvima4DDjF4YYzZFN1XLGt3j10MRKKNHrn8pV7I90KqvLk2uodmBASn0HGnGRCEFyJDgkCJT9BqOXbfATcxtMt5yBpf1HRk9f4CQvW3yuKfqd8kSyUnDYN7xHQBAxqYrlihW6jZ0yb38fx8WNvmjRxNknoUsr9m8qO7vfkbAaff+AwBG39UTOFrGpRk909/BVLze69vEyupJurNQDN01MP7IumwbCaG2wYTqC8T1OYm4huBQqhI+AhG2fkznAS2FEL/3NGDLZGjPfsozEnyahwt6NQl6idBumBlSaZcFoO0ulROa1yVEuBA8ODgk9zH9pADcuo3Yg7fgkHZ0FNwrqEBbEEG0c8q6rv3a/aBFDVXTTmvKLUMS83bkafMuMGCH+pm5KBV6wD/Bq0PXjeY6PXo9ucPQFWCeaIsWPsaYso668aYMSqStbaoReg4iDgC95uNHBnba59SbMWj3ASW/1nmsgaXYzov1H2lIEMuS0OO5h+gtC/ft+Alja/qVhEUN9+SpQz5St9KHfMBsIw/O9hucCx/egsmge9Wxdr18fTlesGDUs3Quco5vZwJuscutf1dHTylwORwQKJztnRoyQoHY6v5lMqbmoI/xs72H6mcCaHlk= 18 | distributions: sdist bdist_wheel 19 | - provider: pypi 20 | user: codeskyblue 21 | password: 22 | secure: gP+Fo/jBmk32PW2p01wSX/qL6rj6Y7X+ZZ1XLHKUXtBCwVQtAvYtqlevVIKuvima4DDjF4YYzZFN1XLGt3j10MRKKNHrn8pV7I90KqvLk2uodmBASn0HGnGRCEFyJDgkCJT9BqOXbfATcxtMt5yBpf1HRk9f4CQvW3yuKfqd8kSyUnDYN7xHQBAxqYrlihW6jZ0yb38fx8WNvmjRxNknoUsr9m8qO7vfkbAaff+AwBG39UTOFrGpRk909/BVLze69vEyupJurNQDN01MP7IumwbCaG2wYTqC8T1OYm4huBQqhI+AhG2fkznAS2FEL/3NGDLZGjPfsozEnyahwt6NQl6idBumBlSaZcFoO0ulROa1yVEuBA8ODgk9zH9pADcuo3Yg7fgkHZ0FNwrqEBbEEG0c8q6rv3a/aBFDVXTTmvKLUMS83bkafMuMGCH+pm5KBV6wD/Bq0PXjeY6PXo9ucPQFWCeaIsWPsaYso668aYMSqStbaoReg4iDgC95uNHBnba59SbMWj3ASW/1nmsgaXYzov1H2lIEMuS0OO5h+gtC/ft+Alja/qVhEUN9+SpQz5St9KHfMBsIw/O9hucCx/egsmge9Wxdr18fTlesGDUs3Quco5vZwJuscutf1dHTylwORwQKJztnRoyQoHY6v5lMqbmoI/xs72H6mcCaHlk= 23 | distributions: sdist bdist_wheel 24 | on: 25 | tags: true 26 | after_deploy: 27 | - "curl -X POST https://readthedocs.org/build/atx" 28 | notifications: 29 | webhooks: 30 | urls: 31 | - https://webhooks.gitter.im/e/8b03bdd6acb150af0088 32 | on_success: change # options: [always|never|change] default: always 33 | on_failure: always # options: [always|never|change] default: always 34 | on_start: never # options: [always|never|change] default: always 35 | -------------------------------------------------------------------------------- /ABOUT.rst: -------------------------------------------------------------------------------- 1 | Documents in -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # DESIGN 2 | ## Watch 3 | 循环检测,当某事件发生时,执行特定操作 4 | 5 | ``` 6 | from atx import script_utils as wu 7 | ``` 8 | 9 | ### 只触发一次效果 10 | ```py 11 | btn_login = d(text=u'登陆', className='Button') 12 | once = scu.Once() 13 | once.exists(btn_login) and btn_login.click() 14 | ``` 15 | 16 | Same as 17 | ```py 18 | btn_login = d(text=u'登陆', className='Button') 19 | trigged = False 20 | if not trigged and btn_login.exists: 21 | trigged = True 22 | btn_login.click() 23 | ``` 24 | 25 | ### 存在并点击效果 26 | ``` 27 | scu.safe_click(d(text='Update')) 28 | ``` 29 | 30 | 等价于 31 | 32 | ``` 33 | btn_update = d(text='Update') 34 | if btn_update.exists: 35 | btn_update.click() 36 | ``` 37 | 38 | ### Timeout效果 39 | 40 | ``` 41 | with scu.while_timeout(50, safe=True): 42 | pass 43 | # raise scu.Continue() 44 | # raise scu.Break() 45 | ``` 46 | 47 | 等价于 48 | 49 | ``` 50 | safe = True 51 | deadline = time.time() + 50 52 | while time.time() < deadline: 53 | pass 54 | # continue 55 | # break 56 | else: 57 | if not safe: 58 | raise RuntimeError("while timeout') 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 历史起源 2 | 该项目重构于过去写过一个项目 , 3 | 因原有代码冗余太严重,维护成本太高,于是就用ATX取代之(Develop from 2016-02-25)。 4 | 5 | 新版有哪些新的功能呢? 6 | 7 | ### 与原版主要变化 8 | * 简化安装方式,只需要安装opencv以及通过pip安装atx 无其他依赖 9 | * 支持原生UI元素的查找和点击 10 | * 截图方式从原有缓慢的adb截图,改成默认uiautomator截图,可选minicap截图(1080x1920手机截图平均耗时0.2s) 11 | * 优化图像的自动缩放算法,以便同样的脚本可以适应不同的机器 12 | * 支持Watch用法,可持续监控界面,当某个元素出现时执行特定操作 13 | * 截图客户端从网页服务器变成了python-Tkinter写的客户端 使用python -matx gui启动 14 | * 支持dir(dev) 查看元素已有的方法(-_-! 之前代码写的不好,并不支持) 15 | * 更稳定的依赖库控制,与travis持续集成,可在代码更新后自动发布到pypi 16 | * 移除性能监控功能(目前性能测试使用第三方工具 腾讯GT) 17 | * 图像匹配默认使用模版匹配,将SIFT匹配改为可选 18 | 19 | [更多More](CHANGELOG) 20 | -------------------------------------------------------------------------------- /README_ADVANCED.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | Read this doc before you read [README.md](README.md) 3 | 4 | ## How to catch click_image errors 5 | 6 | ```py 7 | import atx 8 | 9 | d = atx.connect() 10 | try: 11 | d.click_image('button.png', timeout=1) 12 | except atx.Error as e: 13 | print e.data 14 | ``` 15 | 16 | Expect result if button.png is not found, you will find matched is False 17 | 18 | ``` 19 | FindPoint(pos=(115, 1014), confidence=0.5189774632453918, method='template', matched=False) 20 | ``` 21 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | ### Blockly 4 | 1. 连接安卓设备(done) 5 | 6 | 7 | 8 | 2. 监控 9 | 10 | 11 | 12 | 3. 坐标点击(done) 13 | 14 | 15 | 16 | ### 测试用例草案 17 | 目录结构 18 | 19 | ``` 20 | testcase_examples/ 21 | ├── blockly.py 22 | ├── blockly.xml 23 | └── images 24 | └── blockly 25 | └── button@rsl(1080x1920)offset(12_30).png 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Appveyor is Windows CI: https://ci.appveyor.com/project/bdarnell/tornado 2 | environment: 3 | global: 4 | TORNADO_EXTENSION: "1" 5 | 6 | # We only build with 3.5 because it works out of the box, while other 7 | # versions require lots of machinery. 8 | matrix: 9 | - PYTHON: "C:\\Python27" 10 | PYTHON_VERSION: "2.7.x" 11 | PYTHON_ARCH: "32" 12 | 13 | - PYTHON: "C:\\Python27-x64" 14 | PYTHON_VERSION: "2.7.x" 15 | PYTHON_ARCH: "64" 16 | 17 | install: 18 | # Make sure the right python version is first on the PATH. 19 | - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" 20 | 21 | # Check that we have the expected version and architecture for Python 22 | - "python --version" 23 | - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" 24 | 25 | # Upgrade to the latest version of pip to avoid it displaying warnings 26 | # about it being out of date. 27 | - "pip install --disable-pip-version-check --user --upgrade pip" 28 | 29 | - "pip install tox wheel" 30 | 31 | build: false # Not a C# project, build stuff at the test step instead. 32 | 33 | test_script: 34 | # Build the compiled extension and run the project tests. 35 | # This is a bit of a hack that won't scale when py36 is out, 36 | # but for now it lets us avoid duplication with .travis.yml and tox.ini. 37 | # Running "py35-full" would be nice but it's failing on installing 38 | # dependencies with no useful logs. 39 | - "tox -e py35" 40 | 41 | after_test: 42 | # If tests are successful, create binary packages for the project. 43 | - "python setup.py bdist_wheel" 44 | - ps: "ls dist" 45 | 46 | artifacts: 47 | # Archive the generated packages in the ci.appveyor.com build report. 48 | - path: dist\* 49 | 50 | #on_success: 51 | # - TODO: upload the content of dist/*.whl to a public wheelhouse 52 | # -------------------------------------------------------------------------------- /atx/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """This module is to make mobile test more easily 5 | """ 6 | 7 | from __future__ import absolute_import 8 | from __future__ import print_function 9 | 10 | import os 11 | import sys 12 | import six 13 | 14 | import pkg_resources 15 | try: 16 | version = pkg_resources.get_distribution("atx").version 17 | except pkg_resources.DistributionNotFound: 18 | version = 'unknown' 19 | 20 | from atx.consts import * 21 | from atx.errors import * 22 | from atx.drivers import Pattern, Bounds, ImageCrop 23 | import atx.adbkit.client 24 | 25 | 26 | # make a global var for easily use 27 | adb_client = atx.adbkit.client.Client() 28 | 29 | 30 | def _connect_url(*args): 31 | if len(args) == 0: 32 | return os.getenv('ATX_CONNECT_URL') 33 | return args[0] 34 | 35 | 36 | def _detect_platform(connect_url): 37 | if os.getenv('ATX_PLATFORM'): 38 | return os.getenv('ATX_PLATFORM') 39 | 40 | if not connect_url: # None or "" 41 | return 'android' 42 | elif connect_url.startswith('http://'): # WDA use http url as connect str 43 | return 'ios' 44 | else: 45 | return 'android' 46 | 47 | 48 | def connect(*args, **kwargs): 49 | """Connect to a device, and return its object 50 | Args: 51 | platform: string one of 52 | 53 | Returns: 54 | None 55 | 56 | Raises: 57 | SyntaxError, EnvironmentError 58 | """ 59 | connect_url = _connect_url(*args) 60 | platform = kwargs.pop('platform', _detect_platform(connect_url)) 61 | 62 | cls = None 63 | if platform == 'android': 64 | os.environ['JSONRPC_TIMEOUT'] = "60" # default is 90s which is too long. 65 | devcls = __import__('atx.drivers.android') 66 | cls = devcls.drivers.android.AndroidDevice 67 | elif platform == 'windows': 68 | devcls = __import__('atx.drivers.windows') 69 | cls = devcls.drivers.windows.WindowsDevice 70 | elif platform == 'ios': 71 | devcls = __import__('atx.drivers.ios_webdriveragent') 72 | cls = devcls.drivers.ios_webdriveragent.IOSDevice 73 | elif platform == 'webdriver': 74 | devcls = __import__('atx.drivers.webdriver') 75 | cls = devcls.drivers.webdriver.WebDriver 76 | elif platform == 'dummy': # for py.test use 77 | devcls = __import__('atx.drivers.dummy') 78 | cls = devcls.drivers.dummy.DummyDevice 79 | 80 | if cls is None: 81 | raise SyntaxError('Platform: %s not exists' % platform) 82 | 83 | c = cls(connect_url, **kwargs) 84 | c.platform = platform 85 | return c 86 | 87 | 88 | 89 | # def _sig_handler(signum, frame): 90 | # print >>sys.stderr, 'Signal INT catched !!!' 91 | # sys.exit(1) 92 | # signal.signal(signal.SIGINT, _sig_handler) 93 | -------------------------------------------------------------------------------- /atx/adbkit/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Blueprint, not finished yet. 5 | 6 | >>> dev.packages() 7 | [{'name': 'com.example.demo', 'version': 2}] 8 | >>> dev.forward_list() 9 | [{'local': 'tcp:8001', 'remote': 'tcp:8000'}] 10 | >>> dev.properties() 11 | {'ro.build.brand', 'MI2', ...} 12 | >>> dev.install('demo.apk') 13 | True 14 | >>> dev.uninstall('com.example.demo', keep_data=True) # DONE 15 | True 16 | >>> dev.logcat() # TODO 17 | >>> dev.pull('/data/local/tmp/_screen.png', './') 18 | True 19 | >>> dev.push('./demo.apk', '/data/local/tmp/demo.apk') 20 | True 21 | >>> dev.listdir('/data/local/tmp') 22 | ['_screen.png'] 23 | >>> dev.shell('ls', '-l', '/data/local/tmp/') 24 | :output as string, replace \r\n to '\n' 25 | >>> dev.start_activity('com.example.demo', '.Client') 26 | None 27 | >>> dev.stat('/data/local/tmp/_screen.png') 28 | :posix.stat_result object 29 | >>> dev.current_app() 30 | com.example.demo 31 | >>> dev.orientation() 32 | : one of [1-4] 33 | >>> dev.screenshot() # DONE 34 | : PIL image object 35 | >>> dev.keyevent('HOME') 36 | None 37 | >>> dev.open_minicap() 38 | True 39 | >>> dev.open_minitouch() 40 | True 41 | >>> dev.touch(100, 100) 42 | None 43 | >>> dev.swipe() # TODO 44 | >>> dev.pinch() # only in minitouch 45 | """ 46 | 47 | from __future__ import absolute_import 48 | 49 | from atx.adbkit.client import Client 50 | 51 | 52 | if __name__ == '__main__': 53 | adb = Client() 54 | print(adb.devices()) 55 | print(adb.version()) 56 | dev = adb.device() #'10.250.210.165:57089') 57 | # print dev.keyevent('HOME') 58 | print(dev.display) 59 | # for pkg in dev.packages(): 60 | # print pkg 61 | # dev.screenshot('s.png', scale=1.0) 62 | print(dev.is_locked()) 63 | print(dev.wake()) 64 | dev.click(568, 1488) 65 | print(dev.current_app()) 66 | -------------------------------------------------------------------------------- /atx/adbkit/openstf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/adbkit/openstf/__init__.py -------------------------------------------------------------------------------- /atx/adbkit/openstf/test_service.py: -------------------------------------------------------------------------------- 1 | #-*- encoding: utf-8 -*- 2 | 3 | import sys 4 | import time 5 | import service 6 | 7 | def on_battery(info): 8 | print 'on battery info', info 9 | 10 | def on_rotation(rot): 11 | print 'on rotation', rot 12 | 13 | def test_service(): 14 | serial = "DU2SSE1467010532"; #hwH60 15 | 16 | from random import randint 17 | import stfwire_pb2 as wire 18 | 19 | reqs = [ 20 | (wire.GET_VERSION, wire.GetVersionRequest()), 21 | (wire.DO_IDENTIFY, wire.DoIdentifyRequest(serial=serial)), 22 | (wire.DO_ADD_ACCOUNT_MENU, wire.DoAddAccountMenuRequest()), 23 | (wire.DO_REMOVE_ACCOUNT, wire.DoRemoveAccountRequest(type="nsdfjslfs")), 24 | (wire.GET_ACCOUNTS, wire.GetAccountsRequest(type="root")), 25 | (wire.GET_BROWSERS, wire.GetBrowsersRequest()), 26 | (wire.GET_CLIPBOARD, wire.GetClipboardRequest(type=wire.TEXT)), 27 | (wire.GET_DISPLAY, wire.GetDisplayRequest(id=0)), 28 | (wire.GET_PROPERTIES, wire.GetPropertiesRequest(properties=["ro.product.device"])), 29 | (wire.GET_RINGER_MODE, wire.GetRingerModeRequest()), 30 | (wire.GET_SD_STATUS, wire.GetSdStatusRequest()), 31 | (wire.GET_WIFI_STATUS, wire.GetWifiStatusRequest()), 32 | (wire.SET_CLIPBOARD, wire.SetClipboardRequest(type=wire.TEXT, text="hello world")), 33 | (wire.SET_KEYGUARD_STATE, wire.SetKeyguardStateRequest(enabled=False)), 34 | (wire.SET_RINGER_MODE, wire.SetRingerModeRequest(mode=wire.VIBRATE)), 35 | (wire.SET_WAKE_LOCK, wire.SetWakeLockRequest(enabled=False)), 36 | (wire.SET_WIFI_ENABLED, wire.SetWifiEnabledRequest(enabled=True)), 37 | (wire.SET_MASTER_MUTE, wire.SetMasterMuteRequest(enabled=True)), 38 | ] 39 | 40 | total = len(reqs) 41 | idx = 0 42 | queue = service.service_queue 43 | pack = service.pack 44 | 45 | service.start_stf_service() 46 | service.listen_service() 47 | 48 | while True: 49 | 50 | if randint(1, 10) < 3 and idx < total: 51 | mtype, request = reqs[idx] 52 | msg = pack(mtype, request, idx) 53 | queue.put(msg) 54 | idx += 1 55 | time.sleep(1) 56 | 57 | if sys.platform == 'win32': 58 | import msvcrt 59 | def getchar(): 60 | return msvcrt.getch() 61 | else: 62 | import tty 63 | import termios 64 | 65 | def getchar(): 66 | fd = sys.stdin.fileno() 67 | old_settings = termios.tcgetattr(fd) 68 | try: 69 | tty.setraw(sys.stdin.fileno()) 70 | ch = sys.stdin.read(1) 71 | finally: 72 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 73 | return ch 74 | 75 | def test_type(): 76 | import locale 77 | _, encoding = locale.getdefaultlocale() 78 | buf = '' 79 | while True: 80 | ch = getchar() 81 | if ch == '\x03': # Ctrl+C 82 | break 83 | buf += ch 84 | try: 85 | text = buf.decode(encoding) 86 | except: 87 | pass 88 | else: 89 | print 'try to input', repr(text) 90 | if len(buf) == 1: 91 | service.keyboard(buf) 92 | else: 93 | service.type(text) 94 | buf = '' 95 | 96 | def test_agent(): 97 | service.start_stf_agent(restart=True) 98 | service.listen_agent() 99 | 100 | print 'KEYCODE_HOME' 101 | service.keyevent('KEYCODE_HOME') 102 | #service.wake() 103 | 104 | print 'test ascii_type Ctrl+C to stop' 105 | while True: 106 | ch = getchar() 107 | print 'try to input', repr(ch) 108 | if ch == '\x03': # Ctrl+C 109 | break 110 | continue 111 | service.ascii_type(ch) 112 | 113 | print 'test keyboard Ctrl+C to stop' 114 | while True: 115 | ch = getchar() 116 | print 'try to input', repr(ch) 117 | if ch == '\x03': # Ctrl+C 118 | break 119 | continue 120 | service.keyboard(ch) 121 | 122 | #service.stop() 123 | 124 | def testall(): 125 | service.start() 126 | 127 | service.on_battery_event(on_battery) 128 | service.on_rotation_event(on_rotation) 129 | 130 | service.identify() 131 | time.sleep(2) 132 | service.keyevent('KEYCODE_HOME') 133 | time.sleep(2) 134 | 135 | print 'wifi is', service.get_wifi_status() 136 | print 'disable', service.set_wifi_enabled(False) 137 | print 'wifi is', service.get_wifi_status() 138 | print 'enable', service.set_wifi_enabled(True) 139 | print 'wifi is', service.get_wifi_status() 140 | time.sleep(1) 141 | 142 | print 'set rotation' 143 | print service.set_rotation(1) 144 | time.sleep(1) 145 | print service.set_rotation(2) 146 | time.sleep(1) 147 | print service.set_rotation(3) 148 | time.sleep(1) 149 | print service.set_rotation(0) 150 | time.sleep(1) 151 | 152 | print 'display', service.get_display() 153 | time.sleep(1) 154 | 155 | print 'test type, please input' 156 | test_type() 157 | 158 | service.stop() 159 | 160 | if __name__ == '__main__': 161 | #test_service() 162 | #test_agent() 163 | testall() 164 | -------------------------------------------------------------------------------- /atx/adbkit/openstf/tkinput.py: -------------------------------------------------------------------------------- 1 | #-*- encoding: utf-8 -*- 2 | 3 | import sys 4 | import Tkinter as tk 5 | 6 | import service 7 | import keycode 8 | 9 | if sys.platform == 'win32': 10 | from ctypes import wintypes, byref, windll 11 | import win32con 12 | 13 | def handle_hotkey(root, callback): 14 | msg = wintypes.MSG() 15 | if windll.user32.GetMessageA(byref(msg), None, 0, 0) != 0: 16 | if msg.message == win32con.WM_HOTKEY: 17 | if msg.wParam == 1: 18 | print 'Hotkey triggered!' 19 | callback() 20 | windll.user32.TranslateMessage(byref(msg)) 21 | windll.user32.DispatchMessageA(byref(msg)) 22 | root.after(1, handle_hotkey, root, callback) 23 | 24 | # hotkey map refs: https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx 25 | # not yet used here. 26 | def register_hotkey(root, key, callback): 27 | key = key.split('-') 28 | mod = 0 29 | if 'Ctrl' in key: 30 | mod |= win32con.MOD_CONTROL 31 | if 'Shift' in key: 32 | mod |= win32con.MOD_SHIFT 33 | if 'Alt' in key: 34 | mod |= win32con.MOD_ALT 35 | key = key[-1].upper() 36 | assert key in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 37 | if windll.user32.RegisterHotKey(None, 1, mod, ord(key)) != 0: 38 | print("Hotkey registered!") 39 | handle_hotkey(root, callback) 40 | 41 | else: 42 | def register_hotkey(root, key, callback): 43 | print 'Register hotkey failed.' 44 | 45 | def main(): 46 | service.start() 47 | 48 | root = tk.Tk() 49 | root.resizable(0, 0) 50 | root.title('STF Input') 51 | sv = tk.StringVar() 52 | 53 | if sys.platform == 'win32': 54 | backspace = '\x08' 55 | else: 56 | backspace = '\x7f' 57 | 58 | def send(event, sv=sv): 59 | char = event.char 60 | if not char: 61 | return 62 | text = sv.get() 63 | if char == '\r' and text: # use to input 64 | service.type(text) 65 | sv.set('') 66 | return 67 | if char == backspace and text: # use to delete, not avaialable. 68 | sv.set('') 69 | return 70 | if char == '\x16': # skip 71 | service.keyboard(char) 72 | sv.set('') 73 | return 'break' 74 | if char in keycode.KEYBOARD_KEYS or char in keycode.CTRLED_KEYS: 75 | service.keyboard(char) 76 | 77 | entry = tk.Entry(root, textvariable=sv) 78 | entry.pack() 79 | entry.focus_set() 80 | entry.bind('', send) 81 | 82 | state = [1] 83 | def toggle(root=root, entry=entry): 84 | if state[0] == 0: 85 | root.deiconify() 86 | entry.focus_set() 87 | state[0] = 1 88 | else: 89 | root.withdraw() 90 | state[0] = 0 91 | 92 | register_hotkey(root, 'Ctrl-Alt-Z', toggle) # not very well with IME 93 | 94 | try: 95 | root.mainloop() 96 | finally: 97 | service.stop() 98 | 99 | if __name__ == '__main__': 100 | main() -------------------------------------------------------------------------------- /atx/apkparse.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | 4 | from xml.dom import minidom 5 | from apkutils import APK 6 | 7 | 8 | class Manifest(object): 9 | def __init__(self, content): 10 | self._dom = minidom.parseString(content) 11 | self._permissions = None 12 | 13 | @property 14 | def package_name(self): 15 | return self._dom.documentElement.getAttribute('package') 16 | 17 | @property 18 | def version_code(self): 19 | return self._dom.documentElement.getAttribute("android:versionCode") 20 | 21 | @property 22 | def version_name(self): 23 | return self._dom.documentElement.getAttribute("android:versionName") 24 | 25 | @property 26 | def permissions(self): 27 | if self._permissions is not None: 28 | return self._permissions 29 | self._permissions = [] 30 | for item in self._dom.getElementsByTagName("uses-permission"): 31 | self._permissions.append(str(item.getAttribute("android:name"))) 32 | return self._permissions 33 | 34 | @property 35 | def main_activity(self): 36 | """ 37 | Returns: 38 | the name of the main activity 39 | """ 40 | x = set() 41 | y = set() 42 | for item in self._dom.getElementsByTagName("activity"): 43 | for sitem in item.getElementsByTagName("action"): 44 | val = sitem.getAttribute("android:name") 45 | if val == "android.intent.action.MAIN": 46 | x.add(item.getAttribute("android:name")) 47 | for sitem in item.getElementsByTagName("category"): 48 | val = sitem.getAttribute("android:name") 49 | if val == "android.intent.category.LAUNCHER": 50 | y.add(item.getAttribute("android:name")) 51 | z = x.intersection(y) 52 | if len(z) > 0: 53 | return z.pop() 54 | return None 55 | 56 | 57 | def parse_apkfile(file): 58 | ''' 59 | Args: 60 | - file: filename or file object 61 | Returns: 62 | Manifest(Class) 63 | ''' 64 | apk = APK(file) 65 | return Manifest(apk.get_org_manifest()) 66 | 67 | 68 | if __name__ == '__main__': 69 | m = parse_apkfile("your-apk.apk") 70 | print(m.version_code) 71 | -------------------------------------------------------------------------------- /atx/cmds/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/__init__.py -------------------------------------------------------------------------------- /atx/cmds/doctor.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # check symbols are from: 5 | # http://www.i2symbol.com/symbols/check 6 | 7 | import os 8 | import subprocess 9 | 10 | from colorama import init, Fore, Back, Style 11 | init() 12 | 13 | 14 | def print_info(message, success): 15 | fore_color = Fore.GREEN 16 | symbol = '✔' if os.name != 'nt' else '[GOOD]' 17 | if not success: 18 | fore_color = Fore.RED 19 | symbol = '✘' if os.name != 'nt' else '[FAIL]' 20 | 21 | print(fore_color + "{symbol} {message}".format(symbol=symbol, message=message) + Style.RESET_ALL) 22 | 23 | 24 | def main(): 25 | # check if adb exists 26 | try: 27 | subprocess.call(['adb', 'version']) 28 | print_info("adb found in env PATH", True) 29 | except OSError: 30 | print_info("adb not found in env PATH", False) 31 | 32 | 33 | if __name__ == '__main__': 34 | main() -------------------------------------------------------------------------------- /atx/cmds/info.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | import atx 6 | 7 | 8 | def main(serial, host, port): 9 | d = atx.connect(serial, host=host, port=port) 10 | props = d.properties 11 | (w, h) = d.display 12 | info = { 13 | 'serial': d.serial, 14 | 'product.model': props['ro.product.model'], 15 | 'product.brand': props.get('ro.product.brand'), 16 | 'sys.country': props.get('persist.sys.country'), 17 | 'display': '%dx%d' % (w, h), 18 | 'version.sdk': int(props.get('ro.build.version.sdk', 0)), 19 | 'version.release': props.get('ro.build.version.release'), 20 | 'product.cpu.abi': props.get('ro.product.cpu.abi'), 21 | } 22 | print(json.dumps(info, indent=4)) 23 | -------------------------------------------------------------------------------- /atx/cmds/install.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import atexit 5 | import os 6 | import re 7 | import shutil 8 | import sys 9 | import time 10 | import tempfile 11 | 12 | import tqdm 13 | 14 | from atx import logutils 15 | from atx import adbkit 16 | from atx.cmds import utils 17 | 18 | try: 19 | import subprocess32 as subprocess 20 | except: 21 | import subprocess 22 | 23 | 24 | log = logutils.getLogger('install') 25 | DEFAULT_REMOTE_PATH = '/data/local/tmp/_atx_tmp.apk' 26 | __apks = { 27 | 'utf7ime': 'http://o8oookdsx.qnssl.com/android_unicode_ime-debug.apk', 28 | 'atx-assistant': 'https://o8oookdsx.qnssl.com/atx-assistant-1.0.4.apk', 29 | } 30 | 31 | 32 | def clean(tmpdir): 33 | log.info('Remove temp directory') 34 | shutil.rmtree(tmpdir) 35 | 36 | 37 | def adb_pushfile(adb, filepath, remote_path): 38 | filesize = os.path.getsize(filepath) 39 | pb = tqdm.tqdm(unit='B', unit_scale=True, total=filesize) 40 | p = adb.raw_cmd('push', filepath, remote_path) 41 | 42 | while True: 43 | try: 44 | p.wait(0.5) 45 | except subprocess.TimeoutExpired: 46 | pb.n = get_file_size(adb, remote_path) 47 | pb.refresh() 48 | # log.info("Progress %dM/%dM", get_file_size(remote_path) >>20, filesize >>20) 49 | pass 50 | except (KeyboardInterrupt, SystemExit): 51 | p.kill() 52 | raise 53 | except: 54 | raise 55 | else: 56 | # log.info("Success pushed into device") 57 | break 58 | pb.close() 59 | 60 | 61 | def get_file_size(adb, remote_path): 62 | try: 63 | output = adb.run_cmd('shell', 'ls', '-l', remote_path) 64 | m = re.search(r'\s(\d+)', output) 65 | if not m: 66 | return 0 67 | return int(m.group(1)) 68 | except subprocess.CalledProcessError as e: 69 | log.warn("call error: %s", e) 70 | time.sleep(.1) 71 | return 0 72 | 73 | 74 | def adb_remove(adb, path): 75 | p = adb.raw_cmd('shell', 'rm', path) 76 | stdout, stderr = p.communicate() 77 | if stdout or stderr: 78 | log.warn('%s\n%s', stdout, stderr) 79 | 80 | 81 | def adb_install(adb, remote_path): 82 | stdout = adb.run_cmd('shell', 'pm', 'install', '-rt', remote_path) 83 | # stdout, _ = p.communicate() 84 | if stdout.find('Success') == -1: 85 | raise IOError("Adb install failed: %s" % stdout) 86 | 87 | 88 | def adb_must_install(adb, remote_path, package_name): 89 | try: 90 | adb_install(adb, remote_path) 91 | except IOError: 92 | log.info("Remove already installed app: %s", package_name) 93 | adb.raw_cmd('uninstall', package_name).wait() 94 | adb_install(adb, remote_path) 95 | 96 | 97 | def main(path, serial=None, host=None, port=None, start=False): 98 | adb = adbkit.Client(host, port).device( 99 | serial) # adbutils.Adb(serial, host, port) 100 | 101 | # use qiniu paths 102 | if __apks.get(path): 103 | path = __apks.get(path) 104 | 105 | if re.match(r'^https?://', path): 106 | tmpdir = tempfile.mkdtemp(prefix='atx-install-') 107 | log.info("Create temp directory: %s", tmpdir) 108 | 109 | # FIXME(ssx): will not called when Ctrl+C pressed in windows git-bash 110 | atexit.register(clean, tmpdir) 111 | 112 | urlpath = path 113 | target = os.path.join(tmpdir, '_tmp.apk') 114 | path = target 115 | log.info("Download from: %s", urlpath) 116 | utils.http_download(urlpath, target) 117 | 118 | import atx.apkparse as apkparse 119 | manifest = apkparse.parse_apkfile(path) 120 | log.info("APK package name: %s", manifest.package_name) 121 | log.info("APK main activity: %s", manifest.main_activity) 122 | 123 | log.info("Push file to android device") 124 | adb_pushfile(adb, path, DEFAULT_REMOTE_PATH) 125 | 126 | log.info("Install ..., will take a few seconds") 127 | adb_must_install(adb, DEFAULT_REMOTE_PATH, manifest.package_name) 128 | log.info("Remove _tmp.apk") 129 | adb_remove(adb, DEFAULT_REMOTE_PATH) 130 | 131 | if start: 132 | log.info("Start app '%s'" % manifest.package_name) 133 | adb.raw_cmd('shell', 'am', 'start', '-n', 134 | manifest.package_name+'/'+manifest.main_activity).wait() 135 | log.info("Success") 136 | -------------------------------------------------------------------------------- /atx/cmds/iosdeveloper.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import atexit 6 | import os 7 | import subprocess 8 | import tempfile 9 | import logging 10 | import shutil 11 | 12 | from atx.cmds.utils import http_download 13 | 14 | 15 | __alias = { 16 | '9.3': '9.3 (13E230)', # 2016-05-04 17 | } 18 | 19 | # Can also be found in directory 20 | IMAGE_BASE_DIR = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/' 21 | IMAGE_BASE_URL = 'http://gohttp.nie.netease.com/tools/tools-ios/DeveloperImages/' 22 | logger = logging.getLogger('ios') 23 | 24 | 25 | def init(): 26 | ch = logging.StreamHandler() 27 | fmt = "%(asctime)s [%(name)s:%(lineno)4s] %(message)s" 28 | formatter = logging.Formatter(fmt) 29 | ch.setFormatter(formatter) 30 | logger.addHandler(ch) 31 | logger.setLevel(logging.DEBUG) 32 | 33 | 34 | def check_output(cmds, shell=False): 35 | try: 36 | output = subprocess.check_output(cmds, stderr=subprocess.STDOUT, shell=shell) 37 | return output 38 | except subprocess.CalledProcessError as e: 39 | logger.warn('Failed to run command: %s', ' '.join(cmds)) 40 | logger.warn('Error output:\n%s', e.output) 41 | raise 42 | 43 | 44 | def look_path(name, search_paths=[], env_path=True): 45 | os.pathsep 46 | if env_path: 47 | search_paths += os.getenv('PATH').split(os.pathsep) 48 | for directory in search_paths: 49 | if not os.path.isdir(directory): 50 | continue 51 | filepath = os.path.join(directory, name) 52 | if os.path.isfile(filepath): 53 | return filepath 54 | return None 55 | 56 | 57 | __execpath = {} 58 | 59 | def look_exec(name): 60 | if __execpath.get(name): 61 | return __execpath[name] 62 | 63 | ext = '.exe' if os.name == 'nt' else '' 64 | search_paths = [ 65 | r'C:\Program Files (x86)\Quamotion\iMobileDevice', 66 | r'D:\Program Files (x86)\Quamotion\iMobileDevice', 67 | r'E:\Program Files (x86)\Quamotion\iMobileDevice', 68 | r'C:\Program Files\Quamotion\iMobileDevice', 69 | r'D:\Program Files\Quamotion\iMobileDevice', 70 | r'E:\Program Files\Quamotion\iMobileDevice', 71 | ] 72 | filepath = look_path(name+ext, search_paths) 73 | __execpath[name] = filepath 74 | return filepath 75 | 76 | 77 | def idevice(name, *args): 78 | exec_name = 'idevice' + name 79 | exec_path = look_exec(exec_name) 80 | if not exec_path: 81 | raise EnvironmentError('Necessary binary ("%s") not found.' % exec_name) 82 | return check_output([exec_path] + list(args)) 83 | 84 | 85 | def devices(): 86 | udids = [udid.strip() for udid in idevice('_id', '-l').splitlines() if udid.strip()] 87 | return {udid: idevice('name', '-u', udid).decode('utf-8').strip() for udid in udids} 88 | 89 | 90 | def device_product_version(udid): 91 | return idevice('info', '-u', udid, '-k', 'ProductVersion').strip() 92 | 93 | 94 | def download(filename, tmpdir, version, base_url=IMAGE_BASE_URL): 95 | if sys.platform == 'darwin': 96 | version = __alias.get(version, version) 97 | abs_path = os.path.join(IMAGE_BASE_DIR, version, filename) 98 | if os.path.exists(abs_path): 99 | return abs_path 100 | target_path = os.path.join(tmpdir, filename) 101 | source_url = base_url + '/'.join([version, filename]) 102 | logger.info("Download %s/%s", version, filename) 103 | return http_download(source_url, target_path) 104 | 105 | 106 | def select_device(): 107 | devs = devices() 108 | if len(devs) == 0: 109 | raise EnvironmentError('iPhone device is not attached.') 110 | if len(devs) > 1: 111 | raise EnvironmentError('More than one iPhone device detected.') 112 | 113 | udid = devs.keys()[0] 114 | devname = devs[udid] 115 | return udid, devname 116 | 117 | 118 | def is_mounted(udid): 119 | return 'ImagePresent: true' in idevice('imagemounter', '-l', '-u', udid) 120 | 121 | 122 | def mount_image(udid, image_file, image_signature_file): 123 | if is_mounted(udid): 124 | logger.info("Developer image has already mounted.") 125 | return 126 | idevice('imagemounter', '-u', udid, image_file, image_signature_file) 127 | logger.info("^_^ Developer image has been mounted.") 128 | 129 | 130 | def check_enviroment(): 131 | return look_exec('idevice_id') is not None 132 | 133 | 134 | def main(udid=None): 135 | init() 136 | 137 | if not check_enviroment(): 138 | sys.exit("No imobiledevice found in $PATH, but you can download from here\n\n %s" %( 139 | "http://quamotion.mobi/iMobileDevice/Download",)) 140 | 141 | logger.info("Make tmp dir ...") 142 | tmpdir = tempfile.mkdtemp(prefix='atx-ios-developer-') 143 | if not tmpdir: 144 | logger.warn("tmpdir create failed.") 145 | sys.exit(1) 146 | 147 | @atexit.register 148 | def _clean(): 149 | logger.info("Cleaning tmp dir: %s", tmpdir) 150 | shutil.rmtree(tmpdir) 151 | 152 | udid, devname = select_device() 153 | long_version = device_product_version(udid) 154 | version = '.'.join(long_version.split('.')[:2]) 155 | logger.info("Device udid is %s", udid) 156 | logger.info("Device name is %s", devname) 157 | logger.info("Device version is %s", long_version) 158 | 159 | if is_mounted(udid): 160 | logger.info(" Developer Image has already mounted.") 161 | return 162 | 163 | image_file = download('DeveloperDiskImage.dmg', tmpdir, version) 164 | image_signature_file = download('DeveloperDiskImage.dmg.signature', tmpdir, version) 165 | 166 | mount_image(udid, image_file, image_signature_file) 167 | -------------------------------------------------------------------------------- /atx/cmds/minicap.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Description: minicap setup scripts, 5 | # Usage: python minicap_setup.py -s serialno -H host -P port 6 | # Author: ydbn2153 7 | # Created: ydbn2153 <2016-03-15> 8 | # Modified: hzsunshx <2016-03-19> 9 | 10 | import argparse 11 | import os 12 | import sys 13 | import shutil 14 | import subprocess 15 | import tempfile 16 | import urllib 17 | import functools 18 | 19 | from atx import logutils 20 | from atx.cmds.utils import http_download 21 | 22 | logger = logutils.getLogger('minicap') 23 | 24 | 25 | def log(*args): 26 | logger.info(*args) 27 | 28 | 29 | def check_output(cmdstr, shell=True): 30 | output = subprocess.check_output(cmdstr, stderr=subprocess.STDOUT, shell=shell) 31 | return output 32 | 33 | 34 | def run_adb(*args, **kwargs): 35 | cmds = ['adb'] 36 | serialno = kwargs.get('serialno', None) 37 | if serialno: 38 | cmds.extend(['-s', serialno]) 39 | host = kwargs.get('host') 40 | if host: 41 | cmds.extend(['-H', host]) 42 | port = kwargs.get('port') 43 | if port: 44 | cmds.extend(['-P', str(port)]) 45 | cmds.extend(args) 46 | cmds = map(str, cmds) 47 | cmdline = subprocess.list2cmdline(cmds) 48 | try: 49 | return check_output(cmdline, shell=True) 50 | except Exception, e: 51 | raise EnvironmentError('run cmd: {} failed. {}'.format(cmdline, e)) 52 | 53 | 54 | def main(serialno=None, host=None, port=None): 55 | logger.info("Minicap install started!") 56 | 57 | adb = functools.partial(run_adb, serialno=serialno, host=host, port=port) 58 | 59 | # Figure out which ABI and SDK 60 | logger.info("Make temp dir ...") 61 | tmpdir = tempfile.mkdtemp(prefix='ins-minicap-') 62 | logger.debug(tmpdir) 63 | try: 64 | logger.info("Retrive device information ...") 65 | abi = adb('shell', 'getprop', 'ro.product.cpu.abi').strip() 66 | sdk = adb('shell', 'getprop', 'ro.build.version.sdk').strip() 67 | 68 | minicap_base_url = "https://github.com/codeskyblue/stf-binaries/raw/master/node_modules/minicap-prebuilt/prebuilt/" 69 | logger.info("Downloading minicap.so ....") 70 | url = minicap_base_url+abi+"/lib/android-"+sdk+"/minicap.so" 71 | target_path = os.path.join(tmpdir, 'minicap.so') 72 | http_download(url, target_path) 73 | logger.info("Push data to device ....") 74 | adb('push', target_path, '/data/local/tmp') 75 | 76 | logger.info("Downloading minicap ....") 77 | url = minicap_base_url+abi+"/bin/minicap" 78 | target_path = os.path.join(tmpdir, 'minicap') 79 | http_download(url, target_path) 80 | logger.info("Push data to device ....") 81 | adb('push', target_path, '/data/local/tmp') 82 | adb('shell', 'chmod', '0755', '/data/local/tmp/minicap') 83 | 84 | logger.info("Checking [dump device info] ...") 85 | print adb('shell', 'LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -i') 86 | logger.info("Minicap install finished !") 87 | 88 | logger.info("Downloading minitouch ....") 89 | url = "https://github.com/codeskyblue/stf-binaries/raw/master/node_modules/minitouch-prebuilt/prebuilt/"+abi+"/bin/minitouch" 90 | target_path = os.path.join(tmpdir, 'minitouch') 91 | http_download(url, target_path) 92 | logger.info("Push data to device ....") 93 | adb('push', target_path, '/data/local/tmp') 94 | adb('shell', 'chmod', '0755', '/data/local/tmp/minitouch') 95 | 96 | logger.info("Checking [dump device info] ...") 97 | print adb('shell', '/data/local/tmp/minitouch -h') 98 | logger.info("Minitouch install successfully ^_^") 99 | 100 | except Exception, e: 101 | logger.error('error: %s', e) 102 | finally: 103 | if tmpdir: 104 | logger.info("Cleaning temp dir") 105 | shutil.rmtree(tmpdir) 106 | 107 | 108 | if __name__ == "__main__": 109 | parser = argparse.ArgumentParser("cli") 110 | parser.add_argument("-s", "--serialno", help="serialno of device", default=None) 111 | parser.add_argument("-H", "--host", help="host of remote device", default=None) 112 | parser.add_argument("-P", "--port", help="port of remote device", default=None) 113 | args = parser.parse_args(sys.argv[1:]) 114 | main(serialno=args.serialno, host=args.host, port=args.port) 115 | -------------------------------------------------------------------------------- /atx/cmds/monkey.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import random 5 | 6 | import atx 7 | import cv2 8 | from atx import imutils 9 | 10 | 11 | # codeskyblue by 2016-05-03 12 | # 过新手流程倒是挺容易的,不过还是有好多东西识别不出来,比如退出键,阴影下的属性切换键 13 | # 还需要结合其他的方法继续去弄 14 | 15 | def choose_point(frame): 16 | h, w = frame.shape[:2] 17 | framesize = h*w 18 | minarea = framesize/1000 19 | print 'minarea:', minarea 20 | 21 | hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) 22 | cv2.imshow('hsv', hsv) 23 | H, S, V = cv2.split(hsv) 24 | V[V<200] = 0 25 | # cv2.imshow('H', H) 26 | # cv2.imshow('S', S) 27 | cv2.imshow('V', V) 28 | 29 | mask = V 30 | mask = cv2.dilate(mask, None, iterations=2) 31 | # mask = cv2.erode(mask, None, iterations=2) 32 | cv2.imshow('mask', mask) 33 | 34 | (cnts, _) = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, 35 | cv2.CHAIN_APPROX_SIMPLE) 36 | 37 | points = [] 38 | for c in cnts: 39 | # if the contour is too small, ignore it 40 | area = cv2.contourArea(c) 41 | print area 42 | if area < minarea: 43 | continue 44 | if area > framesize/2: 45 | continue 46 | # if cv2.contourArea(c) < minarea: 47 | # continue 48 | 49 | # compute the bounding box for the contour, draw it on the frame, 50 | # and update the text 51 | # 计算轮廓的边界框,在当前帧中画出该框 52 | (x, y, w, h) = cv2.boundingRect(c) 53 | points.append((x, y, w, h)) 54 | cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) 55 | # text = "Occupied" 56 | 57 | # random pick point 58 | if not points: 59 | return None 60 | pt = random.choice(points) 61 | (x, y, w, h) = pt 62 | cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2) 63 | cv2.imshow('frame', frame) 64 | return (x+w/2, y+h/2) 65 | # cv2.waitKey(0) 66 | 67 | 68 | def main(serial=None, host=None, port=None): 69 | d = atx.connect(serial, host=host, port=port) 70 | while True: 71 | pilimg = d.screenshot() 72 | cv2img = imutils.from_pillow(pilimg) 73 | # cv2img = cv2.imread('tmp.png') 74 | # cv2.imwrite('tmp.png', cv2img) 75 | cv2img = cv2.resize(cv2img, fx=0.5, fy=0.5, dsize=(0, 0)) 76 | pt = choose_point(cv2img) 77 | print 'click:', pt 78 | if pt: 79 | x, y = pt 80 | d.click(2*x, 2*y) 81 | cv2.waitKey(100) 82 | # import time 83 | # time.sleep(0.1) -------------------------------------------------------------------------------- /atx/cmds/record.py: -------------------------------------------------------------------------------- 1 | #-*- encoding: utf-8 -*- 2 | 3 | import os 4 | import os.path 5 | import time 6 | 7 | from atx.record.android import RecordDevice, AndroidRecorder 8 | from atx.record.draft_editor import run as run_draft_editor 9 | 10 | def main(serial=None, host=None, port=None, workdir=".", nonui_activities=None, edit_mode=False): 11 | workdir = os.path.abspath(workdir) 12 | 13 | if edit_mode: 14 | run_draft_editor(workdir, None) 15 | return 16 | 17 | if not os.path.exists(workdir): 18 | os.makedirs(workdir) 19 | 20 | d = RecordDevice(serialno=serial, host=host, port=port) 21 | 22 | rec = AndroidRecorder(d, workdir) 23 | if nonui_activities: 24 | for a in nonui_activities: 25 | rec.add_nonui_activity(a) 26 | 27 | rec.start() 28 | 29 | try: 30 | time.sleep(4) 31 | print '-'*20 + ' STARTED ' + '-'*20 32 | print 'Please operate on the phone. Press Ctrl+C to stop.' 33 | while True: 34 | time.sleep(1) 35 | except KeyboardInterrupt: 36 | pass 37 | 38 | rec.stop() 39 | print '-'*20 + ' STOPPED ' + '-'*20 40 | 41 | if len(rec.frames) > 0: 42 | print 'start web service to modify recorded case' 43 | run_draft_editor(workdir, None) 44 | else: 45 | print 'No action recorded.' 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /atx/cmds/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # parse and run atx.yml 5 | # 6 | # # example of atx.yml 7 | # # 8 | # installation: http://example.com/demo.apk 9 | # script: 10 | # - python test1.py 11 | # - python test2.py 12 | # notification: 13 | # popo: 14 | # - someone@example.com 15 | # 16 | from __future__ import absolute_import 17 | 18 | import os 19 | import sys 20 | import json 21 | from argparse import Namespace 22 | 23 | import yaml 24 | try: 25 | import subprocess32 as subprocess 26 | except: 27 | import subprocess 28 | 29 | 30 | def json2obj(data): 31 | return json.loads(json.dumps(data), object_hook=lambda d: Namespace(**d)) 32 | 33 | 34 | def prompt(message): 35 | print('>>> ' + message) 36 | 37 | 38 | def must_exec(*cmds, **kwargs): 39 | prompt("Exec %s" % cmds) 40 | shell = kwargs.get('shell', False) 41 | cmdline = cmds[0] if shell else subprocess.list2cmdline(cmds) 42 | ret = os.system(cmdline) 43 | if ret != 0: 44 | raise SystemExit("Execute '%s' error" % cmdline) 45 | 46 | 47 | def install(src): 48 | prompt("Install") 49 | must_exec('python', '-matx', 'install', src) 50 | 51 | 52 | def runtest(scripts): 53 | prompt("Run scripts") 54 | for script in scripts: 55 | must_exec(script, shell=True) 56 | 57 | 58 | def notify_popo(users, message): 59 | prompt("Notify popo users") 60 | print('Skip, todo') 61 | for user in users: 62 | pass 63 | # maybe should not put code here 64 | # print users, message 65 | 66 | 67 | def main(config_file='atx.yml'): 68 | if not os.path.exists(config_file): 69 | sys.exit('config file (%s) not found.' % config_file) 70 | 71 | with open(config_file, 'rb') as f: 72 | cfg = json2obj(yaml.load(f)) 73 | 74 | try: 75 | if hasattr(cfg, 'installation'): 76 | install(cfg.installation) 77 | 78 | if hasattr(cfg, 'script'): 79 | if isinstance(cfg.script, basestring): 80 | scripts = [cfg.script] 81 | else: 82 | scripts = cfg.script 83 | runtest(scripts) 84 | finally: 85 | if hasattr(cfg, 'notification'): 86 | if hasattr(cfg.notification, 'popo'): 87 | notify_popo(cfg.notification.popo, 'hi') 88 | 89 | 90 | if __name__ == '__main__': 91 | main('atx.yml') -------------------------------------------------------------------------------- /atx/cmds/screencap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # 4 | # Usage: python -matx screen [-s 0.8] 5 | from __future__ import absolute_import 6 | 7 | import time 8 | from cStringIO import StringIO 9 | from PIL import Image 10 | 11 | from atx import adbkit 12 | 13 | 14 | def main(host=None, port=None, serial=None, scale=1.0, out='screenshot.png', method='minicap'): 15 | """ 16 | If minicap not avaliable then use uiautomator instead 17 | 18 | Disable scale for now. 19 | Because -s scale is conflict of -s serial 20 | """ 21 | print('Started screencap') 22 | start = time.time() 23 | 24 | client = adbkit.Client(host=host, port=port) 25 | device = client.device(serial) 26 | im = device.screenshot(scale=scale) 27 | im.save(out) 28 | print('Time spend: %.2fs' % (time.time() - start)) 29 | print('File saved to "%s"' % out) 30 | 31 | try: 32 | import win32clipboard 33 | 34 | output = StringIO() 35 | im.convert("RGB").save(output, "BMP") 36 | data = output.getvalue()[14:] 37 | output.close() 38 | 39 | win32clipboard.OpenClipboard() 40 | win32clipboard.EmptyClipboard() 41 | win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data) 42 | win32clipboard.CloseClipboard() 43 | print('Copied to clipboard') 44 | except: 45 | pass # ignore 46 | 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /atx/cmds/screenrecord.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # 4 | # Usage: python -matx screenrecord -o out.avi 5 | 6 | import os 7 | import time 8 | import traceback 9 | import cv2 10 | import numpy as np 11 | 12 | from atx.adbkit.client import Client 13 | from atx.adbkit.device import Device 14 | from atx.adbkit.mixins import MinicapStreamMixin, RotationWatcherMixin 15 | 16 | class AdbWrapper(RotationWatcherMixin, MinicapStreamMixin, Device): 17 | def __init__(self, *args, **kwargs): 18 | super(self.__class__, self).__init__(*args, **kwargs) 19 | self.open_rotation_watcher(on_rotation_change=lambda v: self.open_minicap_stream()) 20 | 21 | def get_adb(host, port, serial): 22 | client = Client(host, port) 23 | if serial is None: 24 | serial = list(client.devices().keys())[0] 25 | return AdbWrapper(client, serial) 26 | 27 | def main(serial=None, host=None, port=None, output='out.avi', scale=0.5, portrait=False, overwrite=True, verbose=True): 28 | if os.path.exists(output): 29 | print 'output file exists!' 30 | if overwrite: 31 | print 'overwriting', output 32 | os.remove(output) 33 | else: 34 | return 35 | 36 | adb = get_adb(host, port, serial) 37 | 38 | img = adb.screenshot_cv2() 39 | while img is None: 40 | time.sleep(1) 41 | img = adb.screenshot_cv2() 42 | if verbose: 43 | cv2.imshow('screen', img) 44 | 45 | w, h, _ = adb.display 46 | w, h = int(w*scale), int(h*scale) 47 | framesize = (w, h) if portrait else (h, w) 48 | fps = 24.0 49 | # refs http://www.fourcc.org/codecs.php 50 | # avaiable fourccs: XVID, MJPG 51 | fourcc = cv2.cv.FOURCC(*'MJPG') 52 | writer = cv2.VideoWriter(output, fourcc, fps, framesize) 53 | 54 | # video (width, height), images should be resized to fit in video frame. 55 | vw, vh = framesize 56 | 57 | tic = time.clock() 58 | toc = time.clock() 59 | while True: 60 | try: 61 | time.sleep(1.0/fps - max(toc-tic, 0)) 62 | tic = time.clock() 63 | img = adb.screenshot_cv2() 64 | h, w = img.shape[:2] 65 | if h*vw == w*vh: 66 | h, w = vh, vw 67 | frame = cv2.resize(img, dsize=(w, h)) 68 | else: 69 | frame = np.zeros((vh, vw, 3), dtype=np.uint8) 70 | sh = vh*1.0/h 71 | sw = vw*1.0/w 72 | if sh < sw: 73 | h, w = vh, int(sh*w) 74 | else: 75 | h, w = int(sw*h), vw 76 | left, top = (vw-w)/2, (vh-h)/2 77 | frame[top:top+h, left:left+w, :] = cv2.resize(img, dsize=(w, h)) 78 | 79 | writer.write(frame) 80 | toc = time.clock() 81 | if verbose: 82 | cv2.imshow('screen', frame) 83 | cv2.waitKey(1) 84 | except KeyboardInterrupt: 85 | print 'Done' 86 | break 87 | except: 88 | traceback.print_exc() 89 | break 90 | writer.release() 91 | 92 | if __name__ == '__main__': 93 | main() -------------------------------------------------------------------------------- /atx/cmds/static/css/index.css: -------------------------------------------------------------------------------- 1 | html, body{ 2 | height: 100%; 3 | } 4 | 5 | .container-full { 6 | margin: 0 auto; 7 | width: 100%; 8 | } 9 | 10 | .phone-screen { 11 | min-height: 8em; 12 | width: 100%; 13 | text-align: center; 14 | } 15 | pre.console { 16 | height: 10em; 17 | overflow: auto; 18 | } 19 | 20 | .point { 21 | position: absolute; 22 | width: 10px; 23 | height: 10px; 24 | margin-left: -5px; 25 | margin-top: -5px; 26 | border-radius: 10px; 27 | border: 1px solid red; 28 | pointer-events: none; 29 | } 30 | 31 | .image-rect, .ui-rect, .image-crop { 32 | position: absolute; 33 | border: 2px solid red; 34 | pointer-events: none; 35 | } 36 | 37 | .clearfix:after { 38 | content: ""; 39 | display: table; 40 | clear: both; 41 | } 42 | 43 | #imagesDiv > ul > li { 44 | float: left; 45 | padding: 10px; 46 | list-style: none; 47 | text-align: center; 48 | height: 140px; 49 | } 50 | 51 | #imagesDiv img { 52 | max-width: 100px; 53 | max-height: 100px; 54 | } 55 | -------------------------------------------------------------------------------- /atx/cmds/static/css/jquery.fancybox.min.css: -------------------------------------------------------------------------------- 1 | /*! fancyBox v2.1.5 fancyapps.com | fancyapps.com/fancybox/#license */.fancybox-wrap,.fancybox-skin,.fancybox-outer,.fancybox-inner,.fancybox-image,.fancybox-wrap iframe,.fancybox-wrap object,.fancybox-nav,.fancybox-nav span,.fancybox-tmp{padding:0;margin:0;border:0;outline:0;vertical-align:top}.fancybox-wrap{position:absolute;top:0;left:0;z-index:8020}.fancybox-skin{position:relative;background:#f9f9f9;color:#444;text-shadow:none;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.fancybox-opened{z-index:8030}.fancybox-opened .fancybox-skin{-webkit-box-shadow:0 10px 25px rgba(0,0,0,0.5);-moz-box-shadow:0 10px 25px rgba(0,0,0,0.5);box-shadow:0 10px 25px rgba(0,0,0,0.5)}.fancybox-outer,.fancybox-inner{position:relative}.fancybox-inner{overflow:hidden}.fancybox-type-iframe .fancybox-inner{-webkit-overflow-scrolling:touch}.fancybox-error{color:#444;font:14px/20px "Helvetica Neue",Helvetica,Arial,sans-serif;margin:0;padding:15px;white-space:nowrap}.fancybox-image,.fancybox-iframe{display:block;width:100%;height:100%}.fancybox-image{max-width:100%;max-height:100%}#fancybox-loading,.fancybox-close,.fancybox-prev span,.fancybox-next span{background-image:url('fancybox_sprite.png')}#fancybox-loading{position:fixed;top:50%;left:50%;margin-top:-22px;margin-left:-22px;background-position:0 -108px;opacity:.8;cursor:pointer;z-index:8060}#fancybox-loading div{width:44px;height:44px;background:url('fancybox_loading.gif') center center no-repeat}.fancybox-close{position:absolute;top:-18px;right:-18px;width:36px;height:36px;cursor:pointer;z-index:8040}.fancybox-nav{position:absolute;top:0;width:40%;height:100%;cursor:pointer;text-decoration:none;background:transparent url('blank.gif');-webkit-tap-highlight-color:rgba(0,0,0,0);z-index:8040}.fancybox-prev{left:0}.fancybox-next{right:0}.fancybox-nav span{position:absolute;top:50%;width:36px;height:34px;margin-top:-18px;cursor:pointer;z-index:8040;visibility:hidden}.fancybox-prev span{left:10px;background-position:0 -36px}.fancybox-next span{right:10px;background-position:0 -72px}.fancybox-nav:hover span{visibility:visible}.fancybox-tmp{position:absolute;top:-99999px;left:-99999px;visibility:hidden;max-width:99999px;max-height:99999px;overflow:visible !important}.fancybox-lock{overflow:hidden !important;width:auto}.fancybox-lock body{overflow:hidden !important}.fancybox-lock-test{overflow-y:hidden !important}.fancybox-overlay{position:absolute;top:0;left:0;overflow:hidden;display:none;z-index:8010;background:url('fancybox_overlay.png')}.fancybox-overlay-fixed{position:fixed;bottom:0;right:0}.fancybox-lock .fancybox-overlay{overflow:auto;overflow-y:scroll}.fancybox-title{visibility:hidden;font:normal 13px/20px "Helvetica Neue",Helvetica,Arial,sans-serif;position:relative;text-shadow:none;z-index:8050}.fancybox-opened .fancybox-title{visibility:visible}.fancybox-title-float-wrap{position:absolute;bottom:0;right:50%;margin-bottom:-35px;z-index:8050;text-align:center}.fancybox-title-float-wrap .child{display:inline-block;margin-right:-100%;padding:2px 20px;background:transparent;background:rgba(0,0,0,0.8);-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;text-shadow:0 1px 2px #222;color:#FFF;font-weight:bold;line-height:24px;white-space:nowrap}.fancybox-title-outside-wrap{position:relative;margin-top:10px;color:#fff}.fancybox-title-inside-wrap{padding-top:10px}.fancybox-title-over-wrap{position:absolute;bottom:0;left:0;color:#fff;padding:10px;background:#000;background:rgba(0,0,0,.8)}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (min--moz-device-pixel-ratio:1.5),only screen and (min-device-pixel-ratio:1.5){#fancybox-loading,.fancybox-close,.fancybox-prev span,.fancybox-next span{background-image:url('fancybox_sprite@2x.png');background-size:44px 152px}#fancybox-loading div{background-image:url('fancybox_loading@2x.gif');background-size:24px 24px}} 2 | -------------------------------------------------------------------------------- /atx/cmds/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/favicon.ico -------------------------------------------------------------------------------- /atx/cmds/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/favicon.png -------------------------------------------------------------------------------- /atx/cmds/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /atx/cmds/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /atx/cmds/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /atx/cmds/static/help.md: -------------------------------------------------------------------------------- 1 | # 备忘 2 | 3 | ## FAQ 4 | 5 | 1. 如何使用代码添加组件到Canvas 6 | 7 | ``` 8 | var xml = Blockly.Xml.textToDom('123'); 9 | Blockly.Xml.domToWorkspace(xml, workspace); 10 | ``` -------------------------------------------------------------------------------- /atx/cmds/static/icons/back.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/back.ico -------------------------------------------------------------------------------- /atx/cmds/static/icons/exit.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/exit.ico -------------------------------------------------------------------------------- /atx/cmds/static/icons/home.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/home.ico -------------------------------------------------------------------------------- /atx/cmds/static/icons/menu.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/menu.ico -------------------------------------------------------------------------------- /atx/cmds/static/icons/power.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/power.ico -------------------------------------------------------------------------------- /atx/cmds/static/icons/rotate.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/rotate.ico -------------------------------------------------------------------------------- /atx/cmds/static/icons/rotate2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/rotate2.ico -------------------------------------------------------------------------------- /atx/cmds/static/icons/save.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/save.ico -------------------------------------------------------------------------------- /atx/cmds/static/icons/volume_down.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/volume_down.ico -------------------------------------------------------------------------------- /atx/cmds/static/icons/volume_up.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/icons/volume_up.ico -------------------------------------------------------------------------------- /atx/cmds/static/js/jquery.mousewheel.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Mousewheel 3.1.13 3 | * 4 | * Copyright 2015 jQuery Foundation and other contributors 5 | * Released under the MIT license. 6 | * http://jquery.org/license 7 | */ 8 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){function b(b){var g=b||window.event,h=i.call(arguments,1),j=0,l=0,m=0,n=0,o=0,p=0;if(b=a.event.fix(g),b.type="mousewheel","detail"in g&&(m=-1*g.detail),"wheelDelta"in g&&(m=g.wheelDelta),"wheelDeltaY"in g&&(m=g.wheelDeltaY),"wheelDeltaX"in g&&(l=-1*g.wheelDeltaX),"axis"in g&&g.axis===g.HORIZONTAL_AXIS&&(l=-1*m,m=0),j=0===m?l:m,"deltaY"in g&&(m=-1*g.deltaY,j=m),"deltaX"in g&&(l=g.deltaX,0===m&&(j=-1*l)),0!==m||0!==l){if(1===g.deltaMode){var q=a.data(this,"mousewheel-line-height");j*=q,m*=q,l*=q}else if(2===g.deltaMode){var r=a.data(this,"mousewheel-page-height");j*=r,m*=r,l*=r}if(n=Math.max(Math.abs(m),Math.abs(l)),(!f||f>n)&&(f=n,d(g,n)&&(f/=40)),d(g,n)&&(j/=40,l/=40,m/=40),j=Math[j>=1?"floor":"ceil"](j/f),l=Math[l>=1?"floor":"ceil"](l/f),m=Math[m>=1?"floor":"ceil"](m/f),k.settings.normalizeOffset&&this.getBoundingClientRect){var s=this.getBoundingClientRect();o=b.clientX-s.left,p=b.clientY-s.top}return b.deltaX=l,b.deltaY=m,b.deltaFactor=f,b.offsetX=o,b.offsetY=p,b.deltaMode=0,h.unshift(b,j,l,m),e&&clearTimeout(e),e=setTimeout(c,200),(a.event.dispatch||a.event.handle).apply(this,h)}}function c(){f=null}function d(a,b){return k.settings.adjustOldDeltas&&"mousewheel"===a.type&&b%120===0}var e,f,g=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],h="onwheel"in document||document.documentMode>=9?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],i=Array.prototype.slice;if(a.event.fixHooks)for(var j=g.length;j;)a.event.fixHooks[g[--j]]=a.event.mouseHooks;var k=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var c=h.length;c;)this.addEventListener(h[--c],b,!1);else this.onmousewheel=b;a.data(this,"mousewheel-line-height",k.getLineHeight(this)),a.data(this,"mousewheel-page-height",k.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var c=h.length;c;)this.removeEventListener(h[--c],b,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(b){var c=a(b),d=c["offsetParent"in a.fn?"offsetParent":"parent"]();return d.length||(d=a("body")),parseInt(d.css("fontSize"),10)||parseInt(c.css("fontSize"),10)||16},getPageHeight:function(b){return a(b).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};a.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})}); -------------------------------------------------------------------------------- /atx/cmds/static/recorder.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/cmds/static/recorder.ico -------------------------------------------------------------------------------- /atx/cmds/tcpproxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import print_function 5 | 6 | import sys 7 | try: 8 | import maproxy.proxyserver 9 | except: 10 | sys.exit("Require maproxy installed. Run: pip install maproxy") 11 | 12 | import tornado.ioloop 13 | import socket 14 | 15 | 16 | def main(forward=26944, host='127.0.0.1', listen=5555): 17 | ''' 18 | Args: 19 | - forward(int): local forward port 20 | - host(string): local forward host 21 | - listen(int): listen port 22 | ''' 23 | # HTTP->HTTP: On your computer, browse to "http://127.0.0.1:81/" and you'll get http://www.google.com 24 | server = maproxy.proxyserver.ProxyServer("127.0.0.1", forward) 25 | server.listen(listen) 26 | print("Local IP:", socket.gethostbyname(socket.gethostname())) 27 | print("0.0.0.0:{} -> {}:{}".format(listen, host, forward)) 28 | tornado.ioloop.IOLoop.instance().start() 29 | 30 | if __name__ == '__main__': 31 | main() -------------------------------------------------------------------------------- /atx/cmds/utils.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import requests 5 | 6 | 7 | def http_download(url, target_path): 8 | """Download file to local 9 | Args: 10 | - url(string): url request path 11 | - target_path(string): download destination 12 | """ 13 | r = requests.get(url, stream=True) 14 | with open(target_path, 'wb') as f: 15 | # shutil.copyfileobj(resp, f) 16 | for chunk in r.iter_content(chunk_size=1024): 17 | if chunk: 18 | f.write(chunk) 19 | return target_path 20 | -------------------------------------------------------------------------------- /atx/comtools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | 7 | import time 8 | 9 | 10 | class CountdownTimer(object): 11 | def __init__(self, timeout): 12 | self._timeout = timeout 13 | self._timeend = time.time() + timeout 14 | 15 | def reset(self, timeout=None): 16 | if timeout: 17 | self._timeout = timeout 18 | self._timeend = time.time() + self._timeout 19 | 20 | def ticking(self): 21 | return time.time() < self._timeend -------------------------------------------------------------------------------- /atx/consts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | SCREENSHOT_METHOD_UIAUTOMATOR = 'uiautomator' 5 | SCREENSHOT_METHOD_MINICAP = 'minicap' 6 | SCREENSHOT_METHOD_AUTO = 'auto' 7 | 8 | IMAGE_MATCH_METHOD_TMPL = 'template' 9 | IMAGE_MATCH_METHOD_SIFT = 'sift' 10 | IMAGE_MATCH_METHOD_AUTO = 'auto' 11 | 12 | EVENT_UIAUTO_TOUCH = 1 << 0 13 | EVENT_UIAUTO_CLICK = 1 << 0 # alias for touch 14 | EVENT_UIAUTO_SWIPE = 1 << 2 15 | 16 | EVENT_SCREENSHOT = 1 << 3 17 | EVENT_CLICK = 1 << 4 18 | EVENT_CLICK_IMAGE = 1 << 5 19 | EVENT_ASSERT_EXISTS = 1 << 6 # Deprecated 20 | 21 | EVENT_ALL = EVENT_SCREENSHOT | EVENT_CLICK | EVENT_CLICK_IMAGE 22 | # 1 - 2 - 3 23 | # 4 - 5 - 6 24 | # 7 - 8 - 9 25 | 26 | NW = 1 27 | N = 2 28 | NE = 3 29 | W = 4 30 | E = 6 31 | SW = 7 32 | S = 8 33 | SE = 9 34 | -------------------------------------------------------------------------------- /atx/drivers/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import re 6 | import collections 7 | 8 | import six 9 | from atx import imutils 10 | from atx import strutils 11 | 12 | 13 | FindPoint = collections.namedtuple('FindPoint', ['pos', 'confidence', 'method', 'matched']) 14 | Display = collections.namedtuple('Display', ['width', 'height']) 15 | 16 | 17 | __boundstuple = collections.namedtuple('Bounds', ['left', 'top', 'right', 'bottom']) 18 | 19 | class Bounds(__boundstuple): 20 | def __init__(self, *args, **kwargs): 21 | self._area = None 22 | 23 | def is_inside(self, x, y): 24 | v = self 25 | return x > v.left and x < v.right and y > v.top and y < v.bottom 26 | 27 | @property 28 | def area(self): 29 | if not self._area: 30 | v = self 31 | self._area = (v.right-v.left) * (v.bottom-v.top) 32 | return self._area 33 | 34 | @property 35 | def center(self): 36 | v = self 37 | return (v.left+v.right)/2, (v.top+v.bottom)/2 38 | 39 | def __mul__(self, mul): 40 | return Bounds(*(int(v*mul) for v in self)) 41 | 42 | class ImageCrop(object): 43 | def __init__(self, src, bound): 44 | self.src = src 45 | l, t, w, h = bound 46 | self.bound = Bounds(l, t, l+w, t+h) 47 | 48 | class Pattern(object): 49 | def __init__(self, name, image=None, offset=None, anchor=0, rsl=None, resolution=None, th=None, threshold=None): 50 | """ 51 | Args: 52 | name: image filename 53 | image: opencv image object 54 | offset: offset of image center 55 | anchor: not supported 56 | resolution: image origin screen resolution 57 | rsl: alias of resolution 58 | threshold: image match threshold, usally (0, 1] 59 | th: alias of threshold 60 | """ 61 | if isinstance(name, ImageCrop): 62 | self._name = name.src 63 | self._bound = name.bound 64 | else: 65 | self._name = name 66 | self._bound = None 67 | 68 | self._image = image # if image is None, it will delay to pattern_open function 69 | self._offset = offset 70 | self._resolution = rsl or resolution 71 | self._threshold = th or threshold 72 | if isinstance(image, six.string_types): 73 | self._name = image 74 | 75 | # search format name.1080x1920.png 76 | if self._resolution is None: 77 | m = re.search(r'\.(\d+)x(\d+)\.', self._name) 78 | if m: 79 | (w, h) = sorted(map(int, (m.group(1), m.group(2)))) 80 | # TODO(ssx): gcd(w, h), make sure the biggest < 20 81 | self._resolution = (w, h) 82 | 83 | if self._offset is None: 84 | m = re.search(r'\.([LRTB])(\d+)([LRTB])(\d+)\.', self._name) 85 | if m: 86 | offx, offy = 0, 0 87 | for i in (1, 3): 88 | flag, number = m.group(i), int(m.group(i+1)) 89 | if flag in ('L', 'R'): 90 | offx = number/100.0 * (1 if flag == 'R' else -1) 91 | if flag in ('T', 'B'): 92 | offy = number/100.0 * (1 if flag == 'B' else -1) 93 | self._offset = (offx, offy) 94 | 95 | def __str__(self): 96 | return 'Pattern(name: {}, offset: {})'.format(strutils.encode(self._name), self.offset) 97 | 98 | def save(self, path): 99 | """ save image to path """ 100 | import cv2 101 | cv2.imwrite(path, self._image) 102 | 103 | @property 104 | def image(self): 105 | if self._bound is None: 106 | return self._image 107 | else: 108 | return imutils.crop(self._image, *self._bound) 109 | 110 | @property 111 | def offset(self): 112 | return self._offset 113 | 114 | @property 115 | def resolution(self): 116 | return self._resolution 117 | 118 | @property 119 | def threshold(self): 120 | return self._threshold 121 | -------------------------------------------------------------------------------- /atx/drivers/dummy.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # dummy device is used for test 4 | 5 | from __future__ import absolute_import 6 | from __future__ import print_function 7 | 8 | import os 9 | 10 | from PIL import Image 11 | 12 | from atx.drivers.mixin import DeviceMixin, hook_wrap 13 | from atx.drivers import Display 14 | from atx import consts 15 | 16 | 17 | __dir__ = os.path.dirname(os.path.abspath(__file__)) 18 | 19 | class DummyDevice(DeviceMixin): 20 | def __init__(self, *args, **kwargs): 21 | DeviceMixin.__init__(self) 22 | self._display = Display(1280, 720) 23 | self._rotation = 1 24 | self.last_click = None 25 | self.serial = '1234' 26 | self._fail_first_screenshot = False 27 | 28 | def _take_screenshot(self): 29 | """ Take a screenshot """ 30 | # screen size: 1280x720 31 | if self._fail_first_screenshot: 32 | self._fail_first_screenshot = False 33 | raise IOError("dummy fail screenshot") 34 | screen_path = os.path.join(__dir__, '../../tests/media/dummy_screen.png') 35 | screen = Image.open(screen_path) 36 | return screen 37 | 38 | @property 39 | def display(self): 40 | return self._display 41 | 42 | @property 43 | def rotation(self): 44 | return self._rotation 45 | 46 | def click(self, x, y): 47 | self.last_click = (x, y) -------------------------------------------------------------------------------- /atx/drivers/ios_uiautomation/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # License under MIT 5 | 6 | 7 | from __future__ import absolute_import 8 | 9 | import os 10 | import json 11 | import time 12 | 13 | import yaml 14 | import subprocess32 as subprocess 15 | from PIL import Image 16 | 17 | from atx import consts 18 | from atx import errors 19 | from atx import patch 20 | from atx import base 21 | from atx import imutils 22 | from atx import strutils 23 | from atx import logutils 24 | from atx import ioskit 25 | from atx.drivers import Bounds, Display 26 | from atx.drivers.mixin import DeviceMixin, hook_wrap 27 | 28 | 29 | __dir__ = os.path.dirname(os.path.abspath(__file__)) 30 | log = logutils.getLogger(__name__) 31 | 32 | class IOSDevice(DeviceMixin): 33 | def __init__(self, bundle_id=None, udid=None): 34 | DeviceMixin.__init__(self) 35 | 36 | self.d = ioskit.Device(udid) 37 | self.udid = self.d.udid 38 | 39 | self._proc = None 40 | self._display = None #Display(2208, 1242) 41 | self._scale = 1 42 | self._env = os.environ.copy() 43 | self._init_display() 44 | 45 | self.screen_rotation = 1 # TODO: auto judge 46 | 47 | if not bundle_id: 48 | print 'WARNING [ios.py]: bundle_id is not set' #, use "com.netease.atx.apple" instead.' 49 | # self._init_instruments('com.netease.atx.apple') 50 | else: 51 | self._init_instruments(bundle_id) 52 | 53 | def _init_display(self): 54 | model = self.d.info['HardwareModel'] 55 | with open(os.path.join(__dir__, 'ios-models.yml'), 'rb') as f: 56 | items = yaml.load(f.read()) 57 | for item in items: 58 | if model == item.get('model'): 59 | (width, height) = map(int, item.get('pixel').split('x')) 60 | self._scale = item.get('scale') 61 | self._display = Display(width*self._scale, height*self._scale) 62 | break 63 | if self._display is None: 64 | raise RuntimeError("TODO: not support your phone for now, You need contact the author.") 65 | 66 | def _init_instruments(self, bundle_id): 67 | self._bootstrap = os.path.join(__dir__, 'bootstrap.sh') 68 | self._bundle_id = bundle_id 69 | self._env.update({'UDID': self.udid, 'BUNDLE_ID': self._bundle_id}) 70 | # 1. remove pipe 71 | # subprocess.check_output([self._bootstrap, 'reset'], env=self._env) 72 | # 2. start instruments 73 | self._proc = subprocess.Popen([self._bootstrap, 'instruments'], env=self._env, stdout=subprocess.PIPE) 74 | self.sleep(5.0) 75 | self._wait_instruments() 76 | 77 | def _wait_instruments(self): 78 | ret = self._run('1') 79 | if ret != 1: 80 | log.error('Instruments stdout:\n' + self._proc.stdout.read()) 81 | raise RuntimeError('Instruments start failed, expect 1 but got %s' % (ret,)) 82 | 83 | def _run(self, code): 84 | # print self._proc.poll() 85 | # print code 86 | encoded_code = json.dumps({'command': code}) 87 | output = subprocess.check_output([self._bootstrap, 'run', encoded_code], env=self._env) 88 | # print output 89 | try: 90 | return json.loads(output) 91 | except: 92 | print 'unknown json output:', output 93 | return output 94 | 95 | def _run_nowait(self, code): 96 | ''' TODO: change to no wait ''' 97 | print self._proc.poll() 98 | encoded_code = json.dumps({'command': code, 'nowait': True}) 99 | output = subprocess.check_output([self._bootstrap, 'run', '--nowait', encoded_code], env=self._env) 100 | return output 101 | 102 | def _close(self): 103 | print 'Terminate instruments' 104 | if self._proc: 105 | self._proc.terminate() 106 | # 1. remove pipe 107 | subprocess.check_output([self._bootstrap, 'reset'], env=self._env) 108 | 109 | def __del__(self): 110 | if hasattr(self, '_bootstrap'): 111 | self._close() 112 | 113 | @property 114 | def rotation(self): 115 | return self.screen_rotation 116 | 117 | @property 118 | def display(self): 119 | return self._display 120 | 121 | @property 122 | def info(self): 123 | return self.d.info 124 | 125 | def screenshot(self, filename=None): 126 | ''' 127 | Take ios screenshot 128 | Args: 129 | - filename(string): optional 130 | Returns: 131 | PIL.Image object 132 | ''' 133 | image = self.d.screenshot() 134 | if self.rotation: 135 | method = getattr(Image, 'ROTATE_{}'.format(self.rotation*90)) 136 | image = image.transpose(method) 137 | if filename: 138 | image.save(filename) 139 | return image 140 | 141 | def click(self, x, y): 142 | ''' 143 | Simulate click operation 144 | Args: 145 | - x (int): position of x 146 | - y (int): position of y 147 | Returns: 148 | self 149 | ''' 150 | self._run_nowait('target.tap({x: %d, y: %d})' % (x/self._scale, y/self._scale)) 151 | return self 152 | 153 | def install(self, filepath): 154 | self.d.install(filepath) 155 | 156 | def sleep(self, sec): 157 | self.delay(sec) 158 | 159 | def type(self, text): 160 | self._run_nowait('$.typeString(%s)' % json.dumps(text)) 161 | 162 | def start_app(self, bundle_id): 163 | self.d.start_app(bundle_id) 164 | 165 | def current_app(self): 166 | ''' todo, maybe return dict is a better way ''' 167 | return self._run('target.frontMostApp().bundleID()').strip().strip('"') 168 | -------------------------------------------------------------------------------- /atx/drivers/ios_uiautomation/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | # 3 | 4 | set -e 5 | WORKDIR=$PWD 6 | cd $(dirname $0) 7 | 8 | export PATH="/usr/local/bin":$PATH 9 | 10 | BUNDLE_ID=${BUNDLE_ID:?} 11 | UDID=${UDID:-$(idevice_id -l)} 12 | TEST="./instruments-test.js" 13 | 14 | TRACETEMPLATE="/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.xrplugin/Contents/Resources/Automation.tracetemplate" 15 | 16 | SOCKPATH="/tmp/atx-taskqueue.sock" 17 | QUEUE="/usr/local/bin/python -m atx.taskqueue --unix $SOCKPATH --room $UDID" 18 | 19 | test -d $RESULTPATH || mkdir $RESULTPATH 20 | case "$1" in 21 | instruments) 22 | echo "Start python taskqueue" 23 | python -m atx.taskqueue web &>/tmp/atx-taskqueue.log & 24 | echo "Start instruments" 25 | exec instruments -w ${UDID:?} -t "Automation" -D $WORKDIR/cli.trace $BUNDLE_ID -e UIASCRIPT $TEST &>/tmp/atx-instruments.log # -e UIARESULTSPATH $RESULTPATH 26 | #exec instruments -w ${UDID:?} -t "$TRACETEMPLATE" $BUNDLE_ID -e UIASCRIPT $TEST # -e UIARESULTSPATH $RESULTPATH 27 | ;; 28 | run) 29 | shift 30 | NOWAIT= 31 | if test "$1" = "--nowait" 32 | then 33 | shift 34 | NOWAIT=true 35 | fi 36 | TASK_ID=$($QUEUE post "$1") 37 | if test -z "$NOWAIT" 38 | then 39 | exec $QUEUE retr "$TASK_ID" 40 | fi 41 | ;; 42 | get) 43 | shift 44 | # use curl instead of python -m atx.taskqueue get 45 | curl -ss --unix-socket $SOCKPATH -X GET http:/rooms/$UDID 46 | ;; 47 | put) 48 | shift 49 | # $1: task_id, $2: data 50 | # echo "{\"id\": \"$1\", \"result\": $2}" | curl -ss --unix-socket $SOCKPATH -X POST http:/rooms/$UDID 51 | $QUEUE put "$1" "$2" 52 | ;; 53 | reset) 54 | # todo, maybe quit too much 55 | # $QUEUE quit 56 | $QUEUE clean 57 | ;; 58 | test) 59 | $0 run '{"command": 1}' 60 | ;; 61 | *) 62 | /bin/echo "Usage: $0 [ARGS]" 63 | ;; 64 | esac 65 | 66 | -------------------------------------------------------------------------------- /atx/drivers/ios_uiautomation/instruments-test.js: -------------------------------------------------------------------------------- 1 | #import "mechanic.js" 2 | 3 | var $ = $ || mechanic; 4 | 5 | (function($) { 6 | var target = UIATarget.localTarget(); 7 | var app = target.frontMostApp(); 8 | 9 | $.extend($, { 10 | debug: UIALogger.logMessage, 11 | cmd: function(path, args, timeout) { 12 | return target.host().performTaskWithPathArgumentsTimeout(path, args, timeout); 13 | }, 14 | delay: function(seconds) { 15 | target.delay(seconds); 16 | }, 17 | orientation: function(orientation) { 18 | if (orientation === undefined || orientation === null) return target.deviceOrientation(); 19 | else target.setDeviceOrientation(orientation); 20 | }, 21 | }) 22 | 23 | 24 | $.extend($, { 25 | error: function(s) { 26 | UIALogger.logError(s); 27 | }, 28 | warn: function(s) { 29 | UIALogger.logWarning(s); 30 | }, 31 | debug: function(s) { 32 | UIALogger.logDebug(s); 33 | }, 34 | message: function(s) { 35 | UIALogger.logMessage(s); 36 | }, 37 | rotate: function(options) { 38 | target.rotateWithOptions(options); 39 | }, 40 | typeString: function(str){ 41 | target.frontMostApp().keyboard().typeString(str); 42 | } 43 | }) 44 | })(mechanic); 45 | 46 | String.prototype.trim = function(char, type) { 47 | if (char) { 48 | if (type == 'left') { 49 | return this.replace(new RegExp('^\\' + char + '+', 'g'), ''); 50 | } else if (type == 'right') { 51 | return this.replace(new RegExp('\\' + char + '+$', 'g'), ''); 52 | } 53 | return this.replace(new RegExp('^\\' + char + '+|\\' + char + '+$', 'g'), ''); 54 | } 55 | return this.replace(/^\s+|\s+$/g, ''); 56 | }; 57 | 58 | var target = UIATarget.localTarget(); 59 | var app = target.frontMostApp(); 60 | 61 | // $.debug("Hello" + JSON.stringify(target.rect())) 62 | $.message("Instruments is ready") 63 | 64 | while (true) { 65 | $.message("Wait for command") 66 | var result = $.cmd('./bootstrap.sh', ['get'], 50); 67 | if (!result.stdout.trim()) { // == 15) { 68 | continue; 69 | } 70 | $.debug("exitCode: " + result.exitCode); 71 | $.debug("stdout: " + result.stdout); 72 | $.debug("stderr: " + result.stderr); 73 | // $.message("delay 1s") 74 | // $.delay(1) 75 | 76 | if (result.exitCode !== 0) { 77 | continue 78 | } 79 | 80 | try { 81 | var req = JSON.parse(result.stdout); 82 | if (!req.id){ 83 | $.warn("Reqest need ID"); 84 | continue 85 | } 86 | var rawRes = eval(req.data.command); 87 | if (req.data.nowait) { 88 | continue; 89 | } 90 | 91 | var res = JSON.stringify(rawRes); 92 | $.debug("Result: " + res); 93 | var ret = $.cmd('./bootstrap.sh', ['put', req.id, res], 5); 94 | $.debug("Result exitCode: " + ret.exitCode); 95 | $.debug("Result stdout: " + ret.stdout); 96 | $.debug("Result stderr: " + ret.stderr); 97 | } catch (err) { 98 | $.error("Error: " + err.message); 99 | // $.cmd('./bootstrap.sh', ['put', req.id, JSON.stringify("error:" + err.message)], 5); 100 | } 101 | } -------------------------------------------------------------------------------- /atx/drivers/ios_uiautomation/ios-models.yml: -------------------------------------------------------------------------------- 1 | # most from ios-deploy source code 2 | # 3 | # - name (string): name of apple device 4 | # - pixel (string): ${width}x${height} 5 | # - scale (int): scale factor 6 | --- 7 | - name: iPhone 6 Plus 8 | model: N56AP 9 | pixel: 414x736 10 | scale: 3 11 | - name: iPhone 5c (GSM) 12 | model: N48AP 13 | pixel: 320x568 14 | scale: 2 15 | - name: iPhone 5c (Global/CDMA) 16 | model: N49AP 17 | pixel: 320x568 18 | scale: 2 -------------------------------------------------------------------------------- /atx/drivers/ios_uiautomation/mechanic.js: -------------------------------------------------------------------------------- 1 | /* The usage is some like jQuery */ 2 | 3 | var mechanic = (function() { 4 | var target = UIATarget.localTarget(); 5 | var app = target.frontMostApp(), 6 | window = app.mainWindow(), 7 | emptyArray = [], 8 | slice = emptyArray.slice; 9 | 10 | target.setTimeout(0); 11 | 12 | function $() {} 13 | 14 | $.extend = function(target) { 15 | var key; 16 | slice.call(arguments, 1).forEach(function(source) { 17 | for (key in source) target[key] = source[key]; 18 | }); 19 | return target; 20 | }; 21 | 22 | $.inArray = function(elem, array, i) { 23 | return emptyArray.indexOf.call(array, elem, i); 24 | }; 25 | 26 | $.map = function(elements, callback) { 27 | var value, values = [], 28 | i, key; 29 | if (likeArray(elements)) { 30 | for (i = 0; i < elements.length; i++) { 31 | value = callback(elements[i], i); 32 | if (value != null) values.push(value); 33 | } 34 | } else { 35 | for (key in elements) { 36 | value = callback(elements[key], key); 37 | if (value != null) values.push(value); 38 | } 39 | } 40 | return flatten(values); 41 | }; 42 | 43 | $.each = function(elements, callback) { 44 | var i, key; 45 | if (likeArray(elements)) { 46 | for (i = 0; i < elements.length; i++) { 47 | if (callback.call(elements[i], i, elements[i]) === false) return elements; 48 | } 49 | } else { 50 | for (key in elements) { 51 | if (callback.call(elements[key], key, elements[key]) === false) return elements; 52 | } 53 | } 54 | return elements; 55 | }; 56 | 57 | return $; 58 | })(); 59 | -------------------------------------------------------------------------------- /atx/drivers/webdriver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import 5 | from __future__ import print_function 6 | 7 | import base64 8 | import json 9 | import os 10 | import time 11 | import urlparse 12 | from collections import namedtuple 13 | 14 | import requests 15 | from PIL import Image 16 | from StringIO import StringIO 17 | 18 | from atx.drivers.mixin import DeviceMixin, hook_wrap 19 | from atx.drivers import Display 20 | from atx import consts 21 | 22 | 23 | DEBUG = False 24 | 25 | def convert(dictionary): 26 | """ 27 | Convert dict to namedtuple 28 | """ 29 | return namedtuple('GenericDict', dictionary.keys())(**dictionary) 30 | 31 | 32 | def urljoin(*urls): 33 | """ 34 | The default urlparse.urljoin behavior look strange 35 | Standard urlparse.urljoin('http://a.com/foo', '/bar') 36 | Expect: http://a.com/foo/bar 37 | Actually: http://a.com/bar 38 | 39 | This function fix that. 40 | """ 41 | return reduce(urlparse.urljoin, [u.strip('/')+'/' for u in urls if u.strip('/')], '').rstrip('/') 42 | 43 | 44 | class WebDriverError(Exception): 45 | def __init__(self, status, value): 46 | self.status = status 47 | self.value = value 48 | 49 | def __str__(self): 50 | return 'WebDriverError(status=%d, value=%s)' % (self.status, self.value) 51 | 52 | 53 | def httpdo(method, url, data=None): 54 | """ 55 | Do HTTP Request 56 | """ 57 | if isinstance(data, dict): 58 | data = json.dumps(data) 59 | if DEBUG: 60 | print "Shell: curl -X {method} -d '{data}' '{url}'".format(method=method, data=data or '', url=url) 61 | 62 | fn = dict(GET=requests.get, POST=requests.post, DELETE=requests.delete)[method] 63 | response = fn(url, data=data) 64 | retjson = response.json() 65 | if DEBUG: 66 | print 'Return:', json.dumps(retjson, indent=4) 67 | r = convert(retjson) 68 | if r.status != 0: 69 | raise WebDriverError(r.status, r.value) 70 | return r 71 | 72 | 73 | class _Client(object): 74 | def __init__(self, device_url): 75 | self.__device_url = device_url 76 | 77 | def screenshot(self): 78 | """Take screenshot 79 | Return: 80 | PIL.Image 81 | """ 82 | url = urljoin(self.__device_url, "screenshot") 83 | r = httpdo('GET', url) 84 | raw_image = base64.b64decode(r.value) 85 | return Image.open(StringIO(raw_image)) 86 | 87 | 88 | class WebDriver(DeviceMixin): 89 | def __init__(self, device_url): 90 | DeviceMixin.__init__(self) 91 | self.__display = None 92 | self._ymc = _Client(device_url) 93 | 94 | @hook_wrap(consts.EVENT_SCREENSHOT) 95 | def screenshot(self, filename=None): 96 | """ Take a screenshot """ 97 | # screen size: 1280x720 98 | screen = self._ymc.screenshot() 99 | if filename: 100 | screen.save(filename) 101 | return screen 102 | 103 | def start_app(self, bundle_id): 104 | pass 105 | 106 | def click(self, x, y): 107 | pass 108 | -------------------------------------------------------------------------------- /atx/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | 6 | 7 | class Error(Exception): 8 | def __init__(self, message, data=None): 9 | self.message = message 10 | self.data = data 11 | 12 | def __str__(self): 13 | if self.data: 14 | return '{}, data: {}'.format(self.message, self.data) 15 | return self.message 16 | 17 | def __repr__(self): 18 | return repr(self.message) 19 | 20 | 21 | class WindowsAppNotFoundError(Error): 22 | pass 23 | 24 | 25 | class ImageNotFoundError(Error): 26 | pass 27 | 28 | class WatchTimeoutError(Error): 29 | pass 30 | 31 | class AssertError(Error): 32 | pass 33 | 34 | class AssertExistsError(AssertError): 35 | pass 36 | -------------------------------------------------------------------------------- /atx/ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/ext/__init__.py -------------------------------------------------------------------------------- /atx/ext/chromedriver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # extension for https://sites.google.com/a/chromium.org/chromedriver/ 5 | # Experimental, maybe change in the future 6 | # Created by 2017-01-20 7 | 8 | from __future__ import absolute_import 9 | 10 | 11 | import atexit 12 | import six 13 | from selenium import webdriver 14 | 15 | if six.PY3: 16 | import subprocess 17 | from urllib.error import URLError 18 | else: 19 | from urllib2 import URLError 20 | import subprocess32 as subprocess 21 | 22 | 23 | class ChromeDriver(object): 24 | def __init__(self, d, port=9515): 25 | self._d = d 26 | self._port = port 27 | 28 | def _launch_webdriver(self): 29 | print("start chromedriver instance") 30 | p = subprocess.Popen(['chromedriver', '--port='+str(self._port)]) 31 | try: 32 | p.wait(timeout=2.0) 33 | return False 34 | except subprocess.TimeoutExpired: 35 | return True 36 | 37 | def driver(self, package=None, attach=True, activity=None, process=None): 38 | """ 39 | Args: 40 | - package(string): default current running app 41 | - attach(bool): default true, Attach to an already-running app instead of launching the app with a clear data directory 42 | - activity(string): Name of the Activity hosting the WebView. 43 | - process(string): Process name of the Activity hosting the WebView (as given by ps). 44 | If not given, the process name is assumed to be the same as androidPackage. 45 | 46 | Returns: 47 | selenium driver 48 | """ 49 | app = self._d.current_app() 50 | capabilities = { 51 | 'chromeOptions': { 52 | 'androidDeviceSerial': self._d.serial, 53 | 'androidPackage': package or app.package, 54 | 'androidUseRunningApp': attach, 55 | 'androidProcess': process or app.package, 56 | 'androidActivity': activity or app.activity, 57 | } 58 | } 59 | 60 | try: 61 | dr = webdriver.Remote('http://localhost:%d' % self._port, capabilities) 62 | except URLError: 63 | self._launch_webdriver() 64 | dr = webdriver.Remote('http://localhost:%d' % self._port, capabilities) 65 | 66 | # always quit driver when done 67 | atexit.register(dr.quit) 68 | return dr 69 | 70 | def windows_kill(self): 71 | subprocess.call(['taskkill', '/F', '/IM', 'chromedriver.exe', '/T']) 72 | 73 | 74 | if __name__ == '__main__': 75 | import atx 76 | d = atx.connect() 77 | driver = ChromeDriver(d).driver() 78 | elem = driver.find_element_by_link_text(u"登录") 79 | elem.click() 80 | driver.quit() 81 | -------------------------------------------------------------------------------- /atx/ext/gt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # extention for http://gt.qq.com/ 5 | # reference doc http://gt.qq.com/docs/a/UseGtWithBroadcast.txt 6 | # 7 | # Experimental, maybe change in the future 8 | # Created by 2016-06-12 9 | 10 | import functools 11 | 12 | 13 | class GT(object): 14 | def __init__(self, d): 15 | self.d = d 16 | self._broadcast = functools.partial(self.d.adb_device.shell, 'am', 'broadcast', '-a') 17 | self._package_name = None 18 | 19 | def start_test(self, package_name, cpu=True, net=True, pss=True): 20 | self._package_name = package_name 21 | 22 | broadcast = self._broadcast 23 | # 1. start app 24 | self.quit() # reset gt 25 | self.d.start_app('com.tencent.wstt.gt')#, 'com.tencent.wstt.gt.activity.GTMainActivity') 26 | # 2. set test package name 27 | broadcast('com.tencent.wstt.gt.baseCommand.startTest', '--es', 'pkgName', package_name) 28 | # 3. set collect params 29 | if cpu: 30 | broadcast('com.tencent.wstt.gt.baseCommand.sampleData', '--ei', 'cpu', '1') 31 | if net: 32 | broadcast('com.tencent.wstt.gt.baseCommand.sampleData', '--ei', 'net', '1') 33 | if pss: 34 | broadcast('com.tencent.wstt.gt.baseCommand.sampleData', '--ei', 'pss', '1') 35 | 36 | # 4. switch back to app 37 | self.d.start_app(package_name) 38 | 39 | def stop_and_save(self): 40 | self._broadcast('com.tencent.wstt.gt.baseCommand.endTest', '--es', 'saveFolderName', self._package_name, 41 | '--es', 'desc', 'Result_of_GT') 42 | print 'Run\n$ adb pull /sdcard/GT/GW/{pkgname}/{version}/{pkgname}'.format(pkgname=self._package_name, version='unknow') 43 | 44 | def quit(self): 45 | self._broadcast('com.tencent.wstt.gt.baseCommand.exitGT') 46 | -------------------------------------------------------------------------------- /atx/ext/report/README.md: -------------------------------------------------------------------------------- 1 | # Report 2 | 利用此插件可以在ATX自动化跑完之后,自动生成HTML报告,方便查看每一步的执行情况 3 | 4 | ## Usage 5 | ```py 6 | import atx 7 | from atx.ext.report import Report # report lib 8 | 9 | 10 | d = atx.connect() 11 | rp = Report(d, save_dir='report') 12 | rp.patch_uiautomator() # for android UI test record (optional) 13 | 14 | rp.info("Test started") # or rp.info("Test started", screenshot=d.screenshot()) 15 | d.click(200, 200) 16 | 17 | # keep screenshot when test fails 18 | rp.error("Oh no.", screenshot=d.screenshot()) 19 | 20 | # assert operations 21 | # param screenshot can be type 22 | # set to None means only take screenshot when assert fails. 23 | # this is also same to other assert functions 24 | rp.assert_equal(1, 2, desc="Hi", screenshot=True, safe=True) 25 | 26 | # assert android ui exists 27 | rp.assert_ui_exists(d(text='Hello'), desc='Hello UI') 28 | 29 | # assert image exists 30 | rp.assert_image_exists('button.png', timeout=10.0, safe=True) 31 | 32 | # close and generate report 33 | rp.close() 34 | ``` 35 | 36 | After done, HTML report will be saved to report dir. with such directory 37 | 38 | ``` 39 | report/ 40 | |-- index.html 41 | |-- result.json 42 | `-- images/ 43 | |-- before_123123123123.png 44 | |-- ... 45 | ``` 46 | 47 | open `index.html` with browser. 48 | 49 | ![report](report.png) -------------------------------------------------------------------------------- /atx/ext/report/patch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Reference 6 | https://github.com/gevent/gevent/blob/master/src/gevent/monkey.py 7 | """ 8 | 9 | from __future__ import absolute_import 10 | import sys 11 | 12 | if sys.version_info[0] >= 3: 13 | string_types = str, 14 | PY3 = True 15 | else: 16 | import __builtin__ # pylint:disable=import-error 17 | string_types = __builtin__.basestring, 18 | PY3 = False 19 | 20 | 21 | # maps module name -> {attribute name: original item} 22 | # e.g. "time" -> {"sleep": built-in function sleep} 23 | saved = {} 24 | 25 | 26 | def is_module_patched(modname): 27 | """Check if a module has been replaced with a cooperative version.""" 28 | return modname in saved 29 | 30 | 31 | def is_object_patched(modname, objname): 32 | """Check if an object in a module has been replaced with a cooperative version.""" 33 | return is_module_patched(modname) and objname in saved[modname] 34 | 35 | 36 | def _get_original(name, items): 37 | d = saved.get(name, {}) 38 | values = [] 39 | module = None 40 | for item in items: 41 | if item in d: 42 | values.append(d[item]) 43 | else: 44 | if module is None: 45 | module = __import__(name) 46 | values.append(getattr(module, item)) 47 | return values 48 | 49 | 50 | def get_original(mod_name, item_name): 51 | """Retrieve the original object from a module. 52 | If the object has not been patched, then that object will still be retrieved. 53 | :param item_name: A string or sequence of strings naming the attribute(s) on the module 54 | ``mod_name`` to return. 55 | :return: The original value if a string was given for ``item_name`` or a sequence 56 | of original values if a sequence was passed. 57 | """ 58 | if isinstance(item_name, string_types): 59 | return _get_original(mod_name, [item_name])[0] 60 | else: 61 | return _get_original(mod_name, item_name) 62 | 63 | _NONE = object() 64 | 65 | 66 | def patch_item(module, attr, newitem): 67 | olditem = getattr(module, attr, _NONE) 68 | if olditem is not _NONE: 69 | saved.setdefault(module, {}).setdefault(attr, olditem) 70 | setattr(module, attr, newitem) 71 | 72 | 73 | def remove_item(module, attr): 74 | olditem = getattr(module, attr, _NONE) 75 | if olditem is _NONE: 76 | return 77 | saved.setdefault(module, {}).setdefault(attr, olditem) 78 | delattr(module, attr) -------------------------------------------------------------------------------- /atx/ext/report/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/ext/report/report.png -------------------------------------------------------------------------------- /atx/imutils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Some code reference 5 | # https://github.com/jrosebr1/imutils 6 | # 7 | 8 | from __future__ import division 9 | from __future__ import print_function 10 | 11 | import re 12 | import os 13 | import base64 14 | from io import BytesIO 15 | 16 | import cv2 17 | import six 18 | import numpy as np 19 | from PIL import Image 20 | 21 | try: 22 | from urllib import urlopen 23 | except ImportError: 24 | from urllib.request import urlopen 25 | # import any special Python 2.7 packages 26 | #if sys.version_info.major == 2: 27 | 28 | # import any special Python 3 packages 29 | #elif sys.version_info.major == 3: 30 | 31 | 32 | __sys_open = open 33 | 34 | 35 | def _open_data_url(data, flag=cv2.IMREAD_COLOR): 36 | pos = data.find('base64,') 37 | if pos == -1: 38 | raise IOError("data url is invalid, head %s" % data[:20]) 39 | 40 | pos += len('base64,') 41 | raw_data = base64.decodestring(data[pos:]) 42 | image = np.asarray(bytearray(raw_data), dtype="uint8") 43 | image = cv2.imdecode(image, flag) 44 | return image 45 | 46 | 47 | def open(image): 48 | ''' 49 | Args: 50 | - image: support many type. filepath or url or data:image/png:base64 51 | Return: 52 | Pattern 53 | Raises 54 | IOError 55 | ''' 56 | if isinstance(image, six.string_types): 57 | name = image 58 | if name.startswith('data:image/'): 59 | return _open_data_url(name) 60 | if re.match(r'^https?://', name): 61 | return url_to_image(name) 62 | if os.path.isfile(name): 63 | img = cv2.imread(name) 64 | if img is None: 65 | raise IOError("Image format error: %s" % name) 66 | return img 67 | raise IOError("Open image(%s) not found" % name) 68 | 69 | return image 70 | 71 | def open_as_pillow(filename): 72 | """ This way can delete file immediately """ 73 | with __sys_open(filename, 'rb') as f: 74 | data = BytesIO(f.read()) 75 | return Image.open(data) 76 | # im = Image.open(filename) 77 | # im.load() 78 | # return im 79 | 80 | 81 | def from_pillow(pil_image): 82 | """ Convert from pillow image to opencv """ 83 | # convert PIL to OpenCV 84 | pil_image = pil_image.convert('RGB') 85 | cv2_image = np.array(pil_image) 86 | # Convert RGB to BGR 87 | cv2_image = cv2_image[:, :, ::-1].copy() 88 | return cv2_image 89 | 90 | 91 | def to_pillow(image): 92 | return Image.fromarray(image[:, :, ::-1].copy()) 93 | # There is another way 94 | # img_bytes = cv2.imencode('.png', image)[1].tostring() 95 | # return Image.open(BytesIO(img_bytes)) 96 | 97 | def url_to_image(url, flag=cv2.IMREAD_COLOR): 98 | """ download the image, convert it to a NumPy array, and then read 99 | it into OpenCV format """ 100 | resp = urlopen(url) 101 | image = np.asarray(bytearray(resp.read()), dtype="uint8") 102 | image = cv2.imdecode(image, flag) 103 | return image 104 | 105 | 106 | def crop(image, left=0, top=0, right=None, bottom=None): 107 | (h, w) = image.shape[:2] 108 | if bottom is None: 109 | bottom = h 110 | if right is None: 111 | right = w 112 | return image[top:bottom, left:right] 113 | 114 | def diff_rect(img1, img2, pos=None): 115 | """find counters include pos in differences between img1 & img2 (cv2 images)""" 116 | diff = cv2.absdiff(img1, img2) 117 | diff = cv2.GaussianBlur(diff, (3, 3), 0) 118 | edges = cv2.Canny(diff, 100, 200) 119 | _, thresh = cv2.threshold(edges, 0, 255, cv2.THRESH_BINARY) 120 | contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) 121 | if not contours: 122 | return None 123 | contours.sort(key=lambda c: len(c)) 124 | # no pos provide, just return the largest different area rect 125 | if pos is None: 126 | cnt = contours[-1] 127 | x0, y0, w, h = cv2.boundingRect(cnt) 128 | x1, y1 = x0+w, y0+h 129 | return (x0, y0, x1, y1) 130 | # else the rect should contain the pos 131 | x, y = pos 132 | for i in range(len(contours)): 133 | cnt = contours[-1-i] 134 | x0, y0, w, h = cv2.boundingRect(cnt) 135 | x1, y1 = x0+w, y0+h 136 | if x0 <= x <= x1 and y0 <= y <= y1: 137 | return (x0, y0, x1, y1) 138 | 139 | def mark_point(img, x, y): 140 | """ 141 | Mark a point 142 | 143 | Args: 144 | - img(numpy): the source image 145 | - x, y(int): position 146 | """ 147 | overlay = img.copy() 148 | output = img.copy() 149 | 150 | alpha = 0.5 151 | radius = max(5, min(img.shape[:2])//15) 152 | center = int(x), int(y) 153 | color = (0, 0, 255) 154 | 155 | cv2.circle(overlay, center, radius, color, -1) 156 | cv2.addWeighted(overlay, alpha, output, 1-alpha, 0, output) 157 | return output 158 | 159 | if __name__ == '__main__': 160 | # image = open('https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png') 161 | image = open('baidu.png') 162 | image = open(image) 163 | # cv2.imwrite('baidu.png', image) 164 | print(image.shape) 165 | image = crop(image, bottom=200, top=100, left=50, right=200) 166 | print(image.shape) 167 | cv2.imwrite('tmp.png', image) 168 | # to_pillow(image).save('b2.png') 169 | -------------------------------------------------------------------------------- /atx/ios/README.txt: -------------------------------------------------------------------------------- 1 | Commands of iOS -------------------------------------------------------------------------------- /atx/ios/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/atx/ios/__init__.py -------------------------------------------------------------------------------- /atx/ios/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import sys 6 | import inspect 7 | from contextlib import contextmanager 8 | 9 | from PIL import Image 10 | from atx import ioskit 11 | 12 | 13 | def inject(func, kwargs): 14 | """ Allow call func with more arguments """ 15 | args = [] 16 | for name in inspect.getargspec(func).args: 17 | args.append(kwargs.get(name)) 18 | return func(*args) 19 | 20 | 21 | def load_main(module_name): 22 | def _inner(parser_args): 23 | __import__(module_name) 24 | mod = sys.modules[module_name] 25 | pargs = vars(parser_args) 26 | print pargs 27 | return inject(mod.main, pargs) 28 | return _inner 29 | 30 | 31 | def _screencap(args): 32 | dev = ioskit.Device(args.udid) 33 | image = dev.screenshot() 34 | if args.rotate: 35 | method = getattr(Image, 'ROTATE_{}'.format(args.rotate)) 36 | image = image.transpose(method) 37 | image.save(args.output) 38 | print 'Screenshot saved to "%s"' % args.output 39 | 40 | 41 | def main(): 42 | ap = argparse.ArgumentParser( 43 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 44 | ap.add_argument("-u", "--udid", required=False, help="iPhone udid") 45 | 46 | subp = ap.add_subparsers() 47 | 48 | @contextmanager 49 | def add_parser(name): 50 | yield subp.add_parser(name, formatter_class=argparse.ArgumentDefaultsHelpFormatter) 51 | 52 | with add_parser('developer') as p: 53 | p.set_defaults(func=load_main('atx.cmds.iosdeveloper')) 54 | 55 | with add_parser('screencap') as p: 56 | p.add_argument('-o', '--output', default='screenshot.png', help='take iPhone screenshot') 57 | p.add_argument('-r', '--rotate', type=int, choices=[0, 90, 180, 270], default=0, help='screen rotation') 58 | p.set_defaults(func=_screencap) 59 | 60 | args = ap.parse_args() 61 | args.func(args) 62 | 63 | 64 | if __name__ == '__main__': 65 | main() 66 | -------------------------------------------------------------------------------- /atx/logutils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import unicode_literals 5 | 6 | import inspect 7 | import logging 8 | import os 9 | import sys 10 | import time 11 | import threading 12 | import datetime 13 | 14 | from atx import strutils 15 | 16 | 17 | class Logger(object): 18 | __alias = { 19 | 'WARNING': 'WARN', 20 | 'CRITICAL': 'FATAL' 21 | } 22 | 23 | def __init__(self, name=None, level=logging.INFO): 24 | if name is None: 25 | name = '-' 26 | self._name = name 27 | self._level = level 28 | self._lock = threading.Lock() 29 | 30 | def _write(self, s): 31 | self._lock.acquire() 32 | sys.stdout.write(s.rstrip() + '\n') 33 | self._lock.release() 34 | 35 | def setLevel(self, level): 36 | ''' 37 | set format level 38 | 39 | Args: 40 | - level: for example, logging.INFO 41 | 42 | ''' 43 | self._level = level 44 | return self 45 | 46 | def _level_write(self, level, str_format, *args): 47 | if level < self._level: 48 | return 49 | 50 | levelname = logging.getLevelName(level) 51 | message = str_format % args if args else str_format 52 | message = strutils.decode(message) 53 | frame, filename, line_number, function_name, lines, index = inspect.stack()[2] 54 | props = dict( 55 | asctime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3], 56 | name=self._name, 57 | filename=os.path.basename(filename), 58 | lineno=line_number, 59 | message=message, 60 | ) 61 | props['levelname'] = Logger.__alias.get(levelname, levelname) 62 | output = u'{asctime} {levelname:<5s} [{name}:{lineno:>4}] {message}'.format(**props) 63 | self._write(output) 64 | 65 | def debug(self, *args, **kwargs): 66 | self._level_write(logging.DEBUG, *args, **kwargs) 67 | 68 | def info(self, *args, **kwargs): 69 | self._level_write(logging.INFO, *args, **kwargs) 70 | 71 | def warn(self, *args, **kwargs): 72 | self._level_write(logging.WARN, *args, **kwargs) 73 | 74 | def error(self, *args, **kwargs): 75 | self._level_write(logging.ERROR, *args, **kwargs) 76 | 77 | def fatal(self, *args, **kwargs): 78 | self._level_write(logging.FATAL, *args, **kwargs) 79 | raise SystemExit(1) 80 | 81 | 82 | def getLogger(name, level=logging.INFO): 83 | # logger = logging.getLogger(name) 84 | # ch = logging.StreamHandler() 85 | # fmt = "%(asctime)s %(levelname)-8.8s [%(name)s:%(lineno)4s] %(message)s" 86 | # ch.setFormatter(logging.Formatter(fmt)) 87 | # ch.setLevel(level) 88 | # logger.handlers = [ch] 89 | return Logger(name, level=level) 90 | 91 | 92 | if __name__ == '__main__': 93 | log = getLogger('test') 94 | log.debug("Should not see it.") 95 | log.setLevel(logging.DEBUG) 96 | log.setLevel(logging.DEBUG) 97 | log.info("This is info message") 98 | log.debug("This is debug message") 99 | log = getLogger('test') 100 | log.warn("This is warning message") 101 | log.error("This is error message") 102 | log.fatal("This is fatal message") 103 | -------------------------------------------------------------------------------- /atx/patch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | 6 | import time 7 | import threading 8 | from functools import partial 9 | 10 | from atx import logutils 11 | 12 | 13 | log = logutils.getLogger(__name__) #base.getLogger('patch') 14 | 15 | 16 | def run_once(f): 17 | ''' 18 | Decorator: Make sure function only call once 19 | not thread safe 20 | 21 | @run_once 22 | def foo(): 23 | print 'bar' 24 | return 1+2 25 | foo() 26 | foo() # 'bar' only print once 27 | ''' 28 | def wrapper(*args, **kwargs): 29 | if not wrapper.has_run: 30 | wrapper.result = f(*args, **kwargs) 31 | wrapper.has_run = True 32 | return wrapper.result 33 | wrapper.has_run = False 34 | return wrapper 35 | 36 | def attachmethod(target): 37 | ''' 38 | Reference: https://blog.tonyseek.com/post/open-class-in-python/ 39 | 40 | class Spam(object): 41 | pass 42 | 43 | @attach_method(Spam) 44 | def egg1(self, name): 45 | print((self, name)) 46 | 47 | spam1 = Spam() 48 | # OpenClass 加入的方法 egg1 可用 49 | spam1.egg1("Test1") 50 | # 输出Test1 51 | ''' 52 | if isinstance(target, type): 53 | def decorator(func): 54 | setattr(target, func.__name__, func) 55 | else: 56 | def decorator(func): 57 | setattr(target, func.__name__, partial(func, target)) 58 | return decorator 59 | 60 | def fuckit(fn): 61 | def decorator(*args, **kwargs): 62 | try: 63 | return fn(*args, **kwargs) 64 | except Exception as e: 65 | args = list(args).extend([k+'='+v for k, v in kwargs.items()]) 66 | print('function(%s(%s)) panic(%s). fuckit' %(fn.__name__, ' ,'.join(args), e)) 67 | return None 68 | return decorator 69 | 70 | def go(fn): 71 | ''' 72 | Decorator 73 | ''' 74 | def decorator(*args, **kwargs): 75 | log.info('begin run func(%s) in background', fn.__name__) 76 | t = threading.Thread(target=fn, args=args, kwargs=kwargs) 77 | t.setDaemon(True) 78 | t.start() 79 | return t 80 | return decorator 81 | 82 | # test code 83 | if __name__ == '__main__': 84 | @go 85 | def say_hello(sleep=0.3, message='hello world'): 86 | time.sleep(sleep) 87 | print(message) 88 | return None 89 | t1 = say_hello(0.1) 90 | t2 = say_hello(0.5, 'this message should not showed') 91 | t1.join() 92 | -------------------------------------------------------------------------------- /atx/record/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /atx/record/draft_editor.py: -------------------------------------------------------------------------------- 1 | #-*- encoding: utf-8 -*- 2 | 3 | import os 4 | import os.path 5 | import json 6 | import socket 7 | import webbrowser 8 | import tornado.ioloop 9 | import tornado.web 10 | import tornado.websocket 11 | import traceback 12 | import time 13 | 14 | from tornado.web import StaticFileHandler 15 | 16 | import atx 17 | from atx.record.android import AndroidRecorder 18 | 19 | __dir__ = os.path.dirname(os.path.abspath(__file__)) 20 | 21 | cm = None 22 | 23 | class CaseManager(object): 24 | def __init__(self, basedir): 25 | self.basedir = basedir 26 | 27 | # record object 28 | record = AndroidRecorder(None, basedir) 29 | obj = {} 30 | with open(os.path.join(basedir, 'frames', 'frames.json')) as f: 31 | obj = json.load(f) 32 | record.device_info = obj['device'] 33 | record.frames = obj['frames'] 34 | self.record = record 35 | 36 | # case 37 | self.casepath = os.path.join(basedir, 'case', 'case.json') 38 | self.case = [] 39 | with open(self.casepath) as f: 40 | self.case = json.load(f) 41 | 42 | self._env = None 43 | 44 | def save_case(self, data): 45 | self.case = json.loads(data) 46 | with open(self.casepath, 'w') as f: 47 | json.dump(self.case, f, indent=2) 48 | 49 | # generate code 50 | try: 51 | AndroidRecorder.process_casefile(self.basedir) 52 | return True 53 | except: 54 | traceback.print_exc() 55 | return False 56 | 57 | def build_exec_env(self): 58 | d = atx.connect() 59 | d.image_path.append(os.path.join(self.basedir, 'case')) 60 | self._env = {'time': time, 'd': d} 61 | 62 | def run_step(self, frameidx): 63 | if self._env is None: 64 | self.build_exec_env() 65 | for row in self.case: 66 | if row['frameidx'] == frameidx: 67 | code = self.record.process_draft(row) 68 | print 'running', row, code 69 | try: 70 | cobj = compile(code, '', 'exec') 71 | ret = eval(cobj, None, self._env) 72 | except Exception as e: 73 | return str(e) 74 | else: 75 | return 'ok, return value: %s' % str(ret) 76 | return 'Not Found.' 77 | 78 | class MainHandler(tornado.web.RequestHandler): 79 | def get(self): 80 | self.redirect('/index.html') 81 | 82 | class CaseHandler(tornado.web.RequestHandler): 83 | 84 | def get(self, *args): 85 | self.write(json.dumps(cm.case)) 86 | self.finish() 87 | 88 | def post(self, *args): 89 | data = self.request.arguments['data'][0] ## get the string 90 | ok = cm.save_case(data) 91 | self.write(json.dumps({'success':ok})) 92 | 93 | class CaseRunnerHandler(tornado.websocket.WebSocketHandler): 94 | 95 | def check_origin(self, origin): 96 | return True 97 | 98 | def open(self, *args, **kwargs): 99 | print 'Websocket connnected.' 100 | 101 | def on_close(self): 102 | print 'Websocket closed.' 103 | 104 | def on_message(self, message): 105 | print 'received:', message 106 | try: 107 | data = json.loads(message) 108 | except Exception as e: 109 | self.write_message({'err': str(e)}) 110 | return 111 | 112 | action = data['action'] 113 | frame = data.get('frame') 114 | if action in ('run_all', 'run_step'): 115 | msg = cm.run_step(frame) 116 | self.write_message({'action':action, 'frame':frame, 'msg': msg}) 117 | else: 118 | self.write_message({'err': 'unknown action %s' % action}) 119 | 120 | def get_valid_port(): 121 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 122 | s.bind(('', 0)) 123 | port = s.getsockname()[1] 124 | s.close() 125 | return port 126 | 127 | def run(basedir, port=8000): 128 | global cm 129 | basedir = os.path.abspath(basedir) 130 | cm = CaseManager(basedir) 131 | application = tornado.web.Application([ 132 | (r'/', MainHandler), 133 | (r'/frames/(.*)', StaticFileHandler, {'path':os.path.join(basedir, 'frames')}), 134 | (r'/case(.*)', CaseHandler), 135 | (r'/run', CaseRunnerHandler), 136 | (r'/(.*)', StaticFileHandler, {'path':os.path.join(__dir__, 'site')}), 137 | ], autoreload=True, static_hash_cache=False) 138 | 139 | if port is None: 140 | port = get_valid_port() 141 | webbrowser.open('http://127.0.0.1:%s' % port, new=2) 142 | 143 | application.listen(port) 144 | print 'Listen on', port 145 | print 'WorkDir:', basedir 146 | print 'Press Ctrl+C to stop...' 147 | try: 148 | tornado.ioloop.IOLoop.instance().start() 149 | except: 150 | print 'Done' 151 | 152 | if __name__ == '__main__': 153 | run('testcase') 154 | -------------------------------------------------------------------------------- /atx/record/site/index.html: -------------------------------------------------------------------------------- 1 | web -------------------------------------------------------------------------------- /atx/record/site/static/css/app.css: -------------------------------------------------------------------------------- 1 | body,html{height:100%}body{min-width:800px;background-color:#f3f3f3;font-family:monospace;font-size:small}body,p{margin:0}ul{padding:0}li{display:inline-block;list-style:none;padding-left:10px}select{border-radius:3px;padding:1px 3px}.frame{background-color:#fff;margin:6px;padding:6px;border-radius:5px}.frame>div.left>img{vertical-align:middle}.left{float:left}.right{float:right}.clear-fix{*overflow:hidden;*zoom:1}.clear-fix:after{display:table;content:"";width:0;clear:both}button.skip{opacity:0;width:20px;height:20px;border:0}button.skip:hover{opacity:1}.skipped{height:6px;padding:0}.skipped>.left,.skipped>.right{display:none}.wrapper{height:100%;flex-grow:1;display:flex;flex-flow:row}.left-panel{flex-grow:1;margin-right:10px}.right-panel{flex-basis:300px;padding:10px}.v-slider{position:relative;vertical-align:middle}.v-slider-bar{background-color:#333;position:absolute}.v-slider-point{background-color:#bbb;position:absolute}.highlight{background:none repeat scroll 0 0 #ffffe0;border:1px solid #e6db55}#ui-overlap{position:relative}#ui-overlap>div,.uibox{position:absolute}.uibox{border:1px solid red}@keyframes breath{0%{opacity:1;box-shadow:0 0 10px 10px #dc143c}50%{opacity:.4;box-shadow:0 0 1px 1px #dc143c}to{opacity:1;box-shadow:0 0 10px 10px #dc143c}}.point{animation:breath 2s ease-in-out infinite;background-color:#fff;border:1px solid red;position:absolute}.chop{display:inline-block;vertical-align:middle}.rect{position:absolute;border:1px solid red}.full{width:100%;height:100%}.resizer{position:absolute;right:0;bottom:0;height:5px;width:5px;border:1px solid red}.resizer:hover{cursor:se-resize}.nav{margin-left:12px}.nav>li{float:left;margin-right:8px;line-height:22px}.toolbar{flex-basis:60px}.tool{margin:5px}.tool img{width:30px;height:30px}.selected{background:none repeat scroll 0 0 #e00}#app{color:#2c3e50;font-family:Source Sans Pro,Helvetica,sans-serif}#app a{color:#42b983;text-decoration:none}.scroll{height:100%;overflow-y:auto}.scroll::-webkit-scrollbar{width:16px}.scroll-wrapper{height:100%;position:relative}.scroll-bottom-more,.scroll-top-more{position:absolute;height:10px;left:5px;right:5px;margin:0 auto;text-align:center}.scroll-top-more{top:-10px}.scroll-bottom-more{bottom:-10px}.arrow-up{border-bottom:10px solid blue}.arrow-down,.arrow-up{margin:0 auto;width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent}.arrow-down{border-top:10px solid blue} -------------------------------------------------------------------------------- /atx/record/site/static/js/manifest.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(n){if(r[n])return r[n].exports;var a=r[n]={exports:{},id:n,loaded:!1};return e[n].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n=window.webpackJsonp;window.webpackJsonp=function(o,p){for(var s,c,l=0,i=[];l=4.1`) to PC 11 | 12 | Open terminal, execute `adb devices`, make sure you see your device. 13 | 14 | ```bash 15 | $ adb devices 16 | List of devices attached 17 | EP7333W7XB device 18 | ``` 19 | 20 | 创建一个python文件 `test.py`, 内容如下 21 | 22 | ```python 23 | # coding: utf-8 24 | import atx 25 | 26 | d = atx.connect() # 如果多个手机连接电脑,则需要填入对应的设备号 27 | d.screenshot('screen.png') # 截图 28 | ``` 29 | 30 | 运行 `python test.py` 31 | 32 | * iOS 33 | 34 | WDA运行完之后,准备好`DEVICE_URL`, 如 `http://localhost:8100` 35 | 如果是真机,需要将localhost改成手机的IP地址 36 | 37 | python代码可以这样写 38 | 39 | ```python 40 | # coding: utf-8 41 | import atx 42 | 43 | d = atx.connect('http://localhost:8100', platform='ios') # platform也可以不指定 44 | print d.status() 45 | ``` 46 | 47 | ## Take screenshot 48 | 49 | - Android: `python -m atx gui` 50 | - iOS: `python -m atx gui --serial $DEVICE_URL` 51 | 52 | `DEVICE_URL` 通常是 `http://localhost:8100` 53 | 54 | 更多命令可以通过`python -m atx gui --help` 查看。如果屏幕超过了整个屏幕可以通过调小 `--scale` 来调整 55 | 56 | 鼠标左键拖拽选择一个按钮或者图标, 按下`Save Cropped`截图保存退出. (按下`Refresh`可以重新刷新屏幕) 57 | 58 | ![gui](../images/atx-gui.gif) 59 | 60 | _PS: 这里其实有个好的IDE截图的最好了,现在是用Tkinter做的,比较简洁,但是可以跨平台,效果也还可以_ 61 | 62 | 截图后的文件另存为 `button.png`, `test.py` 最后增加一行 `d.click_image('button.png')` 63 | 64 | 重新运行 `python test.py`, 此时差不多可以看到代码可以点击那个按钮了 65 | 66 | ## 编辑器 67 | 另外还有一个好用的WEB编辑器,完全独立的本地服务,名叫`atx-webide` 68 | 69 | 虽然简陋,但已经非常好用。代码编辑,截图,测试运行都包含在内。 70 | 71 | 安装方法 72 | 73 | ``` 74 | pip install -U --pre atx-webide 75 | ``` 76 | 77 | 在命令行下使用命令 `python -m atxweb` 启动编辑器 78 | 79 | ## 更多 80 | 81 | 可以使用的接口还有很多,回到README继续看 82 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. atx documentation master file, created by 2 | sphinx-quickstart on Mon Mar 7 13:34:33 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to ATX's documentation! 7 | =================================== 8 | Release you from the boring testing work. 9 | 10 | Documentation 11 | ------------- 12 | 13 | .. toctree:: 14 | :maxdepth: 3 15 | 16 | usage 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /docs/multisize-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/docs/multisize-button.png -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Public class 2 | -------------------------- 3 | 4 | .. autoclass:: atx.drivers.Pattern 5 | :members: 6 | 7 | 8 | Common methods 9 | -------------------------- 10 | 11 | .. autoclass:: atx.drivers.mixin.DeviceMixin 12 | :members: 13 | 14 | 15 | Method only for android 16 | -------------------------- 17 | 18 | .. autoclass:: atx.drivers.android.AndroidDevice 19 | :members: 20 | 21 | .. attribute:: serial 22 | 23 | Android device serial number. **Optional** 24 | 25 | Method only for windows 26 | ----------------------- 27 | .. autoclass:: atx.drivers.windows.WindowsDevice 28 | :members: -------------------------------------------------------------------------------- /images/atx-gui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/images/atx-gui.gif -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/images/demo.gif -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/images/logo.png -------------------------------------------------------------------------------- /images/macmini.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/images/macmini.jpg -------------------------------------------------------------------------------- /images/tkide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/images/tkide.png -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | # 3 | # 4 | set -eu 5 | cd $(dirname $0) 6 | 7 | # Sync to pypi 8 | python setup.py sdist bdist_wheel upload -r ${1:-pypi} 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aircv==1.4.6 2 | Pillow>=2.7.0 3 | tornado>=4.4 4 | imageio>=1.5 5 | futures>=3.0.5 ; python_version <= '2.7' 6 | subprocess32>=3.2.7 ; python_version <= '2.7' 7 | tqdm>=4.5.0 8 | PyYAML 9 | numpy 10 | uiautomator2 11 | six 12 | #numpy>=1.11.0 13 | apkutils>=0.2.6 ; python_version >= '3.4' 14 | maproxy 15 | requests 16 | facebook-wda>=0.2.1 17 | colorama -------------------------------------------------------------------------------- /scripts/100-lines-tcp-proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # This is a simple port-forward / proxy, written using only the default python 3 | # library. If you want to make a suggestion or fix something you can contact-me 4 | # at voorloop_at_gmail.com 5 | # Distributed over IDC(I Don't Care) license 6 | import socket 7 | import select 8 | import time 9 | import sys 10 | 11 | # Changing the buffer_size and delay, you can improve the speed and bandwidth. 12 | # But when buffer get to high or delay go too down, you can broke things 13 | buffer_size = 4096 14 | delay = 0.0001 15 | forward_to = ('127.0.0.1', 5037) 16 | #forward_to = ('10.249.80.122', 57149) 17 | 18 | class Forward: 19 | def __init__(self): 20 | self.forward = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 21 | 22 | def start(self, host, port): 23 | try: 24 | self.forward.connect((host, port)) 25 | return self.forward 26 | except Exception, e: 27 | print e 28 | return False 29 | 30 | class TheServer: 31 | input_list = [] 32 | channel = {} 33 | 34 | def __init__(self, host, port): 35 | self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 36 | self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 37 | self.server.bind((host, port)) 38 | self.server.listen(200) 39 | 40 | def main_loop(self): 41 | self.input_list.append(self.server) 42 | while 1: 43 | time.sleep(delay) 44 | ss = select.select 45 | inputready, outputready, exceptready = ss(self.input_list, [], []) 46 | for self.s in inputready: 47 | if self.s == self.server: 48 | self.on_accept() 49 | break 50 | 51 | self.data = self.s.recv(buffer_size) 52 | if len(self.data) == 0: 53 | self.on_close() 54 | break 55 | else: 56 | self.on_recv() 57 | 58 | def on_accept(self): 59 | forward = Forward().start(forward_to[0], forward_to[1]) 60 | clientsock, clientaddr = self.server.accept() 61 | if forward: 62 | print '[proxy]', clientaddr, "has connected" 63 | self.input_list.append(clientsock) 64 | self.input_list.append(forward) 65 | self.channel[clientsock] = forward 66 | self.channel[forward] = clientsock 67 | else: 68 | print "[proxy] Can't establish connection with remote server.", 69 | print "[proxy] Closing connection with client side", clientaddr 70 | clientsock.close() 71 | 72 | def on_close(self): 73 | print '[proxy]', self.s.getpeername(), "has disconnected" 74 | print '[proxy]', self.channel[self.s].getpeername(), "has disconnected, too" 75 | #remove objects from input_list 76 | self.input_list.remove(self.s) 77 | self.input_list.remove(self.channel[self.s]) 78 | out = self.channel[self.s] 79 | # close the connection with client 80 | self.channel[out].close() # equivalent to do self.s.close() 81 | # close the connection with remote server 82 | self.channel[self.s].close() 83 | # delete both objects from channel dict 84 | del self.channel[out] 85 | del self.channel[self.s] 86 | 87 | def on_recv(self): 88 | data = self.data 89 | # here we can parse and/or modify the data before send forward 90 | #print isinstance(self.s, Forward), self.server 91 | print '[%5s]: %s' % (self.s.getpeername()[1], data) 92 | self.channel[self.s].send(data) 93 | 94 | if __name__ == '__main__': 95 | server = TheServer('', 5555) 96 | try: 97 | server.main_loop() 98 | except KeyboardInterrupt: 99 | print "Ctrl C - Stopping server" 100 | sys.exit(1) 101 | -------------------------------------------------------------------------------- /scripts/AXMLPrinter2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/scripts/AXMLPrinter2.jar -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | airtest recorder 2 | ================== 3 | 4 | ## how to use 5 | ### record 6 | ```sh 7 | adb shell getevent -l | python airtest_recorder.py > main.py 8 | ``` 9 | 10 | ### playback 11 | ```sh 12 | python main.py 13 | ``` 14 | 15 | # other record tool: monkey runer 16 | monkey_record.py and monkey_playback from 17 | 18 | 19 | ### AndroidManifest.xml parser 20 | python-androguard: 21 | 22 | -------------------------------------------------------------------------------- /scripts/airtest_recoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # 2014/07/02 by codeskyblue 5 | # 6 | ''' 7 | convert adb event to air.test program 8 | usage is simple: 9 | python airtest_recorder.py output.py 10 | ''' 11 | 12 | import sys 13 | import time 14 | import subprocess 15 | import re 16 | import os 17 | 18 | DEVSCREEN = '/dev/input/event1' 19 | 20 | __header__ = ''' 21 | import airtest 22 | app = airtest.connect("{id}") 23 | ''' 24 | 25 | def getRawShape(): 26 | output = subprocess.check_output('adb shell getevent -p '+DEVSCREEN, shell=True) 27 | re.compile(r'0035.*max (\d+)') 28 | max_x = re.search(r'0035.*max (\d+)', output).group(1) 29 | max_y = re.search(r'0036.*max (\d+)', output).group(1) 30 | return max_x, max_y 31 | 32 | def getShape(): 33 | rsRE = re.compile('\s*mRestrictedScreen=\(\d+,\d+\) (?P\d+)x(?P\d+)') 34 | for line in subprocess.check_output('adb shell dumpsys window', shell=True).splitlines(): 35 | m = rsRE.match(line) 36 | if m: 37 | return m.groups() 38 | raise RuntimeError('Couldn\'t find mRestrictedScreen in dumpsys') 39 | 40 | def getScale(): 41 | rawx, rawy = getRawShape() 42 | w, h = getShape() 43 | width, height = min(w, h), max(w, h) 44 | print '#raw', rawx, rawy 45 | print '# screen.width:', width 46 | print '# screen.height:', height 47 | if width == w: 48 | print("def click(x, y): app.touch(x, y)") 49 | else: 50 | print("def click(x, y): app.touch(y, %s-x)", width) 51 | print("def sleep(d): app.sleep(d)") 52 | return float(rawx)/float(width), float(rawy)/float(height) 53 | 54 | def getDeviceId(): 55 | output = subprocess.check_output('adb devices', shell=True) 56 | match = re.search(r'([\w\d:.]+)\s+device\s*$', output) 57 | if match: 58 | return match.group(1) 59 | raise RuntimeError("Couldn't find avaliable device") 60 | 61 | def main(pipe, filename): 62 | xs, ys = [], [] 63 | lastOper = '' 64 | touchStart = 0 65 | start = time.time() 66 | begin = time.time() 67 | 68 | deviceId = getDeviceId() 69 | scaleX, scaleY = getScale() 70 | 71 | def record(fmt, *args): 72 | outstr = fmt % args 73 | if filename: 74 | with open(filename, 'a') as file: 75 | file.write(outstr + '\n') 76 | print outstr 77 | 78 | record(__header__.format(id=deviceId)) 79 | 80 | # plen() 81 | while True: 82 | line = pipe.readline() 83 | if not line.startswith(DEVSCREEN): 84 | continue 85 | channel, event, oper, value = line.split() 86 | # print event, oper, value#int(value, 16) 87 | value = int(value, 16) 88 | if oper == 'SYN_REPORT': 89 | continue 90 | 91 | if oper == 'ABS_MT_POSITION_X': 92 | xs.append(value) 93 | elif oper == 'ABS_MT_POSITION_Y': 94 | ys.append(value) 95 | elif oper == 'SYN_MT_REPORT': 96 | if lastOper == oper: 97 | xs = map(lambda x: x/scaleX, xs) 98 | ys = map(lambda y: y/scaleY, ys) 99 | if len(xs) != 0 and len(ys) != 0: # every thing is OK 100 | (x1, y1), (x2, y2) = (xs[0], ys[0]), (xs[-1], ys[-1]) 101 | dist = ((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1))**0.5 102 | 103 | duration = time.time()-touchStart 104 | # print 'Duration:', duration 105 | # touch up 106 | if dist < 50: 107 | record('app.click((%d, %d))', x1, y1) 108 | else: 109 | record('app.drag((%d, %d), (%d, %d))', x1, y1, x2, y2) 110 | xs, ys = [], [] 111 | else: 112 | if len(xs) == 1: 113 | # touch down 114 | record('app.sleep(%.2f)', float(time.time()-start)) 115 | start = time.time() 116 | touchStart = time.time() 117 | lastOper = oper 118 | 119 | if __name__ == '__main__': 120 | try: 121 | filename = None if len(sys.argv) == 1 else sys.argv[1] 122 | if filename and os.path.exists(filename): 123 | os.unlink(filename) 124 | p = subprocess.Popen(['adb', 'shell', 'getevent', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 125 | main(p.stdout, filename) 126 | except KeyboardInterrupt: 127 | print 'Exit' 128 | p.kill() 129 | -------------------------------------------------------------------------------- /scripts/airtoolbox/Makefile: -------------------------------------------------------------------------------- 1 | everything: build 2 | 3 | build: build-linux 4 | GOARCH=arm go build 5 | cp airtoolbox ../../airtest/binfiles/ 6 | ./a.test version > ../../airtest/binfiles/airtoolbox.version 7 | 8 | build-linux: 9 | go build -o a.test 10 | 11 | install: build 12 | adb push airtoolbox /data/local/tmp/ 13 | 14 | test: build-linux 15 | ./a.test 16 | -------------------------------------------------------------------------------- /scripts/airtoolbox/common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | type FlagCommand struct { 14 | Func func(args ...string) error 15 | Usage string 16 | } 17 | 18 | func RunCmd(commands map[string]FlagCommand, head string, args []string) error { 19 | var usage = head //fmt.Sprintf("Usage: %s ...", os.Args[0]) 20 | for name, c := range commands { 21 | usage = usage + "\n\t" + name + "\t\t" + c.Usage 22 | } 23 | if len(args) == 0 || args[0] == "-h" || args[0] == "--help" { 24 | fmt.Println(usage) 25 | return nil 26 | } 27 | cmdName, cmdArgs := args[0], args[1:] 28 | fn, exists := commands[cmdName] 29 | if !exists { 30 | return fmt.Errorf("command(%s) not found", cmdName) 31 | } 32 | return fn.Func(cmdArgs...) 33 | } 34 | 35 | var ( 36 | curpwd string = filepath.Dir(os.Args[0]) 37 | ) 38 | 39 | func atoi(a string) int { 40 | var i int 41 | _, err := fmt.Sscanf(a, "%d", &i) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | return i 46 | } 47 | 48 | func itoa(i int) string { 49 | return strconv.Itoa(i) 50 | } 51 | 52 | func sh(args ...string) (err error) { 53 | c := exec.Command("sh", "-c", strings.Join(args, " ")) 54 | c.Stdout = os.Stdout 55 | c.Stderr = os.Stderr 56 | c.Stdin = os.Stdin 57 | return c.Run() 58 | } 59 | -------------------------------------------------------------------------------- /scripts/airtoolbox/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | const VERSION = "ver1.1" 10 | 11 | var ( 12 | commands = map[string]FlagCommand{ 13 | "version": {cmdVersion, "show version"}, 14 | } 15 | ) 16 | 17 | func cmdVersion(args ...string) (err error) { 18 | fmt.Println(VERSION) 19 | return nil 20 | } 21 | 22 | func main() { 23 | usage := fmt.Sprintf("Usage: %s ...", os.Args[0]) 24 | err := RunCmd(commands, usage, os.Args[1:]) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/airtoolbox/stat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | 8 | "github.com/shirou/gopsutil" 9 | ) 10 | 11 | func init() { 12 | commands["stat"] = FlagCommand{cmdStat, "netio"} 13 | } 14 | 15 | func cmdStat(args ...string) (err error) { 16 | var usage = "Usage: airtoolbox stat ..." 17 | return RunCmd(statCommands, usage, args) 18 | } 19 | 20 | var ( 21 | statCommands = map[string]FlagCommand{ 22 | "netio": {cmdStatNetio, " stat net sent,recv"}, 23 | } 24 | ) 25 | 26 | // convert name to pid 27 | func name2pid(name string) (int32, error) { 28 | pid, err := strconv.Atoi(name) 29 | if err == nil { 30 | return int32(pid), nil 31 | } 32 | pids, err := gopsutil.Pids() 33 | if err != nil { 34 | return 0, err 35 | } 36 | for _, pid := range pids { 37 | p, er := gopsutil.NewProcess(pid) 38 | if er != nil { 39 | continue 40 | } 41 | pname, er := p.Name() 42 | if er != nil { 43 | continue 44 | } 45 | if pname == name { 46 | fmt.Println(pid) 47 | return pid, nil 48 | } 49 | } 50 | return 0, fmt.Errorf("proc(%s) not found", name) 51 | } 52 | 53 | func cmdStatNetio(args ...string) (err error) { 54 | if len(args) != 1 { 55 | return ErrArguments 56 | } 57 | pid, err := name2pid(args[0]) 58 | if err != nil { 59 | return 60 | } 61 | proc, err := gopsutil.NewProcess(pid) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | ncs, err := proc.NetIOCounters() 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | var sent, recv uint64 71 | for _, c := range ncs { 72 | if c.Name == "lo" { 73 | continue 74 | } 75 | sent += c.BytesSent 76 | recv += c.BytesRecv 77 | fmt.Printf("%s: sent:%d, recv:%d\n", c.Name, c.BytesSent, c.BytesRecv) 78 | } 79 | fmt.Printf("send: %dKB\nrecv: %dKB\n", sent>>10, recv>>10) 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /scripts/androaxml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file is part of Androguard. 4 | # 5 | # Copyright (C) 2012, Anthony Desnos 6 | # All rights reserved. 7 | # 8 | # Androguard is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU Lesser General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # Androguard is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Lesser General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Lesser General Public License 19 | # along with Androguard. If not, see . 20 | 21 | import os 22 | __dir__ = os.path.relpath(os.path.dirname(os.path.abspath(__file__))) 23 | 24 | import sys 25 | sys.path.append(os.path.join(__dir__, "androguard.zip")) 26 | 27 | from optparse import OptionParser 28 | from xml.dom import minidom 29 | import codecs 30 | import string 31 | 32 | from androguard.core import androconf 33 | from androguard.core.bytecodes import apk 34 | 35 | 36 | #option_0 = { 'name' : ('-i', '--input'), 'help' : 'filename input (APK or android\'s binary xml)', 'nargs' : 1 } 37 | option_1 = {'name' : ('-f', '--format'), 38 | 'help': 'output format', 39 | 'nargs': 1, 40 | 'default': '$package' 41 | } 42 | option_2 = { 43 | 'name': ('-v', '--version'), 44 | 'help':'version of the API', 45 | 'action': 'count' 46 | } 47 | options = [option_1, option_2] 48 | 49 | def xml2parse(dom, strformat='$package/$activity'): 50 | root = dom.getElementsByTagName("manifest")[0] 51 | package = root.getAttribute('package') 52 | activity = '' 53 | for e in root.getElementsByTagName('activity'): 54 | name = e.getAttribute('android:name') 55 | t = e.getElementsByTagName('intent-filter') 56 | if t: 57 | activity = name 58 | print string.Template(strformat).safe_substitute( 59 | package=package, activity = activity) 60 | 61 | def main(options, filename) : 62 | if filename != None : 63 | buff = "" 64 | 65 | ret_type = androconf.is_android(filename) 66 | if ret_type == "APK": 67 | a = apk.APK(filename) 68 | dom = a.get_android_manifest_xml() 69 | #buff = a.get_android_manifest_xml().toprettyxml(encoding="utf-8") 70 | #a.get_activities() 71 | xml2parse(dom) 72 | elif ".xml" in filename: 73 | ap = apk.AXMLPrinter(open(filename, "rb").read()) 74 | buff = minidom.parseString(ap.get_buff()).toprettyxml(encoding="utf-8") 75 | else: 76 | print "Unknown file type" 77 | return 78 | 79 | #if options.output != None : 80 | # fd = codecs.open(options.output, "w", "utf-8") 81 | # fd.write( buff ) 82 | # fd.close() 83 | #else : 84 | # print buff 85 | 86 | elif options.version != None : 87 | print "Androaxml version %s" % androconf.ANDROGUARD_VERSION 88 | 89 | if __name__ == "__main__" : 90 | parser = OptionParser() 91 | for option in options : 92 | param = option['name'] 93 | del option['name'] 94 | parser.add_option(*param, **option) 95 | 96 | options, arguments = parser.parse_args() 97 | if len(arguments) == 0: 98 | sys.exit('use --help for more help') 99 | 100 | sys.argv[:] = arguments 101 | main(options, arguments[0]) 102 | -------------------------------------------------------------------------------- /scripts/androguard.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/scripts/androguard.zip -------------------------------------------------------------------------------- /scripts/apkview/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | GOOS=windows GOARCH=386 go build 3 | GOOS=linux GOARCH=386 go build 4 | -------------------------------------------------------------------------------- /scripts/apkview/apkview.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "text/template" 14 | ) 15 | 16 | var TMPAPKPATH string 17 | 18 | func init() { 19 | TMPAPKPATH, _ = ioutil.TempDir(".", "apktmp-") //"tmpapk-directory" 20 | } 21 | 22 | var ( 23 | format = flag.String("f", "{{.package}}/{{.activity}}", "output format") 24 | apkpath = flag.String("apk", "", "android pkg path") 25 | quite = flag.Bool("q", false, "suppress debug info") 26 | ) 27 | 28 | func ParseApk() error { 29 | toolpath := filepath.Join(SelfDir(), "apktool.jar") 30 | fmt.Println(toolpath) 31 | c := exec.Command("java", "-jar", toolpath, "decode", "--force", *apkpath, TMPAPKPATH) 32 | if !*quite { 33 | c.Stdout = os.Stdout 34 | c.Stderr = os.Stderr 35 | } 36 | return c.Run() 37 | } 38 | 39 | func SelfDir() string { 40 | selfDir := filepath.Dir(os.Args[0]) 41 | if !filepath.IsAbs(selfDir) { 42 | selfDir, _ = filepath.Abs(selfDir) 43 | } 44 | return selfDir 45 | } 46 | 47 | func ParseManifest() (pkgname string, activity string, err error) { 48 | manifest := filepath.Join(TMPAPKPATH, "AndroidManifest.xml") 49 | data, err := ioutil.ReadFile(manifest) 50 | if err != nil { 51 | return 52 | } 53 | mf := new(Manifest) 54 | if err = xml.Unmarshal(data, mf); err != nil { 55 | return 56 | } 57 | for _, act := range mf.Application.Activities { 58 | if act.IntentFilter.Action.Name == "android.intent.action.MAIN" { 59 | activity = act.Name 60 | break 61 | } 62 | } 63 | return mf.Package, activity, nil 64 | } 65 | 66 | func main() { 67 | defer os.RemoveAll(TMPAPKPATH) 68 | flag.Parse() 69 | if *apkpath == "" { 70 | log.Fatal("need apkpath") 71 | } 72 | if err := ParseApk(); err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | pkgname, activity, err := ParseManifest() 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | wr := bytes.NewBuffer(nil) 81 | tmpl, err := template.New("t").Parse(*format) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | tmpl.Execute(wr, map[string]string{ 86 | "package": pkgname, 87 | "activity": activity}) 88 | fmt.Println(string(wr.Bytes())) 89 | } 90 | -------------------------------------------------------------------------------- /scripts/apkview/proto.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "encoding/xml" 4 | 5 | type Intent struct { 6 | Action struct { 7 | Name string `xml:"name,attr"` 8 | } `xml:"action"` 9 | Category struct { 10 | Name string `xml:"name,attr"` 11 | } `xml:"category"` 12 | } 13 | 14 | type Activity struct { 15 | Label string `xml:"label,attr"` 16 | Name string `xml:"name,attr"` 17 | IntentFilter Intent `xml:"intent-filter"` 18 | } 19 | 20 | type Manifest struct { 21 | XMLName xml.Name `xml:"manifest"` 22 | Package string `xml:"package,attr"` 23 | Application struct { 24 | Activities []Activity `xml:"activity"` 25 | } `xml:"application"` 26 | } 27 | -------------------------------------------------------------------------------- /scripts/compile.sh: -------------------------------------------------------------------------------- 1 | set -eux 2 | TARGET=/data/local/tmp 3 | GOARCH=arm go build airinput.go 4 | adb push airinput $TARGET/ 5 | adb shell chmod 755 $TARGET/airinput 6 | #adb shell rm $TARGET/devices.json 7 | #adb shell $TARGET/airinput test 8 | -------------------------------------------------------------------------------- /scripts/gui1.py: -------------------------------------------------------------------------------- 1 | try: 2 | from Tkinter import * 3 | import Tkinter as ttk 4 | except: 5 | from tkinter import * 6 | from tkinter import ttk 7 | 8 | def calculate(*args): 9 | try: 10 | value = float(feet.get()) 11 | meters.set((0.3048 * value * 10000.0 + 0.5)/10000.0) 12 | except ValueError: 13 | pass 14 | 15 | root = Tk() 16 | root.title("Feet to Meters") 17 | 18 | mainframe = ttk.Frame(root)#, padding="3 3 12 12") 19 | mainframe.grid(column=0, row=0, sticky=(N, W, E, S)) 20 | mainframe.columnconfigure(0, weight=1) 21 | mainframe.rowconfigure(0, weight=1) 22 | 23 | feet = StringVar() 24 | meters = StringVar() 25 | 26 | feet_entry = ttk.Entry(mainframe, width=7, textvariable=feet) 27 | feet_entry.grid(column=2, row=1, sticky=(W, E)) 28 | 29 | ttk.Label(mainframe, textvariable=meters).grid(column=2, row=2, sticky=(W, E)) 30 | ttk.Button(mainframe, text="Calculate", command=calculate).grid(column=3, row=3, sticky=W) 31 | 32 | ttk.Label(mainframe, text="feet").grid(column=3, row=1, sticky=W) 33 | ttk.Label(mainframe, text="is equivalent to").grid(column=1, row=2, sticky=E) 34 | ttk.Label(mainframe, text="meters").grid(column=3, row=2, sticky=W) 35 | 36 | for child in mainframe.winfo_children(): child.grid_configure(padx=5, pady=5) 37 | 38 | feet_entry.focus() 39 | root.bind('', calculate) 40 | 41 | root.mainloop() 42 | -------------------------------------------------------------------------------- /scripts/image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'hzsunshx' 5 | 6 | #import numpy as np 7 | import cv2 8 | #from matplotlib import pyplot as plt 9 | 10 | 11 | MIN_MATCH_COUNT = 8 12 | 13 | def _middlePoint(pts): 14 | def add(p1, p2): 15 | return (p1[0]+p2[0], p1[1]+p2[1]) 16 | def distance(p1, p2): 17 | import math 18 | l2 = (p1[0]-p2[0])*(p1[0]-p2[0]) + (p1[1]-p2[1])*(p1[1]-p2[1]) 19 | return math.sqrt(l2) 20 | # debug 21 | for p in pts: 22 | print 'Point:', p.pt 23 | length = len(pts) 24 | sumx, sumy = reduce(add, [p.pt for p in pts]) 25 | point = sumx/length, sumy/length 26 | print 'step1: result=', point 27 | 28 | # filter out ok points 29 | avg_distance = sum([distance(point, p.pt) for p in pts])/length 30 | print 'avg distance=', avg_distance 31 | good = [] 32 | sumx, sumy = 0.0, 0.0 33 | for p in pts: 34 | print 'point: %s, distance: %.2f' %(p.pt, distance(p.pt, point)) 35 | if distance(p.pt, point) < 1.2*avg_distance: 36 | good.append(p.pt) 37 | sumx += p.pt[0] 38 | sumy += p.pt[1] 39 | else: 40 | print 'not good', p.pt 41 | print 'step1: result=', point 42 | point = map(long, (sumx/len(good), sumy/len(good))) 43 | print 'step2: point=', point 44 | return point 45 | 46 | def find_image_position(origin='origin.png', query='query.png', outfile=None): 47 | ''' 48 | find all image positions 49 | @return None if not found else a tuple: (origin.shape, query.shape, postions) 50 | might raise Exception 51 | ''' 52 | img1 = cv2.imread(query, 0) # query image(small) 53 | img2 = cv2.imread(origin, 0) # train image(big) 54 | 55 | # Initiate SIFT detector 56 | sift = cv2.SIFT() 57 | 58 | # find the keypoints and descriptors with SIFT 59 | kp1, des1 = sift.detectAndCompute(img1,None) 60 | kp2, des2 = sift.detectAndCompute(img2,None) 61 | print len(kp1), len(kp2) 62 | 63 | FLANN_INDEX_KDTREE = 0 64 | index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5) 65 | search_params = dict(checks = 50) 66 | 67 | # flann 68 | flann = cv2.FlannBasedMatcher(index_params, search_params) 69 | matches = flann.knnMatch(des1, des2, k=2) 70 | 71 | # store all the good matches as per Lowe's ratio test. 72 | good = [] 73 | for m,n in matches: 74 | if m.distance < 0.7*n.distance: 75 | good.append(m) 76 | print len(kp1), len(kp2), 'good cnt:', len(good) 77 | 78 | if len(good)*1.0/len(kp1) < 0.5: 79 | #if len(good)>> ' + msg 21 | 22 | 23 | def check_output(cmdstr, shell=True): 24 | output = subprocess.check_output(cmdstr, stderr=subprocess.STDOUT, shell=shell) 25 | return output 26 | 27 | 28 | def run_adb(*args, **kwargs): 29 | cmds = ['adb'] 30 | serialno = kwargs.get('serialno', None) 31 | if serialno: 32 | cmds.extend(['-s', serialno]) 33 | host = kwargs.get('host') 34 | if host: 35 | cmds.extend(['-H', host]) 36 | port = kwargs.get('port') 37 | if port: 38 | cmds.extend(['-P', str(port)]) 39 | cmds.extend(args) 40 | cmds = map(str, cmds) 41 | cmdline = subprocess.list2cmdline(cmds) 42 | return check_output(cmdline, shell=False) 43 | 44 | 45 | def minicap_setup(serialno=None, host=None, port=None): 46 | log("Minicap install started!") 47 | 48 | adb = functools.partial(run_adb, serialno=serialno, host=host, port=port) 49 | 50 | # Figure out which ABI and SDK 51 | log("Get device basic information ...") 52 | abi = adb('shell', 'getprop', 'ro.product.cpu.abi').strip() 53 | sdk = adb('shell', 'getprop', 'ro.build.version.sdk').strip() 54 | tmpdir = tempfile.mkdtemp(prefix='ins-minicap-') 55 | log("Make temp dir ...") 56 | print tmpdir 57 | try: 58 | log("Downloading minicap.so ....") 59 | url = "https://github.com/openstf/stf/raw/master/vendor/minicap/shared/android-"+sdk+"/"+abi+"/minicap.so" 60 | target_path = os.path.join(tmpdir, 'minicap.so') 61 | download(url, target_path) 62 | log("Push data to device ....") 63 | adb('push', target_path, '/data/local/tmp') 64 | 65 | log("Downloading minicap ....") 66 | url = "https://github.com/openstf/stf/raw/master/vendor/minicap/bin/"+abi+"/minicap" 67 | target_path = os.path.join(tmpdir, 'minicap') 68 | download(url, target_path) 69 | log("Push data to device ....") 70 | adb('push', target_path, '/data/local/tmp') 71 | adb('shell', 'chmod', '0755', '/data/local/tmp/minicap') 72 | 73 | log("Checking [dump device info] ...") 74 | print adb('shell', 'LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -i') 75 | log("Minicap install finished !") 76 | finally: 77 | if tmpdir: 78 | log("Cleaning temp dir") 79 | shutil.rmtree(tmpdir) 80 | 81 | 82 | def download(url, target_path): 83 | return urllib.urlretrieve(url, target_path) 84 | 85 | 86 | if __name__ == "__main__": 87 | parser = argparse.ArgumentParser("cli") 88 | parser.add_argument("-s", "--serialno", help="serialno of device", default=None) 89 | parser.add_argument("-H", "--host", help="host of remote device", default=None) 90 | parser.add_argument("-P", "--port", help="port of remote device", default=None) 91 | args = parser.parse_args(sys.argv[1:]) 92 | minicap_setup(serialno=args.serialno, host=args.host, port=args.port) -------------------------------------------------------------------------------- /scripts/install.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | ''' 4 | To install airtest, download get-airtest.py 5 | 6 | python get-airtest.py 7 | 8 | Many requirements will be install automaticly. 9 | 10 | pycrypto-2.6.win32-py2.7 11 | pyparsing-2.0.2.win32-py2.7 12 | numpy-MKL-1.8.1.win32-py2.7 13 | python-dateutil-2.2.win32-py2.7 14 | matplotlib-1.3.1.win32-py2.7 15 | Pillow-2.4.0.win32-py2.7 16 | pip-1.5.6.win32-py2.7 17 | setuptools-5.4.2.win32-py2.7 18 | opencv-python-2.4.9.win32-py2.7 19 | ''' 20 | 21 | 22 | import sys 23 | import platform 24 | import subprocess 25 | 26 | CDN_PREFIX = 'http://goandroid.qiniudn.com/airtest/' 27 | 28 | 29 | def log(msg): 30 | print '>>> LOG:', msg 31 | 32 | 33 | def err(msg): 34 | print '>>> ERR:', msg 35 | raw_input('Press Enter to exit: ') 36 | sys.exit(1) 37 | 38 | 39 | # Check 40 | if not platform.system() == 'Windows': 41 | err('Script only support windows platform') 42 | 43 | pyver = platform.python_version() 44 | if not pyver.startswith('2.7'): 45 | err('Need python version 2.7.*, not %s' % (pyver)) 46 | 47 | try: 48 | import pip 49 | except: 50 | log('Install pip') 51 | # import urllib 52 | # code = urllib.urlopen('https://bootstrap.pypa.io/get-pip.py').read() 53 | # exec code # Will exit, not a ok way. 54 | # print 'Next' 55 | code = subprocess.Popen(['easy_install', CDN_PREFIX+'7-pip-1.5.6.win32-py2.7.exe']).wait() 56 | if code == 0: 57 | subprocess.Popen(['python', sys.argv[0]]).wait() 58 | sys.exit(0) 59 | 60 | installed = {} 61 | for soft in pip.pip.get_installed_distributions(): 62 | installed[soft.project_name] = soft.version 63 | 64 | 65 | requirements = [ 66 | ['pyparsing', '2.0.2', '2-pyparsing-2.0.2.win32-py2.7.exe'], 67 | ['numpy', '1.8.1', '3-numpy-MKL-1.8.1.win32-py2.7.exe'], 68 | ['python-dateutil', '2.2', '4-python-dateutil-2.2.win32-py2.7.exe'], 69 | ['matplotlib', '1.3.1', '5-matplotlib-1.3.1.win32-py2.7.exe'], 70 | ['pillow', '2.4.0', '6-Pillow-2.4.0.win32-py2.7.exe'], 71 | ['opencv-python', '2.4.9', '9-opencv-python-2.4.9.win32-py2.7.exe'], 72 | ['pycrypto', '2.6', '10-pycrypto-2.6.win32-py2.7.exe'] 73 | ] 74 | #['setuptools', '5.4.2', '8-setuptools-5.4.2.win32-py2.7.exe'], 75 | 76 | for name, ver, fname in requirements: 77 | if installed.get(name) == ver: 78 | print 'Already installed "%s"' % name 79 | continue 80 | #print installed.get(name), ver, name 81 | print 'Downloading', name, ver 82 | p = subprocess.Popen(['easy_install', CDN_PREFIX+fname]) 83 | p.wait() 84 | 85 | os.system('pip install -i http://pypi.douban.com/simple airtest') 86 | -------------------------------------------------------------------------------- /scripts/jurassic_park_kitchen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/scripts/jurassic_park_kitchen.jpg -------------------------------------------------------------------------------- /scripts/mac_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | confirm () { 4 | # call with a prompt string or use a default 5 | read -r -p "${1:-Are you sure? [y/N]} " response 6 | case "$response" in 7 | [yY][eE][sS]|[yY]) 8 | true 9 | ;; 10 | *) 11 | false 12 | ;; 13 | esac 14 | } 15 | 16 | brew_install() { 17 | if brew list "$1" &>/dev/null 18 | then 19 | echo "PROGRAM $1 already installed" 20 | else 21 | echo "INSTALL $1 ..." 22 | brew install "$1" 23 | fi 24 | } 25 | 26 | echo "!!!!!!!!!!WARNING!!!!!!!!!!!!!!" 27 | echo "DONOT run this script with root privilege" 28 | echo "Starting setup ios environment for airtest..." 29 | echo 30 | 31 | confirm || exit 1 32 | 33 | echo "This may take a long time..." 34 | 35 | echo "-------Step 1: installing brew" 36 | which brew || ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)" 37 | hash -r 38 | 39 | echo "-------Step 2: installing a brewed python" 40 | brew tap homebrew/science 41 | brew tap homebrew/python 42 | #brew update && brew upgrade 43 | brew_install python 44 | brew_install node 45 | 46 | echo "-------Step 3: installing appium" 47 | which appium || npm install -g appium 48 | 49 | echo "-------Step 4: installing opencv" 50 | brew_install opencv 51 | 52 | echo "-------Step 5: installing pillow" 53 | brew_install pillow 54 | 55 | echo "-------Step 6: check" 56 | echo "python: " `which python` 57 | echo "appium: " `appium -h &>/dev/null && echo ok || echo fail` 58 | echo "opencv:" `python -c "import cv2" && echo ok || echo fail` 59 | echo "appium:" `python -c "from PIL import Image" && echo ok || echo fail` 60 | 61 | echo "-------Step 6: installing airtest" 62 | which pip || easy_install pip 63 | set -eu 64 | pip install virtualenv 65 | pip install Appium-Python-Client 66 | pip install --upgrade -i http://mt.nie.netease.com:3141/simple/ airtest 67 | -------------------------------------------------------------------------------- /scripts/monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | nija test example 6 | ''' 7 | 8 | import sys 9 | import os 10 | import airtest 11 | import time 12 | 13 | serialno = os.getenv('AIRTEST_PHONENO') or '10.242.62.143:5555' 14 | appname = os.getenv('AIRTEST_APPNAME') or 'com.netease.rz' 15 | device = os.getenv('DEVICE') or 'android' 16 | 17 | app = airtest.connect(serialno, appname=appname, device=device) 18 | 19 | try: 20 | while True: 21 | app.sleep(10) 22 | app.takeSnapshot('%d.png' % int(time.time())) 23 | except KeyboardInterrupt: 24 | print 'Exit' 25 | 26 | -------------------------------------------------------------------------------- /scripts/monkey_playback.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env monkeyrunner 2 | # Copyright 2010, The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys 17 | from com.android.monkeyrunner import MonkeyRunner 18 | 19 | # The format of the file we are parsing is very carfeully constructed. 20 | # Each line corresponds to a single command. The line is split into 2 21 | # parts with a | character. Text to the left of the pipe denotes 22 | # which command to run. The text to the right of the pipe is a python 23 | # dictionary (it can be evaled into existence) that specifies the 24 | # arguments for the command. In most cases, this directly maps to the 25 | # keyword argument dictionary that could be passed to the underlying 26 | # command. 27 | 28 | # Lookup table to map command strings to functions that implement that 29 | # command. 30 | CMD_MAP = { 31 | 'TOUCH': lambda dev, arg: dev.touch(**arg), 32 | 'DRAG': lambda dev, arg: dev.drag(**arg), 33 | 'PRESS': lambda dev, arg: dev.press(**arg), 34 | 'TYPE': lambda dev, arg: dev.type(**arg), 35 | 'WAIT': lambda dev, arg: MonkeyRunner.sleep(**arg) 36 | } 37 | 38 | # Process a single file for the specified device. 39 | def process_file(fp, device): 40 | for line in fp: 41 | (cmd, rest) = line.split('|') 42 | try: 43 | # Parse the pydict 44 | rest = eval(rest) 45 | except: 46 | print 'unable to parse options' 47 | continue 48 | 49 | if cmd not in CMD_MAP: 50 | print 'unknown command: ' + cmd 51 | continue 52 | 53 | CMD_MAP[cmd](device, rest) 54 | 55 | 56 | def main(): 57 | file = sys.argv[1] 58 | fp = open(file, 'r') 59 | 60 | device = MonkeyRunner.waitForConnection() 61 | 62 | process_file(fp, device) 63 | fp.close(); 64 | 65 | 66 | if __name__ == '__main__': 67 | main() 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /scripts/monkey_recorder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env monkeyrunner 2 | # Copyright 2010, The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from com.android.monkeyrunner import MonkeyRunner as mr 17 | from com.android.monkeyrunner.recorder import MonkeyRecorder as recorder 18 | 19 | device = mr.waitForConnection() 20 | recorder.start(device) 21 | -------------------------------------------------------------------------------- /scripts/pixelmatch.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import cv2 3 | 4 | method = cv2.TM_CCORR_NORMED 5 | method = cv2.TM_SQDIFF_NORMED 6 | 7 | def locate_img(image, template): 8 | img = image.copy() 9 | res = cv2.matchTemplate(img, template, method) 10 | print res 11 | print res.shape 12 | cv2.imwrite('image/shape.png', res) 13 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) 14 | print cv2.minMaxLoc(res) 15 | if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]: 16 | top_left = min_loc 17 | else: 18 | top_left = max_loc 19 | h, w = template.shape 20 | bottom_right = (top_left[0] + w, top_left[1]+h) 21 | cv2.rectangle(img, top_left, bottom_right, 255, 2) 22 | cv2.imwrite('image/tt.jpg', img) 23 | 24 | image = cv2.imread('image/mule_mobile.png', 0) 25 | #image = cv2.imread('image/mule.png', 0) 26 | template = cv2.imread('image/template.png', 0) 27 | locate_img(image, template) 28 | -------------------------------------------------------------------------------- /scripts/py-interpreter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | import time 6 | 7 | 8 | code = '' 9 | lineno = 0 10 | 11 | def interpreter(*args): 12 | print 'I:', args 13 | time.sleep(0.5) 14 | 15 | for line in open('simple-tcp-proxy.py'): 16 | lineno += 1 17 | contain_code = False 18 | if line.strip() and not line.strip().startswith('#'): 19 | contain_code = True 20 | if contain_code: 21 | code += 'interpreter({}, {})\n'.format(lineno, json.dumps(line.strip())) 22 | code += line 23 | 24 | print code 25 | exec(code, {'interpreter': interpreter}) 26 | 27 | # print line.rstrip() 28 | -------------------------------------------------------------------------------- /scripts/run.bat: -------------------------------------------------------------------------------- 1 | adb push airinput /data/local/tmp/ 2 | adb shell chmod 755 /data/local/tmp/airinput 3 | adb shell /data/local/tmp/airinput test 4 | -------------------------------------------------------------------------------- /scripts/runxinput.bat: -------------------------------------------------------------------------------- 1 | adb push hello /data/local/tmp 2 | adb shell chmod 755 /data/local/tmp/hello 3 | adb shell /data/local/tmp/hello test 4 | -------------------------------------------------------------------------------- /scripts/simple-ide.py: -------------------------------------------------------------------------------- 1 | # USAGE 2 | # python click_and_crop.py --image jurassic_park_kitchen.jpg 3 | 4 | # import the necessary packages 5 | import argparse 6 | import cv2 7 | import Tkinter 8 | import tkSimpleDialog 9 | from PIL import ImageTk, Image 10 | from StringIO import StringIO 11 | 12 | 13 | def make_mouse_callback(imgs, ref_pt): 14 | # initialize the list of reference points and boolean indicating 15 | # whether cropping is being performed or not 16 | cropping = [False] 17 | clone = imgs[0] 18 | 19 | def _click_and_crop(event, x, y, flags, param): 20 | # grab references to the global variables 21 | # global ref_pt, cropping 22 | 23 | # if the left mouse button was clicked, record the starting 24 | # (x, y) coordinates and indicate that cropping is being 25 | # performed 26 | if event == cv2.EVENT_LBUTTONDOWN: 27 | ref_pt[0] = (x, y) 28 | cropping[0] = True 29 | 30 | # check to see if the left mouse button was released 31 | elif event == cv2.EVENT_LBUTTONUP: 32 | # record the ending (x, y) coordinates and indicate that 33 | # the cropping operation is finished 34 | ref_pt[1] = (x, y) 35 | cropping[0] = False 36 | 37 | # draw a rectangle around the region of interest 38 | imgs[1] = image = clone.copy() 39 | cv2.rectangle(image, ref_pt[0], ref_pt[1], (0, 255, 0), 2) 40 | cv2.imshow("image", image) 41 | elif event == cv2.EVENT_MOUSEMOVE and cropping[0]: 42 | img2 = clone.copy() 43 | cv2.rectangle(img2, ref_pt[0], (x, y), (0, 255, 0), 2) 44 | imgs[1] = image = img2 45 | cv2.imshow("image", image) 46 | return _click_and_crop 47 | 48 | def interactive_save(image): 49 | img_str = cv2.imencode('.png', image)[1].tostring() 50 | imgpil = Image.open(StringIO(img_str)) 51 | 52 | root = Tkinter.Tk() 53 | root.geometry('{}x{}'.format(400, 400)) 54 | imgtk = ImageTk.PhotoImage(image=imgpil) 55 | panel = Tkinter.Label(root, image=imgtk) #.pack() 56 | panel.pack(side="bottom", fill="both", expand="yes") 57 | Tkinter.Button(root, text="Hello!").pack() 58 | save_to = tkSimpleDialog.askstring("Save cropped image", "Enter filename") 59 | if save_to: 60 | if save_to.find('.') == -1: 61 | save_to += '.png' 62 | print 'Save to:', save_to 63 | cv2.imwrite(save_to, image) 64 | root.destroy() 65 | 66 | def main(): 67 | # construct the argument parser and parse the arguments 68 | ap = argparse.ArgumentParser() 69 | ap.add_argument("-i", "--image", required=True, help="Path to the image") 70 | args = vars(ap.parse_args()) 71 | 72 | # load the image, clone it, and setup the mouse callback function 73 | image = cv2.imread(args["image"]) 74 | clone = image.copy() 75 | images = [clone, image] 76 | ref_pt = [None, None] 77 | 78 | cv2.namedWindow("image") 79 | cv2.setMouseCallback("image", make_mouse_callback(images, ref_pt)) 80 | 81 | # keep looping until the 'q' key is pressed 82 | while True: 83 | # display the image and wait for a keypress 84 | cv2.imshow("image", images[1]) 85 | key = cv2.waitKey(1) & 0xFF 86 | 87 | # if the 'c' key is pressed, break from the loop 88 | if key == ord("c"): 89 | if ref_pt[1] is None: 90 | continue 91 | roi = clone[ref_pt[0][1]:ref_pt[1][1], ref_pt[0][0]:ref_pt[1][0]] 92 | interactive_save(roi) 93 | elif key == ord("q"): 94 | break 95 | 96 | # if there are two reference points, then crop the region of interest 97 | # from teh image and display it 98 | 99 | # close all open windows 100 | cv2.destroyAllWindows() 101 | 102 | if __name__ == '__main__': 103 | main() -------------------------------------------------------------------------------- /scripts/simple-tcp-proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import tornado.ioloop 3 | import maproxy.proxyserver 4 | import socket 5 | 6 | # HTTP->HTTP: On your computer, browse to "http://127.0.0.1:81/" and you'll get http://www.google.com 7 | server = maproxy.proxyserver.ProxyServer("127.0.0.1", 26944) 8 | server.listen(5555) 9 | print("Local IP:", socket.gethostbyname(socket.gethostname())) 10 | print("0.0.0.0:5555 -> 127.0.0.1:26944") 11 | tornado.ioloop.IOLoop.instance().start() -------------------------------------------------------------------------------- /scripts/snow/README.txt: -------------------------------------------------------------------------------- 1 | ## Trigger by WEB 2 | ``` 3 | curl -XPOST demo.automatorx.org/runtest 4 | ``` 5 | -------------------------------------------------------------------------------- /scripts/snow/static/js/index.js: -------------------------------------------------------------------------------- 1 | /* Javascrpit */ 2 | -------------------------------------------------------------------------------- /scripts/snow/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} {% block js %} 2 | 3 | 4 | {% end %} {% block content %} 5 |
6 |
7 | 运行测试 简化版 8 |
9 |
10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | {% end %} 19 | -------------------------------------------------------------------------------- /scripts/snow/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Snow is back 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 50 |
51 | {% block content %} {% end %} 52 |
53 | 54 | 55 | 56 | 57 | 58 | {% block js %} {% end %} 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /scripts/snow/templates/runtest.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} {% block js %} 2 | 3 | 4 | {% end %} {% block content %} 5 |
6 |
7 | 当前进度 简化版 8 |
9 |
10 | 脚本安装中 11 |
12 |
13 | {% end %} 14 | -------------------------------------------------------------------------------- /scripts/snow/web.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import subprocess32 as subprocess 4 | import tornado 5 | import tornado.web 6 | import tornado.ioloop 7 | import tornado.websocket 8 | 9 | from tornado.concurrent import run_on_executor 10 | from concurrent.futures import ThreadPoolExecutor # `pip install futures` for python2 11 | 12 | 13 | class MainHandler(tornado.web.RequestHandler): 14 | def get(self): 15 | self.render('index.html') 16 | 17 | 18 | class TestHandler(tornado.web.RequestHandler): 19 | executor = ThreadPoolExecutor(max_workers=1) 20 | output = '' 21 | running = False 22 | 23 | @run_on_executor 24 | def background_test(self): 25 | self.running = True 26 | proc = subprocess.Popen('echo hello', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 27 | while True: 28 | line = proc.stdout.readline() 29 | if line == '': 30 | break 31 | print line 32 | for client in ProgressHandler.clients: 33 | client.write_message(line) 34 | self.output = self.output + line 35 | self.running = False 36 | 37 | def get(self): 38 | self.render('runtest.html') 39 | 40 | def post(self): 41 | if not self.running: 42 | self.background_test() 43 | self.render('runtest.html', running=self.running) 44 | 45 | 46 | class ProgressHandler(tornado.websocket.WebSocketHandler): 47 | clients = set() 48 | 49 | def open(self): 50 | print 'websocket connected' 51 | self.write_message(TestHandler.output) 52 | self.clients.add(self) 53 | 54 | def on_close(self): 55 | self.clients.remove(self) 56 | print "WebSocket closed" 57 | 58 | def check_origin(self, origin): 59 | return True 60 | 61 | 62 | def make_app(**settings): 63 | settings['template_path'] = 'templates' 64 | settings['static_path'] = 'static' 65 | return tornado.web.Application([ 66 | (r"/", MainHandler), 67 | (r"/runtest", TestHandler), 68 | (r"/ws/progress", ProgressHandler), 69 | ], **settings) 70 | 71 | 72 | def main(): 73 | app = make_app(debug=True) 74 | app.listen(4000) 75 | print 'listening on port 4000' 76 | tornado.ioloop.IOLoop.current().start() 77 | 78 | 79 | if __name__ == '__main__': 80 | main() -------------------------------------------------------------------------------- /scripts/surfaceflinger-fps.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # author: codeskyblue(hzsunshx) 4 | # created: 2016-05-16 5 | # modified: 2016-08-19 6 | # 7 | # Experiment of caculate FPS of android without root 8 | # code reference from chromiumwebapps/chromium 9 | # 10 | # It is insteresting to know this way which can calculate not only fps, but also every frame use time. 11 | # Is it really useful, I don't know, really don't know. 12 | # so code just stop here. 13 | 14 | import time 15 | import subprocess 16 | import re 17 | 18 | nanoseconds_per_second = 1e9 19 | 20 | # view = 'SurfaceView' 21 | 22 | def get_top_view(): 23 | out = subprocess.check_output(['adb', 'shell', 'dumpsys', 'SurfaceFlinger']) 24 | lines = out.replace('\r\n', '\n').splitlines() 25 | max_area = 0 26 | top_view = None 27 | for index, line in enumerate(lines): 28 | line = line.strip() 29 | if not line.startswith('+ Layer '): 30 | continue 31 | m = re.search(r'\((.+)\)', line) 32 | if not m: 33 | continue 34 | view_name = m.group(1) 35 | (x0, y0, x1, y1) = map(int, re.findall(r'(\d+)', lines[index+4])) 36 | cur_area = (x1-x0) * (y1-y0) 37 | if cur_area > max_area: 38 | max_area = cur_area 39 | top_view = view_name 40 | return top_view 41 | 42 | 43 | def init_frame_data(view): 44 | out = subprocess.check_output(['adb', 'shell', 'dumpsys', 'SurfaceFlinger', '--latency-clear', view]) 45 | if out.strip() != '': 46 | raise RuntimeError("Not supported.") 47 | time.sleep(0.1) 48 | (refresh_period, timestamps) = frame_data(view) 49 | base_timestamp = 0 50 | base_index = 0 51 | for timestamp in timestamps: 52 | if timestamp != 0: 53 | base_timestamp = timestamp 54 | break 55 | base_index += 1 56 | 57 | if base_timestamp == 0: 58 | raise RuntimeError("Initial frame collect failed") 59 | return (refresh_period, base_timestamp, timestamps[base_index:]) 60 | 61 | 62 | def frame_data(view): 63 | out = subprocess.check_output(['adb', 'shell', 'dumpsys', 'SurfaceFlinger', '--latency', view]) 64 | results = out.splitlines() 65 | refresh_period = long(results[0]) / nanoseconds_per_second 66 | timestamps = [] 67 | for line in results[1:]: 68 | fields = line.split() 69 | if len(fields) != 3: 70 | continue 71 | (start, submitting, submitted) = map(int, fields) 72 | if submitting == 0: 73 | continue 74 | 75 | timestamp = submitting/nanoseconds_per_second 76 | timestamps.append(timestamp) 77 | return (refresh_period, timestamps) 78 | 79 | 80 | def continue_collect_frame_data(): 81 | view = get_top_view() 82 | if view is None: 83 | raise RuntimeError("Fail to get current SurfaceFlinger view") 84 | print 'Current view:', view 85 | 86 | refresh_period, base_timestamp, timestamps = init_frame_data(view) 87 | while True: 88 | refresh_period, tss = frame_data(view) 89 | last_index = 0 90 | if timestamps: 91 | recent_timestamp = timestamps[-2] 92 | last_index = tss.index(recent_timestamp) 93 | timestamps = timestamps[:-2] + tss[last_index:] 94 | 95 | time.sleep(1.5) 96 | 97 | ajusted_timestamps = [] 98 | for seconds in timestamps[:]: 99 | seconds -= base_timestamp 100 | if seconds > 1e6: # too large, just ignore 101 | continue 102 | ajusted_timestamps.append(seconds) 103 | 104 | from_time = ajusted_timestamps[-1] - 1.0 105 | fps_count = 0 106 | for seconds in ajusted_timestamps: 107 | if seconds > from_time: 108 | fps_count += 1 109 | print fps_count, len(ajusted_timestamps) 110 | 111 | # print ajusted_timestamps 112 | 113 | 114 | if __name__ == '__main__': 115 | continue_collect_frame_data() 116 | -------------------------------------------------------------------------------- /scripts/test-insert-code.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Insert code into files 4 | # 5 | 6 | def insert_code(filename, code, save=True, marker='# ATX CODE'): 7 | content = '' 8 | for line in open(filename, 'rb'): 9 | if line.strip() == marker: 10 | cnt = line.find(marker) 11 | content += line[:cnt] + code 12 | content += line 13 | if save: 14 | with open(filename, 'wb') as f: 15 | f.write(content) 16 | return content 17 | 18 | if __name__ == '__main__': 19 | insert_code('README.md', 'hello world\n') 20 | -------------------------------------------------------------------------------- /scripts/uiautomation/instruments-test.js: -------------------------------------------------------------------------------- 1 | /* The usage is some like jQuery */ 2 | 3 | var mechanic = (function() { 4 | var target = UIATarget.localTarget(); 5 | var app = target.frontMostApp(), 6 | window = app.mainWindow(), 7 | emptyArray = [], 8 | slice = emptyArray.slice; 9 | 10 | target.setTimeout(0); 11 | 12 | function $() {} 13 | 14 | $.extend = function(target) { 15 | var key; 16 | slice.call(arguments, 1).forEach(function(source) { 17 | for (key in source) target[key] = source[key]; 18 | }); 19 | return target; 20 | }; 21 | 22 | $.inArray = function(elem, array, i) { 23 | return emptyArray.indexOf.call(array, elem, i); 24 | }; 25 | 26 | $.map = function(elements, callback) { 27 | var value, values = [], 28 | i, key; 29 | if (likeArray(elements)) { 30 | for (i = 0; i < elements.length; i++) { 31 | value = callback(elements[i], i); 32 | if (value != null) values.push(value); 33 | } 34 | } else { 35 | for (key in elements) { 36 | value = callback(elements[key], key); 37 | if (value != null) values.push(value); 38 | } 39 | } 40 | return flatten(values); 41 | }; 42 | 43 | $.each = function(elements, callback) { 44 | var i, key; 45 | if (likeArray(elements)) { 46 | for (i = 0; i < elements.length; i++) { 47 | if (callback.call(elements[i], i, elements[i]) === false) return elements; 48 | } 49 | } else { 50 | for (key in elements) { 51 | if (callback.call(elements[key], key, elements[key]) === false) return elements; 52 | } 53 | } 54 | return elements; 55 | }; 56 | 57 | return $; 58 | })(); 59 | 60 | 61 | var $ = $ || mechanic; 62 | 63 | (function($) { 64 | var target = UIATarget.localTarget(); 65 | 66 | $.extend($, { 67 | debug: UIALogger.logMessage, 68 | cmd: function(path, args, timeout) { 69 | return target.host().performTaskWithPathArgumentsTimeout(path, args, timeout); 70 | }, 71 | delay: function(seconds) { 72 | target.delay(seconds); 73 | }, 74 | orientation: function(orientation) { 75 | if (orientation === undefined || orientation === null) return target.deviceOrientation(); 76 | else target.setDeviceOrientation(orientation); 77 | }, 78 | }) 79 | 80 | 81 | $.extend($, { 82 | error: function(s) { 83 | UIALogger.logError(s); 84 | }, 85 | warn: function(s) { 86 | UIALogger.logWarning(s); 87 | }, 88 | debug: function(s) { 89 | UIALogger.logDebug(s); 90 | }, 91 | message: function(s) { 92 | UIALogger.logMessage(s); 93 | }, 94 | rotate: function(options) { 95 | target.rotateWithOptions(options); 96 | }, 97 | }) 98 | })(mechanic); 99 | 100 | // var app = target.frontMostApp(); 101 | // var window = app.mainWindow(); 102 | // //target.logElementTree(); 103 | // var host = target.host(); 104 | var target = UIATarget.localTarget(); 105 | 106 | // $.debug("Hello" + JSON.stringify(target.rect())) 107 | $.message("Hello message") 108 | 109 | while (true) { 110 | $.message("Wait for command") 111 | var result = $.cmd('/usr/bin/head', ['-n1', 'test.pipe'], 10); 112 | $.debug("exitCode: " + result.exitCode); 113 | $.debug("stdout: " + result.stdout); 114 | $.debug("stderr: " + result.stderr); 115 | // $.message("delay 1s") 116 | // $.delay(1) 117 | 118 | if (result.exitCode == 0) { 119 | var rawRes = eval(result.stdout); 120 | var res = JSON.stringify(rawRes); 121 | $.cmd('./write_pipe.sh', [res], 5); 122 | } 123 | } 124 | // $.delay(10) 125 | // target.tap({x: 357, y: 350}) 126 | // $.delay(10) -------------------------------------------------------------------------------- /scripts/uiautomation/run_instruments.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | # 3 | 4 | NAME=${NAME:?} 5 | UDID=$(idevice_id -l) 6 | TEST="./instruments-test.js" 7 | 8 | TRACETEMPLATE="/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.xrplugin/Contents/Resources/Automation.tracetemplate" 9 | 10 | instruments -w $UDID -t "$TRACETEMPLATE" $NAME -e UIASCRIPT $TEST -------------------------------------------------------------------------------- /scripts/uiautomation/runjs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | # 3 | 4 | set -eu 5 | 6 | sh write_pipe.sh "$@" 7 | head -n1 test.pipe 8 | -------------------------------------------------------------------------------- /scripts/uiautomation/write_pipe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | # 3 | 4 | CURDIR="$(cd $(dirname $0); pwd)" 5 | echo "$@" > $CURDIR/test.pipe 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = atx 3 | author = codeskyblue 4 | author-email = codeskyblue@gmail.com 5 | summary = Automation test library for android based on opencv 6 | license = Apache 7 | description-file = 8 | ABOUT.rst 9 | home-page = https://github.com/codeskyblue/AirtestX 10 | requires-python = >=2.7,<=3.5 11 | classifier = 12 | Environment :: Console 13 | Intended Audience :: Developers 14 | Operating System :: OS Independent 15 | Programming Language :: Python :: 2.7 16 | Programming Language :: Python :: 3.4 17 | Programming Language :: Python :: 3.5 18 | 19 | [files] 20 | packages = 21 | atx 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # 4 | # Licensed under Apache 2.0 5 | # 6 | 7 | import setuptools 8 | setuptools.setup(setup_requires=['pbr'], pbr=True) 9 | -------------------------------------------------------------------------------- /tests/.fsw.yml: -------------------------------------------------------------------------------- 1 | desc: Auto generated by fswatch [tests] 2 | triggers: 3 | - name: "" 4 | pattens: 5 | - '**/*.go' 6 | - '**/*.c' 7 | - '**/*.py' 8 | env: 9 | DEBUG: "1" 10 | PYTHONPATH: "/f/gitworkspace/airtest" 11 | cmd: python test.py 12 | shell: true 13 | delay: 100ms 14 | signal: KILL 15 | watch_paths: 16 | - . 17 | watch_depth: 5 18 | -------------------------------------------------------------------------------- /tests/README.txt: -------------------------------------------------------------------------------- 1 | 测试方法 2 | 3 | pip install -r requirements.txt 4 | 5 | 6 | py.test --color=no -v test_dummy.py 7 | -------------------------------------------------------------------------------- /tests/media/dummy_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/tests/media/dummy_screen.png -------------------------------------------------------------------------------- /tests/media/haima.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/tests/media/haima.png -------------------------------------------------------------------------------- /tests/media/system-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetEaseGame/ATX/f4415c57b45cb0730e08899cbc92a2af1c047ffb/tests/media/system-app.png -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | mock 3 | -------------------------------------------------------------------------------- /tests/runtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | # Skip all tests in travis 5 | #test -n "${TRAVIS}" && exit 0 6 | 7 | 8 | export PYTHONPATH=$PWD:$PYTHONPATH 9 | cd $(dirname $0) 10 | 11 | python -mpytest -v \ 12 | test_ext_report.py \ 13 | test_dummy.py \ 14 | test_strutils.py \ 15 | test_base.py "$@" 16 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import time 5 | import atx 6 | import logging 7 | from atx import consts 8 | print 'Version:', atx.version 9 | 10 | d = atx.connect(None) 11 | 12 | # 你好 13 | 14 | def screenshot(): 15 | start = time.time() 16 | #d.screenshot_method = consts.SCREENSHOT_METHOD_MINICAP 17 | d.screenshot('ttt.png') 18 | print time.time() - start 19 | 20 | def start_app(): 21 | d.start_app('com.netease.txx') 22 | 23 | def stop_app(): 24 | d.stop_app('com.netease.txx', clear=True) 25 | 26 | def touch(): 27 | d.screenshot_method = consts.SCREENSHOT_METHOD_MINICAP 28 | d.touch_image('button.png') 29 | 30 | if __name__ == '__main__': 31 | log = logging.getLogger('atx') 32 | log.setLevel(logging.DEBUG) 33 | 34 | # d.screen.off() 35 | 36 | #def foo(evt): 37 | #print 'good', evt 38 | #d.click(*evt.pos) 39 | 40 | #with d.watch('simulator', 10) as w: 41 | #w.on(atx.Pattern("mmm.png", offset=(-79, -13))).do(foo).quit() 42 | # # stop_app() 43 | #print 'inside' 44 | #screenshot() 45 | # print d.dump_nodes() 46 | # w.on('setting.png', atx.Watcher.ACTION_TOUCH) 47 | # w.on('common.png', atx.Watcher.ACTION_TOUCH) 48 | 49 | # wid = d.add_watcher(w) 50 | # d.del_watcher(wid) 51 | # while 1: 52 | # screenshot() 53 | # screenshot() 54 | # touch() 55 | -------------------------------------------------------------------------------- /tests/test_android.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import cv2 6 | import time 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | import scipy.fftpack 10 | from cv2 import cv 11 | 12 | def test_screenshot(): 13 | from atx.drivers.android import AndroidDevice 14 | dev = AndroidDevice() 15 | print 'screen display:', dev.display 16 | # screen = dev._screenshot_uiauto() 17 | for i in range(10): 18 | t0 = time.clock() 19 | # screen = dev.screenshot() 20 | screen = dev._screenshot_minicap() 21 | print time.clock() - t0 22 | print dev.screenshot_method 23 | screen.save('tmp.png') 24 | 25 | # def run_test(): 26 | # d = SomeDevice() 27 | # d.connect() //setup info, screen 28 | # d.reset() 29 | 30 | # d.info.serial 31 | # d.info.wlan_ip 32 | # d.info.sreensize 33 | 34 | # d.screen = Screen(device) 35 | # d.screen.resolution screen 1 thread 36 | # d.screen.orientation orientation 1 thread 37 | # d.screen.click(x, y) 38 | # d.screen.save('hello.png') 39 | # d.screen.region(l,t,w,h).save('region.png') 40 | # d.screen.search('xxx.png') 41 | # d.screen.exists('xxx.png') 42 | # d.screen.on() 43 | # d.screen.off() 44 | 45 | # # for android 46 | # d.keys.home() 47 | # d.keys.volup() 48 | # d.keys.voldown() 49 | 50 | # # for windows 51 | # d.text('hello') 52 | 53 | # ## short cuts 54 | # d.controls.install(pkg) uiautomator.device.server.adb 55 | # d.controls.uninstall(pkg) 56 | # d.controls.startapp() 57 | # d.controls.stopapp() 58 | # d.controls.reboot() 59 | 60 | # # recorder.listener.start() 61 | # # recorder.listener.stop() 62 | # # recorder.listener.on_touch_down() listener 1 thread 63 | # # recorder.listener.on_touch_move() 64 | # # recorder.listener.on_touch_up() 65 | # # recorder.listener.on_click() 66 | # # recorder.listener.on_drag() 67 | 68 | # w = d.watcher() 69 | # w.wait('xxx.png', 5).click(x,y).expect('xxx.png', 3) --> Exception means fail 70 | # w.wait(1).click(x,y) 71 | # w.exists('xxx.png') 72 | 73 | def test_minicap(): 74 | from atx.drivers.android_minicap import AndroidDeviceMinicap 75 | 76 | cv2.namedWindow("preview") 77 | d = AndroidDeviceMinicap() 78 | 79 | while True: 80 | try: 81 | h, w = d._screen.shape[:2] 82 | img = cv2.resize(d._screen, (w/2, h/2)) 83 | cv2.imshow('preview', img) 84 | key = cv2.waitKey(1) 85 | if key == 100: # d for dump 86 | filename = time.strftime('%Y%m%d%H%M%S.png') 87 | cv2.imwrite(filename, d._screen) 88 | except KeyboardInterrupt: 89 | break 90 | cv2.destroyWindow('preview') 91 | 92 | def test_minitouch(): 93 | from atx.drivers.android_minicap import SubAdb 94 | 95 | adb = SubAdb() 96 | adb.start_minitouch() 97 | adb.home() 98 | for pos in ((100, 200), (1000, 200), ):#(100, 1900), (1000, 1900)): 99 | adb.touch(*pos) 100 | time.sleep(1) 101 | for i in range(10): 102 | adb.swipe(100, 100, 500, 100) 103 | time.sleep(1) 104 | adb.swipe(500, 100, 100, 100) 105 | time.sleep(1) 106 | return adb 107 | 108 | 109 | if __name__ == '__main__': 110 | # test_screenshot() 111 | test_minicap() 112 | # adb = test_minitouch() -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from atx import base 5 | from atx import strutils 6 | 7 | 8 | def test_list_all_image(): 9 | images = base.list_all_image('media') 10 | images = list(images) 11 | assert len(images) > 0 12 | #print list(images) 13 | #assert 'media/haima.png' in images or 'media\\haima.png' in images 14 | 15 | 16 | def test_image_name_match(): 17 | match = base.image_name_match 18 | assert match('foo.png', 'foo.png') 19 | assert match('./foo.png', 'foo.png') 20 | assert match('.//foo.png', './foo.png') 21 | assert match('foo', 'foo.png') 22 | assert match('foo', 'foo.PNG') 23 | assert match('foo', 'foo.jpg') 24 | assert match('foo', 'bar/foo.png') 25 | assert match('foo', 'bar/./foo.png') 26 | assert match('foo', 'foo@rsl(1280x720).png') 27 | assert match('foo', 'bar/foo@rsl(1280x720).png') 28 | assert match('./foo', 'bar/foo@rsl(1280x720).png') 29 | assert not match('foo.txt', 'foo.png') 30 | assert not match('foo.jpg', 'foo.png') 31 | assert not match('bar/foo', 'foo.jpg') 32 | 33 | 34 | def test_search_image(): 35 | imgpath = base.search_image('haima', path=['media']) 36 | assert imgpath is not None 37 | assert strutils.encode('haima.png') in imgpath 38 | assert strutils.encode('media') in imgpath 39 | 40 | 41 | def test_filename_match(): 42 | # @auto is supported 43 | assert base.filename_match('fight@auto.png', 'fight@4x3.png', 4, 3) == True 44 | assert base.filename_match('fight@auto.png', 'fight@4x3.png', 3, 4) == True 45 | assert base.filename_match('fight@auto.png', 'fight.4x3.png', 3, 4) == True 46 | assert base.filename_match('fight@auto.png', 'fight.3x4.png', 3, 4) == True 47 | 48 | assert base.filename_match('fight@auto.png', 'fight.16x9.png', 3, 4) == False 49 | assert base.filename_match('fight@auto.png', 'fight@16x9.png', 0, 0) == False 50 | 51 | # not support @wxh now 52 | assert base.filename_match('fight@3x4.png', 'fight@3x4.png', 3, 4) == True 53 | 54 | # normal 55 | assert base.filename_match('fight.png', 'fight.png', 3, 4) == True 56 | assert base.filename_match('fight.png', 'fight.png', 0, 0) == True 57 | assert base.filename_match('fight.png', 'fight.jpg', 0, 0) == False 58 | # assert base.filename_match('fight.3x4.png', 'fight.3x4.png', 3, 4) == True 59 | # assert base.filename_match('fight@auto.png', 'fight@4x3.png', 15, 20) == False 60 | 61 | 62 | if __name__ == '__main__': 63 | test_filename_match() 64 | -------------------------------------------------------------------------------- /tests/test_dummy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import time 6 | import mock 7 | 8 | import pytest 9 | import atx 10 | from PIL import Image 11 | 12 | d = atx.connect(platform='dummy') 13 | 14 | def setup_function(f): 15 | d.resolution = (1280, 720) 16 | 17 | 18 | def test_setget_resolution(): 19 | assert d.resolution == (720, 1280) 20 | 21 | d.resolution = None # None is also OK to set 22 | assert d.resolution is None 23 | 24 | d.resolution = (200, 400) 25 | assert d.resolution == (200, 400) 26 | 27 | with pytest.raises(TypeError): 28 | d.resolution = [1, 3] 29 | with pytest.raises(TypeError): 30 | d.resolution = 720 31 | assert d.resolution == (200, 400) 32 | 33 | 34 | def teardown_function(f): 35 | print('teardown') 36 | 37 | 38 | def test_screenshot_normal(): 39 | screen = d.screenshot() 40 | assert screen is not None 41 | 42 | 43 | def test_screenshot_first_fail(): 44 | d._fail_first_screenshot = True 45 | screen = d.screenshot() 46 | assert screen is not None 47 | 48 | 49 | def test_screenshot_always_fail(): 50 | import types 51 | d = atx.connect(platform='dummy') 52 | 53 | def raise_ioerror(self): 54 | raise IOError('error of io') 55 | 56 | d._take_screenshot = types.MethodType(raise_ioerror, d) 57 | with pytest.raises(IOError): 58 | d.screenshot() 59 | 60 | 61 | def test_hook_screenshot(): 62 | called = [False] 63 | 64 | def hook(event): 65 | print('event', event) 66 | called[0] = True 67 | 68 | d.add_listener(hook, atx.EVENT_SCREENSHOT) 69 | d.screenshot() 70 | assert called[0] == True 71 | 72 | #def test_cloudtest_hook(): 73 | # cloudtest.record_operation(d) 74 | # d.screenshot() 75 | 76 | def test_region_screenshot(): 77 | nd = d.region(atx.Bounds(100, 100, 600, 300)) 78 | rs = nd.region_screenshot() 79 | assert rs is not None 80 | assert rs.size == (500, 200) 81 | 82 | # def test_assert_exists(): 83 | # d.assert_exists('media/system-app.png') 84 | 85 | # with pytest.raises(atx.AssertExistsError): 86 | # d.assert_exists('media/haima.png', timeout=0.1) 87 | 88 | def test_click(): 89 | d.click(50, 70) 90 | assert d.last_click == (50, 70) 91 | 92 | def test_click_image(): 93 | """ require aircv installed """ 94 | d.click_image('media/system-app.png') 95 | assert d.last_click == (139, 299) 96 | 97 | def test_click_image_offset1(): 98 | d.click_image(atx.Pattern('media/system-app.png')) 99 | assert d.last_click == (139, 299) 100 | 101 | def test_click_image_offset2(): 102 | d.click_image(atx.Pattern('media/system-app.png', offset=(0.5, 0.5))) 103 | w, h = Image.open('media/system-app.png').size 104 | assert d.last_click == (139+w/2, 299+h/2) 105 | -------------------------------------------------------------------------------- /tests/test_ext_gt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # hzsunshx <2016-06-12> 5 | # make sure only one phone connected to pc/ 6 | 7 | import time 8 | 9 | import atx 10 | from atx.ext.gt import GT 11 | 12 | 13 | d = atx.connect() 14 | 15 | def test_gt(): 16 | gt = GT(d) 17 | gt.start_test('com.netease.my') 18 | print 'test started. wait 5s' 19 | time.sleep(10.0) 20 | gt.stop_and_save() 21 | print 'save perf data' 22 | # time.sleep(3.0) 23 | # gt.quit() 24 | 25 | 26 | if __name__ == '__main__': 27 | test_gt() -------------------------------------------------------------------------------- /tests/test_ext_report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # hzsunshx <2016-06-12> 5 | # make sure only one phone connected to pc/ 6 | 7 | import unittest 8 | import shutil 9 | from collections import namedtuple 10 | from mock import MagicMock, patch 11 | 12 | import atx 13 | from atx.ext.report import Report 14 | from PIL import Image 15 | 16 | 17 | class TestExtReport(unittest.TestCase): 18 | def setUp(self): 19 | self.d = atx.connect(platform='dummy') 20 | self.rp = Report(self.d, save_dir='tmp_report') 21 | 22 | def tearDown(self): 23 | self.rp.close() 24 | shutil.rmtree('tmp_report') 25 | 26 | def test_screenshot(self): 27 | assert self.rp.last_screenshot is None 28 | self.d.screenshot() 29 | assert isinstance(self.rp.last_screenshot, Image.Image) 30 | -------------------------------------------------------------------------------- /tests/test_imutils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import pytest 6 | import string 7 | 8 | import cv2 9 | from atx import imutils 10 | 11 | 12 | def test_mark_point(): 13 | im = cv2.imread('media/system-app.png') 14 | im = imutils.mark_point(im, 50, 50) 15 | cv2.imwrite('tmp.png', im) -------------------------------------------------------------------------------- /tests/test_mixin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from atx.drivers import Pattern 5 | 6 | 7 | def test_pattern_offset(): 8 | pt = Pattern("mixin.L50B50.png") 9 | assert pt.offset == (-0.5, 0.5) 10 | 11 | pt = Pattern("mixin.T50R50.png") 12 | assert pt.offset == (0.5, -0.5) 13 | 14 | pt = Pattern("mixin.T50R50.png") 15 | assert pt.offset == (0.5, -0.5) 16 | -------------------------------------------------------------------------------- /tests/test_record.py: -------------------------------------------------------------------------------- 1 | #-*- encoding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import time 6 | 7 | from atx.drivers.windows import WindowsDevice, find_process_id 8 | from atx.drivers.android import AndroidDevice 9 | from atx.cmds.record import RecorderGUI 10 | 11 | def get_calc_win(): 12 | exe_file = "C:\\Windows\\System32\\calc.exe" 13 | if not find_process_id(exe_file): 14 | os.startfile(exe_file) 15 | time.sleep(3) 16 | 17 | win = WindowsDevice(exe_file=exe_file) 18 | print "window handle", hex(win.hwnd) 19 | return win 20 | 21 | def get_game_win(): 22 | window_name = "MyLuaGame" 23 | win = WindowsDevice(window_name=window_name) 24 | print "window handle", hex(win.hwnd) 25 | return win 26 | 27 | def get_android_dev(): 28 | dev = AndroidDevice() 29 | print 'android devcie', dev._serial 30 | return dev 31 | 32 | if len(sys.argv) > 1 and sys.argv[1] == 'win': 33 | get_device = get_calc_win 34 | else: 35 | get_device = get_android_dev 36 | 37 | def main(): 38 | dev = get_device() 39 | print "display size %dx%d" % dev.display 40 | 41 | r = RecorderGUI(dev) 42 | r.mainloop() 43 | 44 | if __name__ == '__main__': 45 | main() -------------------------------------------------------------------------------- /tests/test_scene_detector.py: -------------------------------------------------------------------------------- 1 | #-*- encoding: utf-8 -*- 2 | 3 | import cv2 4 | import time 5 | 6 | from atx.drivers.android_minicap import AndroidDeviceMinicap 7 | from atx.record.scene_detector import SceneDetector 8 | 9 | def test_detect(): 10 | dev = AndroidDeviceMinicap() 11 | dev._adb.start_minitouch() 12 | time.sleep(3) 13 | 14 | d = SceneDetector('txxscene') 15 | old, new = None, None 16 | while True: 17 | # time.sleep(0.3) 18 | screen = dev.screenshot_cv2() 19 | h, w = screen.shape[:2] 20 | img = cv2.resize(screen, (w/2, h/2)) 21 | 22 | # find hsv 23 | hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) 24 | hls = cv2.cvtColor(img, cv2.COLOR_BGR2HLS) 25 | _, _, V = cv2.split(hsv) 26 | V[V<150] = 0 27 | cv2.imshow('V', V) 28 | _, _, L = cv2.split(hls) 29 | L[L<150] = 0 30 | cv2.imshow('H', L) 31 | 32 | tic = time.clock() 33 | new = str(d.detect(img)) 34 | t = time.clock() - tic 35 | if new != old: 36 | print 'change to', new 37 | print 'cost time', t 38 | old = new 39 | 40 | for _, r in d.current_scene: 41 | x, y, x1, y1 = r 42 | cv2.rectangle(img, (x,y), (x1,y1), (0,255,0) ,2) 43 | cv2.imshow('test', img) 44 | cv2.waitKey(1) 45 | 46 | if __name__ == '__main__': 47 | test_detect() -------------------------------------------------------------------------------- /tests/test_strutils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from atx import strutils 5 | import six 6 | 7 | 8 | def test_encode(): 9 | v = strutils.encode('hello') 10 | assert not isinstance(v, six.text_type) 11 | 12 | v = strutils.encode(u'hello') 13 | assert not isinstance(v, six.text_type) 14 | 15 | 16 | def test_to_string(): 17 | v = strutils.to_string('hello') 18 | assert isinstance(v, str) # no matter py2 or py3 19 | -------------------------------------------------------------------------------- /tests/test_windows.py: -------------------------------------------------------------------------------- 1 | #-*- encoding: utf-8 -*- 2 | 3 | import os 4 | import time 5 | 6 | from atx.drivers.windows import Window, FrozenWindow, WindowsDevice 7 | 8 | 9 | # def _input_left_mouse(self, x, y): 10 | # left, top, right, bottom = self.position 11 | # width, height = right - left, bottom - top 12 | # if x < 0 or x > width or y < 0 or y > height: 13 | # return 14 | 15 | # win32gui.SetForegroundWindow(self._handle) 16 | # pos = win32gui.GetCursorPos() 17 | # win32api.SetCursorPos((left+x, top+y)) 18 | # win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0) 19 | # win32api.Sleep(100) #ms 20 | # win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0) 21 | # win32api.Sleep(100) #ms 22 | # win32api.SetCursorPos(pos) 23 | 24 | # def drag(self): 25 | # pass 26 | 27 | # def _input_keyboard(self, text): 28 | # pass 29 | 30 | 31 | def _test(): 32 | try: 33 | name = u"Windows 任务管理器" 34 | win = FrozenWindow(name.encode("gbk"), exclude_border=True) 35 | win.set_foreground() 36 | time.sleep(0.1) 37 | win._screenshot('taskman-pil.png') 38 | time.sleep(0.5) 39 | win._screenshot_cv2('taskman-cv2.png') 40 | except Exception as e: 41 | print str(e) 42 | 43 | try: 44 | filepath = "C:\\Windows\\System32\\calc.exe" 45 | win = Window(exe_file=filepath) 46 | win.set_foreground() 47 | time.sleep(0.1) 48 | win._screenshot('calc-pil.png') 49 | time.sleep(0.5) 50 | win._screenshot_cv2('calc-cv2.png') 51 | except Exception as e: 52 | print str(e) 53 | 54 | dev = WindowsDevice() 55 | dev.screenshot('screen.png') 56 | 57 | if __name__ == '__main__': 58 | test() 59 | -------------------------------------------------------------------------------- /tests/testcase_examples/blockly.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | import atx 4 | 5 | 6 | __basename = os.path.basename(os.path.splitext(__file__)[0]) 7 | d = atx.connect(platform="android") 8 | d.image_path = [".", "images", os.path.join("images", __basename)] 9 | 10 | d.screenshot('screen.png') 11 | if 0 == 0: 12 | print('你好') 13 | for count in range(1): 14 | print('Hello world') 15 | print('Hello world') -------------------------------------------------------------------------------- /tests/testcase_examples/blockly.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | 5 | 6 | 7 | 8 | screen.png 9 | 10 | 11 | 12 | 13 | 14 | 15 | EQ 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | 0 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 你好 33 | 34 | 35 | 36 | 37 | 38 | 39 | 1 40 | 41 | 42 | 43 | 44 | 45 | 46 | Hello world 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Hello world 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | --------------------------------------------------------------------------------