├── .gitignore ├── DEVELOPMENT.rst ├── MANIFEST.in ├── README.rst ├── bootstrap.py ├── buildout.cfg ├── setup.py ├── src └── AndroidLibrary │ ├── __init__.py │ ├── killableprocess.py │ ├── version.py │ └── winprocess.py └── tests └── apidemos ├── __init__.txt ├── apidemos.txt └── variables.txt /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | gh-pages/ 3 | .installed.cfg 4 | MANIFEST 5 | dist/ 6 | parts/ 7 | src/robotframework_androidlibrary.egg-info/ 8 | develop-eggs/robotframework-androidlibrary.egg-link 9 | android-screenshot-*.png 10 | log.html 11 | output.xml 12 | report.html 13 | ApiDemos.apk 14 | test_servers/ 15 | *.pyc 16 | .irb-history 17 | .irbrc 18 | irb_ios5.sh 19 | screenshot_irb.png -------------------------------------------------------------------------------- /DEVELOPMENT.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | Hacking on robotframework-androidlibrary 3 | ======================================== 4 | 5 | This is a guide on how to work on robotframework-androidlibrary itself, that is - 6 | if you want to use the library to test your own application, please consult 7 | README.rst 8 | 9 | 10 | Prerequisites 11 | ============= 12 | 13 | - Install the `Android SDK `_ 14 | - Install calabash-android v0.2.20:: 15 | 16 | gem install --version '= 0.2.20' calabash-android 17 | 18 | - Create a debug keystore:: 19 | 20 | $ANDROID_SDK/tools/android create project -n dummy_project_to_create_debug_keystore -t 8 -p dummy_project_to_create_debug_keystore -k what.ever -a whatever 21 | cd dummy_project_to_create_debug_keystore 22 | ant debug 23 | cd - 24 | 25 | Development environment 26 | ======================= 27 | 28 | To get started, use the following commands:: 29 | 30 | git clone https://github.com/lovelysystems/robotframework-androidlibrary 31 | cd robotframework-androidlibrary/ 32 | python bootstrap.py --distribute 33 | bin/buildout 34 | 35 | Running tests 36 | ============= 37 | 38 | The library itself is tested using robotframework, to run the tests type:: 39 | 40 | export ANDROID_HOME=path/to/android/sdk 41 | bin/robotframework tests/ 42 | 43 | Optionally, the following parameters can be specified: 44 | 45 | **Highest debug level**:: 46 | 47 | -L TRACE 48 | 49 | **Show the android emulator when running tests**:: 50 | 51 | -v HEADLESS:False 52 | 53 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include src/AndroidLibrary/*.jar 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | robotframework-androidlibrary 2 | ----------------------------- 3 | 4 | **robotframework-androidlibrary** is a `Robot Framework 5 | `_ test library for all your Android 6 | automation needs. 7 | 8 | It uses `Calabash Android `_ to 9 | communicate with your instrumented Android application similar to how `Selenium 10 | WebDriver `_ talks to your web 11 | browser. 12 | 13 | Deprecation Warning 14 | +++++++++++++++++++ 15 | 16 | Lovely Systems does not not support this package anymore and 17 | do not have any follow up package in the same area. If anyone is 18 | interested to continue our efforts and would like to 19 | manage the contributors in this open source project, 20 | feel free to fork the package and give me a hint, so I can 21 | create a link to your fork! 22 | 23 | best regards, Manfred (Github: schwendinger, schwendinger at lovelysystems.com) 24 | 25 | Installation 26 | ++++++++++++ 27 | 28 | To install, just fetch the latest version from PyPI:: 29 | 30 | pip install --upgrade robotframework-androidlibrary 31 | 32 | 33 | Usage 34 | +++++ 35 | 36 | To use the library, import it at the beginning of a Robot Framework Test: 37 | 38 | ============ ================ 39 | Setting Value 40 | ============ ================ 41 | Library AndroidLibrary 42 | ============ ================ 43 | 44 | Documentation 45 | +++++++++++++ 46 | 47 | The keyword documentation can be found at 48 | 49 | Prepare your App 50 | ++++++++++++++++ 51 | 52 | robotframework-androidlibrary uses calabash-android underneath. To install calabash-android (we've only tested this with v0.3.2 yet), use the following command:: 53 | 54 | gem install --version '= 0.4.18' calabash-android 55 | 56 | To prepare your android app look at 57 | 58 | 59 | License 60 | +++++++ 61 | 62 | robotframework is a port of the ruby-based `calabash-android` and therefore 63 | licensed under the `Eclipse Public License (EPL) v1.0 64 | `_ 65 | 66 | Development by `Lovely Systems GmbH `_, 67 | sponsored by `Axel Springer AG `_. 68 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | # Open Source Software licensed under the ZPL license 2 | # Copyright (c) 2006, Zope Foundation and Contributors. 3 | 4 | ############################################################################## 5 | # 6 | # Copyright (c) 2006 Zope Foundation and Contributors. 7 | # All Rights Reserved. 8 | # 9 | # This software is subject to the provisions of the Zope Public License, 10 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 11 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 12 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 13 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 14 | # FOR A PARTICULAR PURPOSE. 15 | # 16 | ############################################################################## 17 | """Bootstrap a buildout-based project 18 | 19 | Simply run this script in a directory containing a buildout.cfg. 20 | The script accepts buildout command-line options, so you can 21 | use the -c option to specify an alternate configuration file. 22 | """ 23 | 24 | import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess 25 | from optparse import OptionParser 26 | 27 | if sys.platform == 'win32': 28 | def quote(c): 29 | if ' ' in c: 30 | return '"%s"' % c # work around spawn lamosity on windows 31 | else: 32 | return c 33 | else: 34 | quote = str 35 | 36 | # See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. 37 | stdout, stderr = subprocess.Popen( 38 | [sys.executable, '-Sc', 39 | 'try:\n' 40 | ' import ConfigParser\n' 41 | 'except ImportError:\n' 42 | ' print 1\n' 43 | 'else:\n' 44 | ' print 0\n'], 45 | stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 46 | has_broken_dash_S = bool(int(stdout.strip())) 47 | 48 | # In order to be more robust in the face of system Pythons, we want to 49 | # run without site-packages loaded. This is somewhat tricky, in 50 | # particular because Python 2.6's distutils imports site, so starting 51 | # with the -S flag is not sufficient. However, we'll start with that: 52 | if not has_broken_dash_S and 'site' in sys.modules: 53 | # We will restart with python -S. 54 | args = sys.argv[:] 55 | args[0:0] = [sys.executable, '-S'] 56 | args = map(quote, args) 57 | os.execv(sys.executable, args) 58 | # Now we are running with -S. We'll get the clean sys.path, import site 59 | # because distutils will do it later, and then reset the path and clean 60 | # out any namespace packages from site-packages that might have been 61 | # loaded by .pth files. 62 | clean_path = sys.path[:] 63 | import site 64 | sys.path[:] = clean_path 65 | for k, v in sys.modules.items(): 66 | if k in ('setuptools', 'pkg_resources') or ( 67 | hasattr(v, '__path__') and 68 | len(v.__path__)==1 and 69 | not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))): 70 | # This is a namespace package. Remove it. 71 | sys.modules.pop(k) 72 | 73 | is_jython = sys.platform.startswith('java') 74 | 75 | setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' 76 | distribute_source = 'http://python-distribute.org/distribute_setup.py' 77 | 78 | # parsing arguments 79 | def normalize_to_url(option, opt_str, value, parser): 80 | if value: 81 | if '://' not in value: # It doesn't smell like a URL. 82 | value = 'file://%s' % ( 83 | urllib.pathname2url( 84 | os.path.abspath(os.path.expanduser(value))),) 85 | if opt_str == '--download-base' and not value.endswith('/'): 86 | # Download base needs a trailing slash to make the world happy. 87 | value += '/' 88 | else: 89 | value = None 90 | name = opt_str[2:].replace('-', '_') 91 | setattr(parser.values, name, value) 92 | 93 | usage = '''\ 94 | [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] 95 | 96 | Bootstraps a buildout-based project. 97 | 98 | Simply run this script in a directory containing a buildout.cfg, using the 99 | Python that you want bin/buildout to use. 100 | 101 | Note that by using --setup-source and --download-base to point to 102 | local resources, you can keep this script from going over the network. 103 | ''' 104 | 105 | parser = OptionParser(usage=usage) 106 | parser.add_option("-v", "--version", dest="version", 107 | help="use a specific zc.buildout version") 108 | parser.add_option("-d", "--distribute", 109 | action="store_true", dest="use_distribute", default=False, 110 | help="Use Distribute rather than Setuptools.") 111 | parser.add_option("--setup-source", action="callback", dest="setup_source", 112 | callback=normalize_to_url, nargs=1, type="string", 113 | help=("Specify a URL or file location for the setup file. " 114 | "If you use Setuptools, this will default to " + 115 | setuptools_source + "; if you use Distribute, this " 116 | "will default to " + distribute_source +".")) 117 | parser.add_option("--download-base", action="callback", dest="download_base", 118 | callback=normalize_to_url, nargs=1, type="string", 119 | help=("Specify a URL or directory for downloading " 120 | "zc.buildout and either Setuptools or Distribute. " 121 | "Defaults to PyPI.")) 122 | parser.add_option("--eggs", 123 | help=("Specify a directory for storing eggs. Defaults to " 124 | "a temporary directory that is deleted when the " 125 | "bootstrap script completes.")) 126 | parser.add_option("-t", "--accept-buildout-test-releases", 127 | dest='accept_buildout_test_releases', 128 | action="store_true", default=False, 129 | help=("Normally, if you do not specify a --version, the " 130 | "bootstrap script and buildout gets the newest " 131 | "*final* versions of zc.buildout and its recipes and " 132 | "extensions for you. If you use this flag, " 133 | "bootstrap and buildout will get the newest releases " 134 | "even if they are alphas or betas.")) 135 | parser.add_option("-c", None, action="store", dest="config_file", 136 | help=("Specify the path to the buildout configuration " 137 | "file to be used.")) 138 | 139 | options, args = parser.parse_args() 140 | 141 | # if -c was provided, we push it back into args for buildout's main function 142 | if options.config_file is not None: 143 | args += ['-c', options.config_file] 144 | 145 | if options.eggs: 146 | eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) 147 | else: 148 | eggs_dir = tempfile.mkdtemp() 149 | 150 | if options.setup_source is None: 151 | if options.use_distribute: 152 | options.setup_source = distribute_source 153 | else: 154 | options.setup_source = setuptools_source 155 | 156 | if options.accept_buildout_test_releases: 157 | args.append('buildout:accept-buildout-test-releases=true') 158 | args.append('bootstrap') 159 | 160 | try: 161 | import pkg_resources 162 | import setuptools # A flag. Sometimes pkg_resources is installed alone. 163 | if not hasattr(pkg_resources, '_distribute'): 164 | raise ImportError 165 | except ImportError: 166 | ez_code = urllib2.urlopen( 167 | options.setup_source).read().replace('\r\n', '\n') 168 | ez = {} 169 | exec ez_code in ez 170 | setup_args = dict(to_dir=eggs_dir, download_delay=0) 171 | if options.download_base: 172 | setup_args['download_base'] = options.download_base 173 | if options.use_distribute: 174 | setup_args['no_fake'] = True 175 | ez['use_setuptools'](**setup_args) 176 | if 'pkg_resources' in sys.modules: 177 | reload(sys.modules['pkg_resources']) 178 | import pkg_resources 179 | # This does not (always?) update the default working set. We will 180 | # do it. 181 | for path in sys.path: 182 | if path not in pkg_resources.working_set.entries: 183 | pkg_resources.working_set.add_entry(path) 184 | 185 | cmd = [quote(sys.executable), 186 | '-c', 187 | quote('from setuptools.command.easy_install import main; main()'), 188 | '-mqNxd', 189 | quote(eggs_dir)] 190 | 191 | if not has_broken_dash_S: 192 | cmd.insert(1, '-S') 193 | 194 | find_links = options.download_base 195 | if not find_links: 196 | find_links = os.environ.get('bootstrap-testing-find-links') 197 | if find_links: 198 | cmd.extend(['-f', quote(find_links)]) 199 | 200 | if options.use_distribute: 201 | setup_requirement = 'distribute' 202 | else: 203 | setup_requirement = 'setuptools' 204 | ws = pkg_resources.working_set 205 | setup_requirement_path = ws.find( 206 | pkg_resources.Requirement.parse(setup_requirement)).location 207 | env = dict( 208 | os.environ, 209 | PYTHONPATH=setup_requirement_path) 210 | 211 | requirement = 'zc.buildout' 212 | version = options.version 213 | if version is None and not options.accept_buildout_test_releases: 214 | # Figure out the most recent final version of zc.buildout. 215 | import setuptools.package_index 216 | _final_parts = '*final-', '*final' 217 | def _final_version(parsed_version): 218 | for part in parsed_version: 219 | if (part[:1] == '*') and (part not in _final_parts): 220 | return False 221 | return True 222 | index = setuptools.package_index.PackageIndex( 223 | search_path=[setup_requirement_path]) 224 | if find_links: 225 | index.add_find_links((find_links,)) 226 | req = pkg_resources.Requirement.parse(requirement) 227 | if index.obtain(req) is not None: 228 | best = [] 229 | bestv = None 230 | for dist in index[req.project_name]: 231 | distv = dist.parsed_version 232 | if _final_version(distv): 233 | if bestv is None or distv > bestv: 234 | best = [dist] 235 | bestv = distv 236 | elif distv == bestv: 237 | best.append(dist) 238 | if best: 239 | best.sort() 240 | version = best[-1].version 241 | if version: 242 | requirement = '=='.join((requirement, version)) 243 | cmd.append(requirement) 244 | 245 | if is_jython: 246 | import subprocess 247 | exitcode = subprocess.Popen(cmd, env=env).wait() 248 | else: # Windows prefers this, apparently; otherwise we would prefer subprocess 249 | exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) 250 | if exitcode != 0: 251 | sys.stdout.flush() 252 | sys.stderr.flush() 253 | print ("An error occurred when trying to install zc.buildout. " 254 | "Look above this message for any errors that " 255 | "were output by easy_install.") 256 | sys.exit(exitcode) 257 | 258 | ws.add_entry(eggs_dir) 259 | ws.require(requirement) 260 | import zc.buildout.buildout 261 | zc.buildout.buildout.main(args) 262 | if not options.eggs: # clean up temporary egg directory 263 | shutil.rmtree(eggs_dir) 264 | -------------------------------------------------------------------------------- /buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | newest = false 3 | extensions = buildout-versions 4 | 5 | develop = . 6 | 7 | parts = develop-eggs robotframework 8 | versions = versions 9 | 10 | [develop-eggs] 11 | recipe = zc.recipe.egg 12 | eggs = robotframework-androidlibrary 13 | 14 | [versions] 15 | robotframework = 2.7.4 16 | 17 | [robotframework] 18 | recipe = zc.recipe.egg 19 | eggs = 20 | robotframework 21 | robotframework-androidlibrary 22 | 23 | entry-points = 24 | robotframework=robot:run_cli 25 | libdoc=robot.libdoc:libdoc_cli 26 | testdoc=robot.testdoc:testdoc_cli 27 | tidy=robot.tidy:tidy_cli 28 | rebot=robot.rebot:rebot_cli 29 | 30 | initialization = 31 | # i have no idea why this hack is neccessary 32 | from robot.rebot import rebot_cli 33 | import robot.rebot 34 | robot.rebot.rebot_cli = rebot_cli 35 | 36 | arguments = sys.argv[1:] 37 | 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from os.path import join, dirname 4 | 5 | execfile(join(dirname(__file__), 'src', 'AndroidLibrary', 'version.py')) 6 | 7 | from distutils.core import setup 8 | 9 | CLASSIFIERS = """ 10 | Programming Language :: Python 11 | Topic :: Software Development :: Testing 12 | """[1:-1] 13 | 14 | long_description=open(join(dirname(__file__), 'README.rst',)).read() 15 | 16 | setup( 17 | name = 'robotframework-androidlibrary', 18 | version = VERSION, 19 | description = 'Robot Framework Automation Library for Android', 20 | long_description = long_description, 21 | author = "Lovely Systems GmbH", 22 | author_email = "office@lovelysystems.com", 23 | url = 'https://github.com/lovelysystems/robotframework-androidlibrary', 24 | license = 'EPL', 25 | keywords = 'robotframework testing testautomation android calabash robotium', 26 | platforms = 'any', 27 | zip_safe = False, 28 | classifiers = CLASSIFIERS.splitlines(), 29 | package_dir = {'' : 'src'}, 30 | install_requires = ['robotframework', 'requests'], 31 | packages = ['AndroidLibrary'], 32 | package_data = {'AndroidLibrary': ['src/AndroidLibrary/*.jar']} 33 | ) 34 | -------------------------------------------------------------------------------- /src/AndroidLibrary/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import subprocess 5 | import requests 6 | from urlparse import urlparse, urljoin 7 | from xml.dom import minidom 8 | from version import VERSION 9 | 10 | __version__ = VERSION 11 | 12 | import robot 13 | from robot.variables import GLOBAL_VARIABLES 14 | from robot.api import logger 15 | 16 | import killableprocess 17 | import tempfile 18 | 19 | 20 | class AndroidLibrary(object): 21 | 22 | ROBOT_LIBRARY_VERSION = VERSION 23 | ROBOT_LIBRARY_SCOPE = 'GLOBAL' 24 | 25 | def __init__(self, ANDROID_HOME=None): 26 | ''' 27 | Path to the Android SDK. 28 | Optional if the $ANDROID_HOME environment variable is set. 29 | ''' 30 | 31 | if ANDROID_HOME is None: 32 | ANDROID_HOME = os.environ['ANDROID_HOME'] 33 | 34 | self._ANDROID_HOME = ANDROID_HOME 35 | self._screenshot_index = 0 36 | 37 | self._adb = self._sdk_path(['platform-tools/adb', 38 | 'platform-tools/adb.exe']) 39 | self._emulator = self._sdk_path(['tools/emulator', 40 | 'tools/emulator.exe']) 41 | self._url = None 42 | self._testserver_proc = None 43 | self._username = None 44 | self._password = None 45 | self._calabash_bin_path = self._env_command(['calabash-android.bat', 46 | 'calabash-android']) 47 | 48 | def _sdk_path(self, paths): 49 | for path in paths: 50 | complete_path = os.path.abspath(os.path.join( 51 | self._ANDROID_HOME, path)) 52 | if os.path.exists(complete_path): 53 | return complete_path 54 | 55 | raise AssertionError("Couldn't find %s binary in %s" % ( 56 | os.path.splitext(os.path.split(complete_path)[1])[0], 57 | os.path.split(complete_path)[0],)) 58 | 59 | def _env_command(self, commands): 60 | for path in os.environ["PATH"].split(os.pathsep): 61 | for command in commands: 62 | exe_file = os.path.join(path, command) 63 | if os.path.isfile(exe_file): 64 | return exe_file 65 | raise AssertionError("Couldn't find binary %s" % os.path.commonprefix(commands)) 66 | 67 | def _request(self, method, url, *args, **kwargs): 68 | 69 | if self._username is not None and self._password is not None: 70 | kwargs['auth'] = (self._username, self._password) 71 | 72 | logging.debug(">> %r %r", args, kwargs) 73 | response = getattr(requests, method)(url, *args, **kwargs) 74 | 75 | return response 76 | 77 | def set_basic_auth(self, username, password): 78 | ''' 79 | Set basic authentication to use with all further API calls 80 | 81 | username is the username to authenticate with, e.g. 'Aladdin' 82 | 83 | password is the password to use, e.g. 'open sesame' 84 | ''' 85 | self._username = username 86 | self._password = password 87 | 88 | def start_emulator(self, avd_name, no_window=False, 89 | language="en", country="us", save_snapshot=False, retries=3, http_proxy=""): 90 | ''' 91 | Starts the Android Emulator. 92 | 93 | `avd_name` Identifier of the Android Virtual Device, for valid values on your machine run "$ANDROID_HOME/tools/android list avd|grep Name` 94 | `no_window` Set to True to start the emulator without GUI, useful for headless environments. 95 | ''' 96 | lang = "persist.sys.language=%s" % language 97 | co = "persist.sys.country=%s" % country 98 | args = [self._emulator, '-avd', avd_name, '-prop', lang, '-prop', co] 99 | 100 | if no_window: 101 | args.append('-no-window') 102 | 103 | if not save_snapshot: 104 | args.append('-no-snapshot-save') 105 | 106 | if len(http_proxy)>0: 107 | args.append('-http-proxy') 108 | args.append(http_proxy) 109 | 110 | logging.debug("$> %s", ' '.join(args)) 111 | 112 | self._emulator_proc = subprocess.Popen(args) 113 | rc, output, errput = self._execute_with_timeout([self._adb, 'wait-for-device'], max_timeout=80, max_attempts=1) 114 | if rc != 0 and retries > 0: 115 | self.stop_emulator() 116 | logging.warn("adb did not respond, retry starting %s " % retries) 117 | self.start_emulator(avd_name, no_window, language, country, save_snapshot, retries - 1) 118 | 119 | def stop_emulator(self): 120 | ''' 121 | Halts a previously started Android Emulator. 122 | ''' 123 | 124 | if not hasattr(self, '_emulator_proc'): 125 | logging.warn("Could not stop Android Emulator: It was not started.") 126 | return 127 | 128 | self._emulator_proc.terminate() 129 | self._emulator_proc.kill() 130 | self._emulator_proc.wait() 131 | self._emulator_proc = None 132 | 133 | def _execute_with_timeout(self, cmd, max_attempts=3, max_timeout=120): 134 | logging.debug("$> %s # with timeout %ds", ' '.join(cmd), max_timeout) 135 | 136 | attempt = 0 137 | 138 | while attempt < max_attempts: 139 | attempt = attempt + 1 140 | out = tempfile.NamedTemporaryFile(delete=False) 141 | err = tempfile.NamedTemporaryFile(delete=False) 142 | p = killableprocess.Popen(cmd, stdout=out, stderr=err) 143 | p.wait(max_timeout) 144 | out.flush() 145 | out.close() 146 | err.flush() 147 | err.close() 148 | 149 | # -9 and 127 are returned by killableprocess when a timeout happens 150 | if p.returncode == -9 or p.returncode == 127: 151 | logging.warn("Executing %s failed executing in less then %d seconds and was killed, attempt number %d of %d" % ( 152 | ' '.join(cmd), max_timeout, attempt, max_attempts)) 153 | continue 154 | 155 | try: 156 | outfile = open(out.name, 'r') 157 | errfile = open(err.name, 'r') 158 | return p.returncode, outfile.read(), errfile.read() 159 | finally: 160 | outfile.close() 161 | os.unlink(out.name) 162 | errfile.close() 163 | os.unlink(errfile.name) 164 | 165 | def _wait_for_package_manager(self): 166 | attempts = 0 167 | max_attempts = 3 168 | 169 | while attempts < max_attempts: 170 | rc, output, errput = self._execute_with_timeout([ 171 | self._adb, "wait-for-device", "shell", "pm", "path", "android"], 172 | max_timeout=60, max_attempts=3) 173 | assert rc == 0, "Waiting for package manager failed: %d, %r, %r" % (rc, output, errput) 174 | 175 | if not 'Could not access the Package Manager.' in output: 176 | return 177 | 178 | raise AssertionError(output) 179 | 180 | def uninstall_application(self, package_name): 181 | self._wait_for_package_manager() 182 | 183 | rc, output, errput = self._execute_with_timeout([self._adb, "uninstall", package_name]) 184 | assert rc == 0, "Uninstalling application failed: %d, %r" % (rc, output) 185 | assert output is not None 186 | logging.debug(output) 187 | assert 'Error' not in output, output 188 | 189 | def install_application(self, apk_file): 190 | ''' 191 | Installs the given Android application package file (APK) on the emulator along with the test server. 192 | 193 | For instrumentation (and thus all remote keywords to work) both .apk 194 | files must be signed with the same key. 195 | 196 | `apk_file` Path the the application to install 197 | ''' 198 | 199 | self._wait_for_package_manager() 200 | 201 | rc, output, errput = self._execute_with_timeout([self._adb, "install", "-r", apk_file], max_timeout=240) 202 | logging.debug(output) 203 | assert rc == 0, "Installing application failed: %d, %r" % (rc, output) 204 | assert output is not None 205 | assert 'Error' not in output, output 206 | 207 | def wait_for_device(self, timeout=120): 208 | ''' 209 | Wait for the device to become available 210 | ''' 211 | rc, output, errput = self._execute_with_timeout([self._adb, 'wait-for-device'], max_timeout=timeout / 3, max_attempts=3) 212 | assert rc == 0, "wait for device application failed: %d, %r" % (rc, output) 213 | 214 | def send_key(self, key_code): 215 | ''' 216 | Send key event with the given key code. See http://developer.android.com/reference/android/view/KeyEvent.html for a list of available key codes. 217 | 218 | `key_code` The key code to send 219 | ''' 220 | rc, output, errput = self._execute_with_timeout([self._adb, 'shell', 'input', 'keyevent', '%d' % key_code], max_attempts=1) 221 | assert rc == 0 222 | 223 | def press_back_button(self): 224 | ''' 225 | Presses the back button. 226 | ''' 227 | response = self._perform_action("go_back") 228 | assert response["success"] is True, "Could not press back button:: %s" % (response["message"]) 229 | 230 | def press_menu_button(self): 231 | ''' 232 | Press the menu button ("KEYCODE_MENU"), same as '| Send Key | 82 |' 233 | ''' 234 | self.send_key(82) 235 | 236 | def set_device_endpoint(self, host='localhost', port=34777): 237 | """*DEPRECATED* Use 'Set Device Url' instead. 238 | 239 | Set the device endpoint where the application is started. 240 | If not set the endpoint defaults to 'localhost:34777'. 241 | 242 | `host` the endpoint's host 243 | `port` the endpoint's port 244 | """ 245 | self.set_device_url('http://%s:%d' % (host, int(port))) 246 | 247 | def set_device_url(self, url='http://localhost:34777/'): 248 | """ 249 | Set the device url where the application is started. 250 | 251 | `url` the base url to use for all requests 252 | """ 253 | 254 | parsed_url = urlparse(url) 255 | 256 | self._port = parsed_url.port 257 | self._hostname = parsed_url.hostname 258 | 259 | self._url = url 260 | 261 | def start_testserver(self, package_name): 262 | ''' 263 | *DEPRECATED* Use 'Start TestServer with apk' instead. 264 | Does not work with calabash-android >= 0.3.0 265 | 266 | Start the remote test server inside the Android Application. 267 | 268 | `package_name` fully qualified name of the application to test 269 | 270 | ''' 271 | if not self._url: 272 | self.set_device_url() 273 | 274 | assert self._hostname == 'localhost', ( 275 | "Device Url was set to %s, but should be set to localhost with the " 276 | "'Set Device Url' keyword to use a local testserver" 277 | ) 278 | 279 | rc, output, errput = self._execute_with_timeout([ 280 | self._adb, 281 | "wait-for-device", 282 | "forward", 283 | "tcp:%d" % self._port, 284 | "tcp:7102" 285 | ]) 286 | 287 | args = [ 288 | self._adb, 289 | "wait-for-device", 290 | "shell", 291 | "am", 292 | "instrument", 293 | "-e", 294 | "class", 295 | "sh.calaba.instrumentationbackend.InstrumentationBackend", 296 | "%s.test/sh.calaba.instrumentationbackend.CalabashInstrumentationTestRunner" % package_name, 297 | ] 298 | 299 | logging.debug("$> %s", ' '.join(args)) 300 | self._testserver_proc = subprocess.Popen(args) 301 | 302 | def start_testserver_with_apk(self, apk): 303 | ''' 304 | Works only with calabash-android >= 0.3.0 305 | Start the remote test server 306 | 307 | `apk` path to the apk to controll 308 | ''' 309 | if not self._url: 310 | self.set_device_url() 311 | 312 | assert self._hostname == 'localhost', ( 313 | "Device Url was set to %s, but should be set to localhost with the " 314 | "'Set Device Url' keyword to use a local testserver" 315 | ) 316 | 317 | rc, output, errput = self._execute_with_timeout([ 318 | self._adb, 319 | "wait-for-device", 320 | "forward", 321 | "tcp:%d" % self._port, 322 | "tcp:7102" 323 | ]) 324 | package_name, main_activity = self._main_activity_from_apk(apk) 325 | if '.' not in main_activity or main_activity[0] == '.': 326 | main_activity = "%s.%s" % (package_name, main_activity.lstrip('.')) 327 | args = [ 328 | self._adb, 329 | "shell", 330 | "am", 331 | "instrument", 332 | "-w", 333 | "-e", 334 | "target_package", 335 | package_name, 336 | "-e", 337 | "main_activity", 338 | main_activity, 339 | "-e", 340 | "test_server_port", 341 | str(7102), 342 | "-e", 343 | "class", 344 | "sh.calaba.instrumentationbackend.InstrumentationBackend", 345 | "%s.test/sh.calaba.instrumentationbackend.CalabashInstrumentationTestRunner" % package_name, 346 | ] 347 | self._testserver_proc = subprocess.Popen(args) 348 | 349 | def _main_activity_from_apk(self, apk): 350 | ''' 351 | Returns the package_name and the Main-Action 352 | from a given apk 353 | ''' 354 | rc, output, errput = self._execute_with_timeout([self._calabash_bin_path, "extract-manifest", apk]) 355 | xmldoc = minidom.parseString(output) 356 | manifest = xmldoc.getElementsByTagName("manifest") 357 | assert len(manifest) > 0, "No tag found in manifest file" 358 | manifest = manifest[0] 359 | package = manifest.getAttribute("package") 360 | assert package is not None, "Could not find package name in apk: %s manifest: %s" % (apk, output) 361 | for node in xmldoc.getElementsByTagName("action"): 362 | if node.getAttribute("android:name") == "android.intent.action.MAIN": 363 | return package, node.parentNode.parentNode.getAttribute("android:name") 364 | return package, None 365 | 366 | def stop_testserver(self): 367 | ''' 368 | Halts a previously started Android Emulator. 369 | ''' 370 | 371 | assert self._testserver_proc is not None, 'Tried to stop a previously started test server, but it was not started.' 372 | 373 | response = self._request("get", urljoin(self._url, 'kill')) 374 | 375 | assert response.status_code == 200, "InstrumentationBackend sent status %d, expected 200" % response.status_code 376 | assert response.text == 'Affirmative!', "InstrumentationBackend replied '%s', expected 'Affirmative'" % response.text 377 | 378 | def connect_to_testserver(self): 379 | ''' 380 | Connect to the previously started test server inside the Android 381 | Application. Performs a handshake. 382 | ''' 383 | 384 | response = self._request("get", urljoin(self._url, 'ping')) 385 | 386 | assert response.status_code == 200, "InstrumentationBackend sent status %d, expected 200" % response.status_code 387 | assert response.text == 'pong', "InstrumentationBackend replied '%s', expected 'pong'" % response.text 388 | 389 | def _perform_action(self, command, *arguments): 390 | action = json.dumps({ 391 | "command": command, 392 | "arguments": arguments, 393 | }) 394 | 395 | logging.debug(">> %r", action) 396 | url = self._url 397 | response = self._request("post", url, data=action, 398 | headers={ 399 | 'Content-Type': 'application/json' 400 | },) 401 | 402 | logging.error("<< %r", url) 403 | logging.error("<< %r", response.text) 404 | assert response.status_code == 200, "InstrumentationBackend sent status %d, expected 200" % response.status_code 405 | try: 406 | response_decoded = json.loads(response.text) 407 | return response_decoded 408 | except ValueError: 409 | return response.text 410 | return "error" 411 | 412 | # BEGIN: STOLEN FROM SELENIUM2LIBRARY 413 | 414 | def _get_log_dir(self): 415 | logfile = GLOBAL_VARIABLES['${LOG FILE}'] 416 | if logfile != 'NONE': 417 | return os.path.dirname(logfile) 418 | return GLOBAL_VARIABLES['${OUTPUTDIR}'] 419 | 420 | def _get_screenshot_paths(self, filename): 421 | if not filename: 422 | self._screenshot_index += 1 423 | filename = 'android-screenshot-%d.png' % self._screenshot_index 424 | else: 425 | filename = filename.replace('/', os.sep) 426 | logdir = self._get_log_dir() 427 | path = os.path.join(logdir, filename) 428 | link = robot.utils.get_link_path(path, logdir) 429 | return path, link 430 | 431 | # END: STOLEN FROM SELENIUM2LIBRARY 432 | 433 | def capture_screenshot(self, filename=None, relative_url='screenshot'): 434 | ''' 435 | Captures a screenshot of the current screen and embeds it in the test report 436 | 437 | Also works in headless environments. 438 | 439 | This keyword might fail if the instrumentation backend is unable to 440 | make a screenshot at the given point in time. 441 | 442 | If you want to try again making a screenshot, you want to wrap this like 443 | 444 | | Wait Until Keyword Succeeds | 20 seconds | 5 seconds | Capture Screenshot | 445 | 446 | If you don't care about the screenshot (as it is a nice-to-have 447 | addition to your test report but not neccessary), use it like this: 448 | 449 | | Run Keyword And Ignore Error | Capture Screenshot | 450 | 451 | `filename` Location where the screenshot will be saved (optional). 452 | `relative_url` URL part, relative to the device endpoint. For the standard setup the default value is sufficient. 453 | ''' 454 | 455 | path, link = self._get_screenshot_paths(filename) 456 | response = self._request("get", urljoin(self._url, relative_url)) 457 | 458 | if response.status_code == 500: 459 | raise AssertionError("Unable to make a screenshot, see documentation on how to handle this") 460 | 461 | assert response.status_code == 200, "InstrumentationBackend sent status %d, expected 200" % response.status_code 462 | 463 | with open(path, 'w') as f: 464 | f.write(response.content) 465 | f.close() 466 | 467 | logger.info('' 468 | '' % (link, link), True, False) 469 | 470 | def screen_should_contain(self, text): 471 | ''' 472 | Asserts that the current screen contains a given text 473 | 474 | `text` String that should be on the current screen 475 | ''' 476 | response = self._perform_action("assert_text", text, True) 477 | assert response["success"] is True, "Screen does not contain text '%s': %s" % (text, response["message"]) 478 | 479 | def screen_should_not_contain(self, text): 480 | ''' 481 | Asserts that the current screen does not contain a given text 482 | 483 | `text` String that should not be on the current screen 484 | ''' 485 | response = self._perform_action("assert_text", text, False) 486 | assert response["success"] is True, "Screen does contain text '%s', but shouldn't have: %s" % (text, response["message"]) 487 | 488 | def touch_button(self, text): 489 | ''' 490 | Touch an android.widget.Button 491 | 492 | `text` is the text the button that will be clicked contains 493 | ''' 494 | response = self._perform_action("press_button_with_text", text) 495 | assert response["success"] is True, "Touching button '%s' failed: %s" % (text, response["message"]) 496 | 497 | def touch_text(self, text): 498 | ''' 499 | Touch a text that is present on the screen 500 | 501 | `text` is the text the button that will be clicked contains 502 | ''' 503 | response = self._perform_action("click_on_text", text) 504 | assert response["success"] is True, "Touching text '%s' failed: %s" % (text, response["message"]) 505 | 506 | def scroll_up(self): 507 | ''' 508 | Scroll up 509 | ''' 510 | response = self._perform_action("scroll_up") 511 | assert response["success"] is True, "Scrolling up failed: %s" % (response["message"]) 512 | 513 | def touch_position(self, percent_left, percent_top): 514 | ''' 515 | Touch a position on the screen 516 | 517 | `percent_left` is the position from the left in percent of the total screen width 518 | `percent_top` is the position from the top in percent of the total screen height 519 | ''' 520 | percent_left = int(percent_left) 521 | percent_top = int(percent_top) 522 | response = self._perform_action("click_on_screen", percent_left, percent_top) 523 | assert response["success"] is True, "Touching position %s, %s failed: %s" % (percent_left, percent_top, response["message"]) 524 | 525 | def scroll_down(self): 526 | ''' 527 | Scroll down 528 | ''' 529 | response = self._perform_action("scroll_down") 530 | assert response["success"] is True, "Scrolling down failed: %s" % (response["message"]) 531 | 532 | def _split_locator(self, locator, default_strategy="css"): 533 | try: 534 | strategy, query = locator.split("=") 535 | except ValueError: 536 | strategy = default_strategy 537 | query = locator 538 | logging.debug("No explicit locator strategy set, using '%s'" % strategy) 539 | return strategy, query 540 | 541 | def set_webview_text(self, locator, value): 542 | ''' 543 | Set the field in the webview to the given value 544 | 545 | `locator` the locator to find the element to change. Valid locators are in the form of css=#element_id or xpath=//input[0] 546 | `value` the new value 547 | ''' 548 | strategy, query = self._split_locator(locator) 549 | response = self._perform_action("set_text", strategy, query, value) 550 | assert response["success"] is True, "Setting webview text failed: %s" % (response["message"]) 551 | 552 | def touch_webview_element(self, locator): 553 | ''' 554 | Touch an element in a webview 555 | 556 | `locator` locator for element to trigger a click event (only css locators are supported at the moment) 557 | ''' 558 | strategy, query = self._split_locator(locator) 559 | response = self._perform_action("touch", strategy, query) 560 | assert response["success"] is True, "Touching Webview element '%s' failed: %s" % (locator, response["message"]) 561 | 562 | def webview_scroll_to(self, locator): 563 | ''' 564 | Scroll to a specific elment in a webview 565 | `locator` locator for element to scroll to (only css locators are supported at the moment) 566 | ''' 567 | strategy, query = self._split_locator(locator) 568 | response = self._perform_action("scroll_to", strategy, query) 569 | assert response["success"] is True, "Scrolling to Webview element '%s' failed: %s" % (locator, response["message"]) 570 | 571 | def set_text(self, locator, value): 572 | ''' 573 | Set text in a native text field. 574 | 575 | See `Set Webview Text` to set the text in an input element in an embedded webview. 576 | 577 | `locator` which text field to set. Valid locators are '' or 'num=' for a numbered text field, 'name=<'name=' for a named text field 578 | 579 | `value` the new value of the native text field 580 | ''' 581 | strategy, query = self._split_locator(locator, "num") 582 | if strategy in ("num", ): 583 | try: 584 | query = int(query, 10) 585 | except ValueError: 586 | raise AssertionError("Could not convert '%s' to integer, but required for '%s' locator strategy" % ( 587 | query, strategy 588 | )) 589 | 590 | api_names = { 591 | 'num': 'enter_text_into_numbered_field', 592 | 'name': 'enter_text_into_named_field', 593 | } 594 | 595 | assert strategy in api_names.keys(), 'Locator strategy must be one of "%s", but was %s' % ( 596 | '", "'.join(api_names.keys()), strategy 597 | ) 598 | 599 | response = self._perform_action(api_names[strategy], value, query) 600 | assert response["success"] is True, "Setting the text '%s' failed: %s" % (locator, response["message"]) 601 | 602 | def webview_should_contain(self, text): 603 | ''' 604 | assert that the webview contains a given text 605 | 606 | `text` the text the webview should contain 607 | ''' 608 | response = self._perform_action("query", "css", "html") 609 | assert text in response[0]["textContent"], "Webview does not contain '%s': %s" % (text, response["message"]) 610 | 611 | def swipe_left(self): 612 | ''' 613 | Performs a swipe gesture to the left 614 | ''' 615 | response = self._perform_action('swipe', 'left') 616 | assert response["success"] is True, "Swiping left failed: %s" % response["message"] 617 | 618 | def swipe_right(self): 619 | ''' 620 | Performs a swipe gesture to the right 621 | ''' 622 | response = self._perform_action('swipe', 'right') 623 | assert response["success"] is True, "Swiping right failed: %s" % response["message"] 624 | 625 | def touch_view(self, locator): 626 | ''' 627 | Touch a view 628 | 629 | `locator` which view will be touched. Valid locators are '' or'desc=' for an imageButton with a contentDescription set. 630 | ''' 631 | strategy, query = self._split_locator(locator, "desc") 632 | response = self._perform_action('click_on_view_by_description', query) 633 | assert response["success"] is True, "Click on view failed: %s" % response["message"] 634 | 635 | def touch_image_button(self, locator): 636 | ''' 637 | Touch an android.widget.ImageButton 638 | 639 | `locator` which image button will be touched. Valid locators are '' or 'num=' for a numbered ImageButton or 'desc=' for an imageButton with a contentDescription set. 640 | ''' 641 | 642 | strategy, query = self._split_locator(locator, "num") 643 | action = "press_image_button_number" 644 | if strategy == "num": 645 | try: 646 | query = int(query, 10) 647 | except ValueError: 648 | raise AssertionError("Could not convert '%s' to integer, but required for '%s' locator strategy" % ( 649 | query, strategy 650 | )) 651 | elif strategy == "desc": 652 | action = "press_image_button_description" 653 | 654 | response = self._perform_action(action, query) 655 | assert response["success"] is True, "Touching image '%s' failed: %s" % (locator,response["message"]) 656 | -------------------------------------------------------------------------------- /src/AndroidLibrary/killableprocess.py: -------------------------------------------------------------------------------- 1 | # killableprocess - subprocesses which can be reliably killed 2 | # 3 | # Parts of this module are copied from the subprocess.py file contained 4 | # in the Python distribution. 5 | # 6 | # Copyright (c) 2003-2004 by Peter Astrand 7 | # 8 | # Additions and modifications written by Benjamin Smedberg 9 | # are Copyright (c) 2006 by the Mozilla Foundation 10 | # 11 | # 12 | # By obtaining, using, and/or copying this software and/or its 13 | # associated documentation, you agree that you have read, understood, 14 | # and will comply with the following terms and conditions: 15 | # 16 | # Permission to use, copy, modify, and distribute this software and 17 | # its associated documentation for any purpose and without fee is 18 | # hereby granted, provided that the above copyright notice appears in 19 | # all copies, and that both that copyright notice and this permission 20 | # notice appear in supporting documentation, and that the name of the 21 | # author not be used in advertising or publicity pertaining to 22 | # distribution of the software without specific, written prior 23 | # permission. 24 | # 25 | # THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, 26 | # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. 27 | # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR 28 | # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 29 | # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 30 | # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION 31 | # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 32 | 33 | r"""killableprocess - Subprocesses which can be reliably killed 34 | 35 | This module is a subclass of the builtin "subprocess" module. It allows 36 | processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method. 37 | 38 | It also adds a timeout argument to Wait() for a limited period of time before 39 | forcefully killing the process. 40 | 41 | Note: On Windows, this module requires Windows 2000 or higher (no support for 42 | Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with 43 | Python 2.5+ or available from http://python.net/crew/theller/ctypes/ 44 | """ 45 | 46 | import subprocess 47 | import sys 48 | import os 49 | import time 50 | import types 51 | 52 | try: 53 | from subprocess import CalledProcessError 54 | except ImportError: 55 | # Python 2.4 doesn't implement CalledProcessError 56 | class CalledProcessError(Exception): 57 | """This exception is raised when a process run by check_call() returns 58 | a non-zero exit status. The exit status will be stored in the 59 | returncode attribute.""" 60 | def __init__(self, returncode, cmd): 61 | self.returncode = returncode 62 | self.cmd = cmd 63 | def __str__(self): 64 | return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) 65 | 66 | mswindows = (sys.platform == "win32") 67 | 68 | if mswindows: 69 | import winprocess 70 | else: 71 | import signal 72 | 73 | def call(*args, **kwargs): 74 | waitargs = {} 75 | if "timeout" in kwargs: 76 | waitargs["timeout"] = kwargs.pop("timeout") 77 | 78 | return Popen(*args, **kwargs).wait(**waitargs) 79 | 80 | def check_call(*args, **kwargs): 81 | """Call a program with an optional timeout. If the program has a non-zero 82 | exit status, raises a CalledProcessError.""" 83 | 84 | retcode = call(*args, **kwargs) 85 | if retcode: 86 | cmd = kwargs.get("args") 87 | if cmd is None: 88 | cmd = args[0] 89 | raise CalledProcessError(retcode, cmd) 90 | 91 | if not mswindows: 92 | def DoNothing(*args): 93 | pass 94 | 95 | class Popen(subprocess.Popen): 96 | if not mswindows: 97 | # Override __init__ to set a preexec_fn 98 | def __init__(self, *args, **kwargs): 99 | if len(args) >= 7: 100 | raise Exception("Arguments preexec_fn and after must be passed by keyword.") 101 | 102 | real_preexec_fn = kwargs.pop("preexec_fn", None) 103 | def setpgid_preexec_fn(): 104 | os.setpgid(0, 0) 105 | if real_preexec_fn: 106 | apply(real_preexec_fn) 107 | 108 | kwargs['preexec_fn'] = setpgid_preexec_fn 109 | 110 | subprocess.Popen.__init__(self, *args, **kwargs) 111 | 112 | if mswindows: 113 | def _execute_child(self, args, executable, preexec_fn, close_fds, 114 | cwd, env, universal_newlines, startupinfo, 115 | creationflags, shell, 116 | p2cread, p2cwrite, 117 | c2pread, c2pwrite, 118 | errread, errwrite): 119 | if not isinstance(args, types.StringTypes): 120 | args = subprocess.list2cmdline(args) 121 | 122 | if startupinfo is None: 123 | startupinfo = winprocess.STARTUPINFO() 124 | 125 | if None not in (p2cread, c2pwrite, errwrite): 126 | startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES 127 | 128 | startupinfo.hStdInput = int(p2cread) 129 | startupinfo.hStdOutput = int(c2pwrite) 130 | startupinfo.hStdError = int(errwrite) 131 | if shell: 132 | startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW 133 | comspec = os.environ.get("COMSPEC", "cmd.exe") 134 | args = comspec + " /c " + args 135 | 136 | # We create a new job for this process, so that we can kill 137 | # the process and any sub-processes 138 | self._job = winprocess.CreateJobObject() 139 | 140 | creationflags |= winprocess.CREATE_SUSPENDED 141 | creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT 142 | 143 | hp, ht, pid, tid = winprocess.CreateProcess( 144 | executable, args, 145 | None, None, # No special security 146 | 1, # Must inherit handles! 147 | creationflags, 148 | winprocess.EnvironmentBlock(env), 149 | cwd, startupinfo) 150 | 151 | self._child_created = True 152 | self._handle = hp 153 | self._thread = ht 154 | self.pid = pid 155 | 156 | winprocess.AssignProcessToJobObject(self._job, hp) 157 | winprocess.ResumeThread(ht) 158 | 159 | if p2cread is not None: 160 | p2cread.Close() 161 | if c2pwrite is not None: 162 | c2pwrite.Close() 163 | if errwrite is not None: 164 | errwrite.Close() 165 | 166 | def kill(self, group=True): 167 | """Kill the process. If group=True, all sub-processes will also be killed.""" 168 | if mswindows: 169 | if group: 170 | winprocess.TerminateJobObject(self._job, 127) 171 | else: 172 | winprocess.TerminateProcess(self._handle, 127) 173 | self.returncode = 127 174 | else: 175 | if group: 176 | os.killpg(self.pid, signal.SIGKILL) 177 | else: 178 | os.kill(self.pid, signal.SIGKILL) 179 | self.returncode = -9 180 | 181 | def wait(self, timeout=-1, group=True): 182 | """Wait for the process to terminate. Returns returncode attribute. 183 | If timeout seconds are reached and the process has not terminated, 184 | it will be forcefully killed. If timeout is -1, wait will not 185 | time out.""" 186 | 187 | if self.returncode is not None: 188 | return self.returncode 189 | 190 | if mswindows: 191 | if timeout != -1: 192 | timeout = timeout * 1000 193 | rc = winprocess.WaitForSingleObject(self._handle, timeout) 194 | if rc == winprocess.WAIT_TIMEOUT: 195 | self.kill(group) 196 | else: 197 | self.returncode = winprocess.GetExitCodeProcess(self._handle) 198 | else: 199 | if timeout == -1: 200 | subprocess.Popen.wait(self) 201 | return self.returncode 202 | 203 | starttime = time.time() 204 | 205 | # Make sure there is a signal handler for SIGCHLD installed 206 | oldsignal = signal.signal(signal.SIGCHLD, DoNothing) 207 | 208 | while time.time() < starttime + timeout - 0.01: 209 | pid, sts = os.waitpid(self.pid, os.WNOHANG) 210 | if pid != 0: 211 | self._handle_exitstatus(sts) 212 | signal.signal(signal.SIGCHLD, oldsignal) 213 | return self.returncode 214 | 215 | # time.sleep is interrupted by signals (good!) 216 | newtimeout = timeout - time.time() + starttime 217 | time.sleep(newtimeout) 218 | 219 | self.kill(group) 220 | signal.signal(signal.SIGCHLD, oldsignal) 221 | subprocess.Popen.wait(self) 222 | 223 | return self.returncode 224 | -------------------------------------------------------------------------------- /src/AndroidLibrary/version.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.2.0' 2 | -------------------------------------------------------------------------------- /src/AndroidLibrary/winprocess.py: -------------------------------------------------------------------------------- 1 | # A module to expose various thread/process/job related structures and 2 | # methods from kernel32 3 | # 4 | # The MIT License 5 | # 6 | # Copyright (c) 2006 the Mozilla Foundation 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a 9 | # copy of this software and associated documentation files (the "Software"), 10 | # to deal in the Software without restriction, including without limitation 11 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 12 | # and/or sell copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | # DEALINGS IN THE SOFTWARE. 25 | 26 | from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE 27 | from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD 28 | 29 | LPVOID = c_void_p 30 | LPBYTE = POINTER(BYTE) 31 | LPDWORD = POINTER(DWORD) 32 | 33 | def ErrCheckBool(result, func, args): 34 | """errcheck function for Windows functions that return a BOOL True 35 | on success""" 36 | if not result: 37 | raise WinError() 38 | return args 39 | 40 | # CloseHandle() 41 | 42 | CloseHandleProto = WINFUNCTYPE(BOOL, HANDLE) 43 | CloseHandle = CloseHandleProto(("CloseHandle", windll.kernel32)) 44 | CloseHandle.errcheck = ErrCheckBool 45 | 46 | # AutoHANDLE 47 | 48 | class AutoHANDLE(HANDLE): 49 | """Subclass of HANDLE which will call CloseHandle() on deletion.""" 50 | def Close(self): 51 | if self.value: 52 | CloseHandle(self) 53 | self.value = 0 54 | 55 | def __del__(self): 56 | self.Close() 57 | 58 | def __int__(self): 59 | return self.value 60 | 61 | def ErrCheckHandle(result, func, args): 62 | """errcheck function for Windows functions that return a HANDLE.""" 63 | if not result: 64 | raise WinError() 65 | return AutoHANDLE(result) 66 | 67 | # PROCESS_INFORMATION structure 68 | 69 | class PROCESS_INFORMATION(Structure): 70 | _fields_ = [("hProcess", HANDLE), 71 | ("hThread", HANDLE), 72 | ("dwProcessID", DWORD), 73 | ("dwThreadID", DWORD)] 74 | 75 | def __init__(self): 76 | Structure.__init__(self) 77 | 78 | self.cb = sizeof(self) 79 | 80 | LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION) 81 | 82 | # STARTUPINFO structure 83 | 84 | class STARTUPINFO(Structure): 85 | _fields_ = [("cb", DWORD), 86 | ("lpReserved", LPWSTR), 87 | ("lpDesktop", LPWSTR), 88 | ("lpTitle", LPWSTR), 89 | ("dwX", DWORD), 90 | ("dwY", DWORD), 91 | ("dwXSize", DWORD), 92 | ("dwYSize", DWORD), 93 | ("dwXCountChars", DWORD), 94 | ("dwYCountChars", DWORD), 95 | ("dwFillAttribute", DWORD), 96 | ("dwFlags", DWORD), 97 | ("wShowWindow", WORD), 98 | ("cbReserved2", WORD), 99 | ("lpReserved2", LPBYTE), 100 | ("hStdInput", HANDLE), 101 | ("hStdOutput", HANDLE), 102 | ("hStdError", HANDLE) 103 | ] 104 | LPSTARTUPINFO = POINTER(STARTUPINFO) 105 | 106 | STARTF_USESHOWWINDOW = 0x01 107 | STARTF_USESIZE = 0x02 108 | STARTF_USEPOSITION = 0x04 109 | STARTF_USECOUNTCHARS = 0x08 110 | STARTF_USEFILLATTRIBUTE = 0x10 111 | STARTF_RUNFULLSCREEN = 0x20 112 | STARTF_FORCEONFEEDBACK = 0x40 113 | STARTF_FORCEOFFFEEDBACK = 0x80 114 | STARTF_USESTDHANDLES = 0x100 115 | 116 | # EnvironmentBlock 117 | 118 | class EnvironmentBlock: 119 | """An object which can be passed as the lpEnv parameter of CreateProcess. 120 | It is initialized with a dictionary.""" 121 | 122 | def __init__(self, dict): 123 | if not dict: 124 | self._as_parameter_ = None 125 | else: 126 | values = ["%s=%s" % (key, value) 127 | for (key, value) in dict.iteritems()] 128 | values.append("") 129 | self._as_parameter_ = LPCWSTR("\0".join(values)) 130 | 131 | # CreateProcess() 132 | 133 | CreateProcessProto = WINFUNCTYPE(BOOL, # Return type 134 | LPCWSTR, # lpApplicationName 135 | LPWSTR, # lpCommandLine 136 | LPVOID, # lpProcessAttributes 137 | LPVOID, # lpThreadAttributes 138 | BOOL, # bInheritHandles 139 | DWORD, # dwCreationFlags 140 | LPVOID, # lpEnvironment 141 | LPCWSTR, # lpCurrentDirectory 142 | LPSTARTUPINFO, # lpStartupInfo 143 | LPPROCESS_INFORMATION # lpProcessInformation 144 | ) 145 | 146 | CreateProcessFlags = ((1, "lpApplicationName", None), 147 | (1, "lpCommandLine"), 148 | (1, "lpProcessAttributes", None), 149 | (1, "lpThreadAttributes", None), 150 | (1, "bInheritHandles", True), 151 | (1, "dwCreationFlags", 0), 152 | (1, "lpEnvironment", None), 153 | (1, "lpCurrentDirectory", None), 154 | (1, "lpStartupInfo"), 155 | (2, "lpProcessInformation")) 156 | 157 | def ErrCheckCreateProcess(result, func, args): 158 | ErrCheckBool(result, func, args) 159 | # return a tuple (hProcess, hThread, dwProcessID, dwThreadID) 160 | pi = args[9] 161 | return AutoHANDLE(pi.hProcess), AutoHANDLE(pi.hThread), pi.dwProcessID, pi.dwThreadID 162 | 163 | CreateProcess = CreateProcessProto(("CreateProcessW", windll.kernel32), 164 | CreateProcessFlags) 165 | CreateProcess.errcheck = ErrCheckCreateProcess 166 | 167 | CREATE_BREAKAWAY_FROM_JOB = 0x01000000 168 | CREATE_DEFAULT_ERROR_MODE = 0x04000000 169 | CREATE_NEW_CONSOLE = 0x00000010 170 | CREATE_NEW_PROCESS_GROUP = 0x00000200 171 | CREATE_NO_WINDOW = 0x08000000 172 | CREATE_SUSPENDED = 0x00000004 173 | CREATE_UNICODE_ENVIRONMENT = 0x00000400 174 | DEBUG_ONLY_THIS_PROCESS = 0x00000002 175 | DEBUG_PROCESS = 0x00000001 176 | DETACHED_PROCESS = 0x00000008 177 | 178 | # CreateJobObject() 179 | 180 | CreateJobObjectProto = WINFUNCTYPE(HANDLE, # Return type 181 | LPVOID, # lpJobAttributes 182 | LPCWSTR # lpName 183 | ) 184 | 185 | CreateJobObjectFlags = ((1, "lpJobAttributes", None), 186 | (1, "lpName", None)) 187 | 188 | CreateJobObject = CreateJobObjectProto(("CreateJobObjectW", windll.kernel32), 189 | CreateJobObjectFlags) 190 | CreateJobObject.errcheck = ErrCheckHandle 191 | 192 | # AssignProcessToJobObject() 193 | 194 | AssignProcessToJobObjectProto = WINFUNCTYPE(BOOL, # Return type 195 | HANDLE, # hJob 196 | HANDLE # hProcess 197 | ) 198 | AssignProcessToJobObjectFlags = ((1, "hJob"), 199 | (1, "hProcess")) 200 | AssignProcessToJobObject = AssignProcessToJobObjectProto( 201 | ("AssignProcessToJobObject", windll.kernel32), 202 | AssignProcessToJobObjectFlags) 203 | AssignProcessToJobObject.errcheck = ErrCheckBool 204 | 205 | # ResumeThread() 206 | 207 | def ErrCheckResumeThread(result, func, args): 208 | if result == -1: 209 | raise WinError() 210 | 211 | return args 212 | 213 | ResumeThreadProto = WINFUNCTYPE(DWORD, # Return type 214 | HANDLE # hThread 215 | ) 216 | ResumeThreadFlags = ((1, "hThread"),) 217 | ResumeThread = ResumeThreadProto(("ResumeThread", windll.kernel32), 218 | ResumeThreadFlags) 219 | ResumeThread.errcheck = ErrCheckResumeThread 220 | 221 | # TerminateJobObject() 222 | 223 | TerminateJobObjectProto = WINFUNCTYPE(BOOL, # Return type 224 | HANDLE, # hJob 225 | UINT # uExitCode 226 | ) 227 | TerminateJobObjectFlags = ((1, "hJob"), 228 | (1, "uExitCode", 127)) 229 | TerminateJobObject = TerminateJobObjectProto( 230 | ("TerminateJobObject", windll.kernel32), 231 | TerminateJobObjectFlags) 232 | TerminateJobObject.errcheck = ErrCheckBool 233 | 234 | # WaitForSingleObject() 235 | 236 | WaitForSingleObjectProto = WINFUNCTYPE(DWORD, # Return type 237 | HANDLE, # hHandle 238 | DWORD, # dwMilliseconds 239 | ) 240 | WaitForSingleObjectFlags = ((1, "hHandle"), 241 | (1, "dwMilliseconds", -1)) 242 | WaitForSingleObject = WaitForSingleObjectProto( 243 | ("WaitForSingleObject", windll.kernel32), 244 | WaitForSingleObjectFlags) 245 | 246 | INFINITE = -1 247 | WAIT_TIMEOUT = 0x0102 248 | WAIT_OBJECT_0 = 0x0 249 | WAIT_ABANDONED = 0x0080 250 | 251 | # GetExitCodeProcess() 252 | 253 | GetExitCodeProcessProto = WINFUNCTYPE(BOOL, # Return type 254 | HANDLE, # hProcess 255 | LPDWORD, # lpExitCode 256 | ) 257 | GetExitCodeProcessFlags = ((1, "hProcess"), 258 | (2, "lpExitCode")) 259 | GetExitCodeProcess = GetExitCodeProcessProto( 260 | ("GetExitCodeProcess", windll.kernel32), 261 | GetExitCodeProcessFlags) 262 | GetExitCodeProcess.errcheck = ErrCheckBool 263 | -------------------------------------------------------------------------------- /tests/apidemos/__init__.txt: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | 3 | Resource variables.txt 4 | 5 | Library AndroidLibrary 6 | Library OperatingSystem 7 | 8 | Suite Setup Setup Suite 9 | Suite Teardown Stop Emulator 10 | 11 | Documentation Tests the ApiDemos.apk that is included in the Android SDK 12 | 13 | *** Keywords *** 14 | 15 | Setup Suite 16 | 17 | Android SDK Should Exist 18 | Update Android SDK 19 | Create Android Virtual Device 20 | 21 | ${HEADLESS_BOOL}= Convert To Boolean ${HEADLESS} 22 | Start Emulator ${EMULATOR_NAME} no_window=${HEADLESS_BOOL} 23 | 24 | Pull ApiDemos.apk from Device 25 | Re-Sign ApiDemos.apk with Debug Keystore 26 | Build Instrumentation App 27 | 28 | Install App 29 | 30 | Stop Emulator 31 | 32 | Android SDK Should Exist 33 | [Documentation] simple sanity check to see if %{ANDROID_HOME} was set correctly 34 | File Should Exist %{ANDROID_HOME}/tools/android 35 | 36 | Update Android SDK 37 | Execute %{ANDROID_HOME}/tools/android update sdk -t android-${API_LEVEL} --no-ui 38 | Execute %{ANDROID_HOME}/tools/android update sdk -t addon-google_apis-google-${API_LEVEL} --no-ui 39 | 40 | Create Android Virtual Device 41 | Execute echo "no" | %{ANDROID_HOME}/tools/android --silent create avd --name ${EMULATOR_NAME} --force -t android-${API_LEVEL} 42 | 43 | Pull ApiDemos.apk from Device 44 | [Timeout] 5 minutes 45 | 46 | Wait For Device 47 | 48 | Remove File ApiDemos.apk 49 | 50 | Wait Until Keyword Succeeds 30 seconds 5 seconds 51 | ... Execute %{ANDROID_HOME}/platform-tools/adb pull /data/app/ApiDemos.apk 52 | 53 | Re-Sign ApiDemos.apk with Debug Keystore 54 | File Should Exist ApiDemos.apk 55 | Execute zip -d ApiDemos.apk META-INF/* 56 | Execute echo "android" | jarsigner -verbose -keystore $HOME/.android/debug.keystore ApiDemos.apk androiddebugkey 57 | 58 | Build Instrumentation App 59 | Remove Directory ${EXECDIR}/test_servers/ recursive=True 60 | Execute calabash-android build ApiDemos.apk 61 | 62 | ${TEST_SERVERS}= List Directory ${EXECDIR}/test_servers/ 63 | Set Global Variable ${TEST_APK} ${TEST_SERVERS[0]} 64 | 65 | Install App 66 | [Timeout] 5 minutes 67 | 68 | Wait for Device 69 | 70 | Uninstall Application com.example.android.apis 71 | 72 | Uninstall Application com.example.android.apis.test 73 | 74 | Install Application ${EXECDIR}/test_servers/${TEST_APK} 75 | 76 | Install Application ${EXECDIR}/ApiDemos.apk 77 | 78 | -------------------------------------------------------------------------------- /tests/apidemos/apidemos.txt: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | 3 | Resource variables.txt 4 | 5 | Library AndroidLibrary 6 | 7 | Suite Setup Setup Suite 8 | Test Setup Setup Test 9 | Test Teardown Stop Testserver 10 | 11 | Test Timeout 10 minutes 12 | 13 | *** Keywords *** 14 | 15 | Setup Suite 16 | ${HEADLESS_BOOL}= Convert To Boolean ${HEADLESS} 17 | Start Emulator ${EMULATOR_NAME} no_window=${HEADLESS_BOOL} 18 | Wait For Device 19 | Press Menu Button 20 | 21 | Setup Test 22 | Start Testserver with apk ApiDemos.apk 23 | 24 | Wait Until Keyword Succeeds 25 | ... 1min 26 | ... 5sec 27 | ... Connect To Testserver 28 | 29 | *** Keywords *** 30 | 31 | Setup Set Text Test 32 | Capture Screenshot 33 | Touch Text Views 34 | Capture Screenshot 35 | Touch Text Controls 36 | Capture Screenshot 37 | Touch Text 1. Light Theme 38 | Capture Screenshot 39 | 40 | *** Test Cases *** 41 | 42 | Screen contains text 43 | Capture Screenshot 44 | 45 | Screen Should Contain App 46 | 47 | Screen does not contain text 48 | Capture Screenshot 49 | 50 | Screen Should Not Contain Hoschi 51 | 52 | Set Text By Number 53 | Setup Set Text Test 54 | Set Text 1 Affe 55 | Capture Screenshot 56 | Screen Should Contain Affe 57 | 58 | Set Text By Number with Explicit Locator 59 | Setup Set Text Test 60 | Set Text num=1 Katze 61 | Capture Screenshot 62 | Screen Should Contain Katze 63 | 64 | Touch Text 65 | Capture Screenshot 66 | 67 | Touch Text Text 68 | Capture Screenshot 69 | 70 | Touch Text Linkify 71 | Capture Screenshot 72 | 73 | Screen Should Contain http 74 | Capture Screenshot 75 | 76 | Scroll a bit 77 | Capture Screenshot 78 | 79 | Touch Text Views 80 | Capture Screenshot 81 | 82 | Touch Text ScrollBars 83 | Capture Screenshot 84 | 85 | Touch Text 2. Fancy 86 | Capture Screenshot 87 | 88 | Scroll Down 89 | Capture Screenshot 90 | 91 | Scroll Up 92 | Capture Screenshot 93 | 94 | Swipe Left 95 | Capture Screenshot 96 | 97 | Swipe Right 98 | Capture Screenshot 99 | 100 | 101 | Touch some imagebuttons 102 | Capture Screenshot 103 | 104 | Touch Text Views 105 | 106 | Capture Screenshot 107 | 108 | Touch Text ImageButton 109 | 110 | Capture Screenshot 111 | 112 | Touch Image Button 1 113 | 114 | Capture Screenshot 115 | 116 | Touch Image Button num=2 117 | -------------------------------------------------------------------------------- /tests/apidemos/variables.txt: -------------------------------------------------------------------------------- 1 | *** Variables *** 2 | 3 | ${EMULATOR_NAME}= robotframework-androidlibrary-emulator 4 | ${API_LEVEL}= 8 5 | ${SKIP_SETUP}= False 6 | ${HEADLESS}= True 7 | 8 | *** Settings *** 9 | 10 | Library OperatingSystem 11 | 12 | *** Keywords *** 13 | 14 | Execute [Arguments] ${command} 15 | ${rc} ${output} = Run And Return Rc And Output ${command} 16 | Should Be Equal As Integers ${rc} 0 17 | 18 | --------------------------------------------------------------------------------