diff --git a/README.md b/README.md index 95fcc59..ca5c5c4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ Features 1. Android emulator 2. noVNC 3. Appium server -4. Browser application for mobile website testing +4. Able to connect to selenium grid +5. Browser application for mobile website testing - Chrome version 55 (for x86 and armeabi) - Firefox version 51 (for x86 and armeabi) @@ -29,14 +30,20 @@ Quick Start 2. Run docker-appium with command: ```bash - docker run -d -p 6080:6080 -p 4723:4723 -v :/target_apk -e ANDROID_VERSION= -e EMULATOR_TYPE= --name appium-container butomo1989/docker-appium + docker run -d -p 6080:6080 -p 4723:4723 -v :/target_apk -e ANDROID_VERSION= -e EMULATOR_TYPE= -e CONNECT_TO_GRID= --name appium-container butomo1989/docker-appium ``` An Example: ```bash - docker run -d -p 6080:6080 -p 4723:4723 -v $PWD/example/sample_apk:/target_apk -e ANDROID_VERSION=4.2.2 -e EMULATOR_TYPE=armeabi --name appium-container butomo1989/docker-appium + docker run -d -p 6080:6080 -p 4723:4723 -v $PWD/example/sample_apk:/target_apk -e ANDROID_VERSION=4.2.2 -e EMULATOR_TYPE=armeabi -e CONNECT_TO_GRID=False --name appium-container butomo1989/docker-appium ``` + **Optional arguments for CONNECT\_TO\_GRID=True** + + -e APPIUM_HOST="": if appium is running under different host. default value: 127.0.0.1 + -e APPIUM_PORT=: if appium is running under different port. default port: 4723 + -e SELENIUM_HOST="": if selenium hub is running under different host. default value: 127.0.0.1 + -e SELENIUM_PORT=: if selenium hub is running under different port. default port: 4444 **Note: use flag *--privileged* and *EMULATOR_TYPE=x86* for ubuntu OS to make emulator faster** diff --git a/service/__init__.py b/service/__init__.py index e69de29..767cb01 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -0,0 +1,4 @@ +import os + +WORKDIR = os.path.dirname(__file__) +CONFIG_FILE = os.path.join(WORKDIR, 'nodeconfig.json') diff --git a/service/appium.py b/service/appium.py new file mode 100644 index 0000000..f1369f9 --- /dev/null +++ b/service/appium.py @@ -0,0 +1,50 @@ +import json + + +def create_node_config(config_file, emulator_name, android_version, appium_host, appium_port, + selenium_host, selenium_port): + """ + Create custom node config file in json format to be able to connect with selenium server. + + :param config_file: config file + :type config_file: str + :param emulator_name: emulator name + :type emulator_name: str + :param android_version: android version of android emulator + :type android_version: str + :param appium_host: host where appium server is running + :type appium_host: str + :param appium_port: port number where where appium server is running + :type appium_port: int + :param selenium_host: host where selenium server is running + :type selenium_host: str + :param selenium_port: port number where selenium server is running + :type selenium_port: int + + """ + config = { + 'capabilities': [ + { + 'platform': 'Android', + 'platformName': 'Android', + 'version': android_version, + 'browserName': emulator_name, + 'maxInstances': 1, + } + ], + 'configuration': { + 'cleanUpCycle': 2000, + 'timeout': 30000, + 'proxy': 'org.openqa.grid.selenium.proxy.DefaultRemoteProxy', + 'url': 'http://{host}:{port}/wd/hub'.format(host=appium_host, port=appium_port), + 'host': appium_host, + 'port': appium_port, + 'maxSession': 6, + 'register': True, + 'registerCycle': 5000, + 'hubHost': selenium_host, + 'hubPort': selenium_port + } + } + with open(config_file, 'w') as cf: + cf.write(json.dumps(config)) diff --git a/service/start.py b/service/start.py index dbfdac2..f6dd3c3 100644 --- a/service/start.py +++ b/service/start.py @@ -3,8 +3,12 @@ import os import re import subprocess +import appium + +from service import CONFIG_FILE + logging.basicConfig() -logger = logging.getLogger('android_appium') +logger = logging.getLogger('main') # not using enum because need to install pip that will make docker image size bigger TYPE_ARMEABI = 'armeabi' @@ -18,7 +22,8 @@ def run(): """ # Get android version package android_version = os.getenv('ANDROID_VERSION', '4.2.2') - os.environ['emulator_name'] = 'emulator_{version}'.format(version=android_version) + emulator_name = 'emulator_{version}'.format(version=android_version) + os.environ['EMULATOR_NAME'] = emulator_name # Get emulator type types = [TYPE_ARMEABI, TYPE_X86] @@ -33,16 +38,35 @@ def run(): subprocess.check_call('Xvfb ${DISPLAY} -screen ${SCREEN} ${SCREEN_WIDTH}x${SCREEN_HEIGHT}x${SCREEN_DEPTH} & ' 'sleep ${TIMEOUT}', shell=True) - # Start noVNC, installation of android packages, emulator creation and appium + # Start noVNC vnc_cmd = 'openbox-session & x11vnc -display ${DISPLAY} -nopw -ncache 10 -forever & ' \ './noVNC/utils/launch.sh --vnc localhost:${LOCAL_PORT} --listen ${TARGET_PORT}' + + # Option to connect with selenium server + connect_to_grid = str_to_bool(str(os.getenv('CONNECT_TO_GRID', False))) + logger.info('Connect with selenium grid? {input}'.format(input=connect_to_grid)) + appium_cmd = 'appium' + if connect_to_grid: + try: + appium_host = os.getenv('APPIUM_HOST', '127.0.0.1') + appium_port = int(os.getenv('APPIUM_PORT', 4723)) + selenium_host = os.getenv('SELENIUM_HOST', '172.17.0.1') + selenium_port = int(os.getenv('SELENIUM_PORT', 4444)) + appium.create_node_config(CONFIG_FILE, emulator_name, android_version, + appium_host, appium_port, selenium_host, selenium_port) + appium_cmd += ' --nodeconfig {file}'.format(file=CONFIG_FILE) + except ValueError as v_err: + logger.error(v_err) + + # Start installation of android packages, emulator creation and appium in a terminal android_cmd = get_android_bash_commands(android_version, emulator_type) if android_cmd: cmd = '({vnc}) & (xterm -T "Android-Appium" -n "Android-Appium" -e \"{android} && ' \ - '/bin/echo $emulator_name && appium\")'.format(vnc=vnc_cmd, android=android_cmd) + '/bin/echo $EMULATOR_NAME && {appium}\")'.format( + vnc=vnc_cmd, android=android_cmd, appium=appium_cmd) else: logger.warning('There is no android packages installed!') - cmd = '({vnc}) & (xterm -e \"appium\")'.format(vnc=vnc_cmd) + cmd = '({vnc}) & (xterm -e \"{appium}\")'.format(vnc=vnc_cmd, appium=appium_cmd) subprocess.check_call(cmd, shell=True) @@ -64,7 +88,7 @@ def get_item_position(keyword, items): """ Get position of item in array by given keyword. - :return: Item position. + :return: item position :rtype: int """ pos = 0 @@ -129,6 +153,18 @@ def get_android_bash_commands(android_version, emulator_type): return bash_command +def str_to_bool(str): + """ + Convert string to boolean. + + :param str: given string + :type str: str + :return: converted string + :rtype: bool + """ + return str.lower() in ('yes', 'true', 't', '1') + + if __name__ == '__main__': logger.setLevel(logging.INFO) run() diff --git a/service/tests/__init__.py b/service/tests/__init__.py index 57c9215..9bfca02 100644 --- a/service/tests/__init__.py +++ b/service/tests/__init__.py @@ -1,4 +1,6 @@ """Unit test for start.py.""" +import os + from unittest import TestCase import mock @@ -9,14 +11,47 @@ from service import start class TestService(TestCase): """Unit test class to test method run.""" - @mock.patch('service.start.get_android_bash_commands') + def setUp(self): + os.environ['ANDROID_VERSION'] = '4.2.2' + os.environ['EMULATOR_TYPE'] = start.TYPE_X86 + os.environ['CONNECT_TO_GRID'] = str(False) + @mock.patch('subprocess.check_call') - def test_service(self, mocked_bash_cmd, mocked_subprocess): - self.assertFalse(mocked_bash_cmd.called) + @mock.patch('service.start.get_android_bash_commands') + def test_without_selenium_server(self, mocked_subprocess, mocked_bash_cmd): self.assertFalse(mocked_subprocess.called) + self.assertFalse(mocked_bash_cmd.called) start.run() - self.assertTrue(mocked_bash_cmd.called) self.assertTrue(mocked_subprocess.called) + self.assertTrue(mocked_bash_cmd.called) + + @mock.patch('subprocess.check_call') + @mock.patch('service.appium.create_node_config') + @mock.patch('service.start.get_android_bash_commands') + def test_with_selenium_server(self, mocked_subprocess, mocked_config, mocked_bash_cmd): + os.environ['CONNECT_TO_GRID'] = str(True) + self.assertFalse(mocked_subprocess.called) + self.assertFalse(mocked_config.called) + self.assertFalse(mocked_bash_cmd.called) + start.run() + self.assertTrue(mocked_subprocess.called) + self.assertTrue(mocked_config.called) + self.assertTrue(mocked_bash_cmd.called) + + @mock.patch('subprocess.check_call') + @mock.patch('service.appium.create_node_config') + @mock.patch('service.start.get_android_bash_commands') + def test_invalid_integer(self, mocked_subprocess, mocked_config, mocked_bash_cmd): + os.environ['CONNECT_TO_GRID'] = str(True) + os.environ['APPIUM_PORT'] = 'test' + self.assertFalse(mocked_subprocess.called) + self.assertFalse(mocked_config.called) + self.assertFalse(mocked_bash_cmd.called) + start.run() + self.assertTrue(mocked_subprocess.called) + self.assertFalse(mocked_config.called) + self.assertTrue(mocked_bash_cmd.called) + self.assertRaises(ValueError) @mock.patch('service.start.get_android_bash_commands') @mock.patch('subprocess.check_call') @@ -28,3 +63,11 @@ class TestService(TestCase): start.run() self.assertTrue(mocked_subprocess.called) self.assertTrue(mocked_logger_warning.called) + + def tearDown(self): + del os.environ['ANDROID_VERSION'] + del os.environ['EMULATOR_TYPE'] + if os.getenv('CONNECT_TO_GRID') == str(True): + del os.environ['CONNECT_TO_GRID'] + if os.getenv('APPIUM_PORT'): + del os.environ['APPIUM_PORT'] diff --git a/service/tests/test_appium_config.py b/service/tests/test_appium_config.py new file mode 100644 index 0000000..ed37e96 --- /dev/null +++ b/service/tests/test_appium_config.py @@ -0,0 +1,17 @@ +"""Unit test for appium.py.""" +import os + +from unittest import TestCase + +from service import CONFIG_FILE +from service import appium + + +class TestAppiumConfig(TestCase): + """Unit test class to test method create_node_config.""" + + def test_create_node_config(self): + self.assertFalse(os.path.exists(CONFIG_FILE)) + appium.create_node_config(CONFIG_FILE, 'emulator_name', '4.2.2', '127.0.0.1', 4723, '127.0.0.1', 4444) + self.assertTrue(os.path.exists(CONFIG_FILE)) + os.remove(CONFIG_FILE)