diff --git a/.travis.yml b/.travis.yml index ce8c9dc..8ab6316 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,15 @@ language: python + python: - "2.7" + install: "pip install -r requirements.txt" -script: nosetests + +script: bash release.sh test 5.0 x86 + branches: only: - master + after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/Dockerfile b/Dockerfile index 1b4604e..fc755da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,21 +78,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN git clone https://github.com/kanaka/noVNC.git \ && cd noVNC/utils && git clone https://github.com/kanaka/websockify websockify -#====================================== -# Install Android SDK and its packages -#====================================== +#===================== +# Install Android SDK +#===================== ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64/jre ENV PATH ${PATH}:${JAVA_HOME}/bin ENV SDK_VERSION=25.2.3 \ - BUILD_TOOL=25.0.2 \ ANDROID_HOME=/root RUN wget -O android.zip https://dl.google.com/android/repository/tools_r${SDK_VERSION}-linux.zip \ && unzip android.zip && rm android.zip ENV PATH ${PATH}:${ANDROID_HOME}/tools -RUN echo y | android update sdk --no-ui -a --filter platform-tools,build-tools-${BUILD_TOOL} -ENV PATH ${PATH}:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/build-tools -RUN mv ${ANDROID_HOME}/tools/emulator ${ANDROID_HOME}/tools/emulator.backup +RUN echo y | android update sdk --no-ui -a --filter platform-tools +ENV PATH ${PATH}:${ANDROID_HOME}/platform-tools #==================================== # Install latest nodejs, npm, appium @@ -102,6 +100,26 @@ RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - \ ENV APPIUM_VERSION 1.6.3 RUN npm install -g appium@$APPIUM_VERSION && npm cache clean +#====================== +# Install SDK packages +#====================== +ARG ANDROID_VERSION=5.0.1 +ARG BUILD_TOOL=21.1.2 +ARG API_LEVEL=21 +ARG PROCESSOR=x86 +ARG SYS_IMG=x86_64 +ENV ANDROID_VERSION=$ANDROID_VERSION \ + BUILD_TOOL=$BUILD_TOOL \ + API_LEVEL=$API_LEVEL \ + PROCESSOR=$PROCESSOR \ + SYS_IMG=$SYS_IMG +RUN echo y | android update sdk --no-ui -a --filter build-tools-${BUILD_TOOL} +ENV PATH ${PATH}:${ANDROID_HOME}/build-tools + +RUN rm ${ANDROID_HOME}/tools/emulator \ + && ln -s ${ANDROID_HOME}/tools/emulator64-${PROCESSOR} ${ANDROID_HOME}/tools/emulator +RUN echo y | android update sdk --no-ui -a -t android-${API_LEVEL},sys-img-${SYS_IMG}-android-${API_LEVEL} + #================================================ # noVNC Default Configurations # These Configurations can be changed through -e @@ -127,11 +145,6 @@ RUN ln -s noVNC/vnc_auto.html noVNC/index.html #=============== EXPOSE 4723 6080 -#================== -# Add Browser APKs -#================== -COPY browser_apk /root/browser_apk - #====================== # Add Emulator Devices #====================== @@ -140,6 +153,6 @@ COPY devices /root/devices #=================== # Run docker-appium #=================== -COPY supervisord.conf /root/ COPY src /root/src +COPY supervisord.conf /root/ CMD /usr/bin/supervisord --configuration supervisord.conf diff --git a/browser_apk/chrome_55.0.2883.91-288309100_min_android4.1_armeabi-v7a.apk b/browser_apk/chrome_55.0.2883.91-288309100_min_android4.1_armeabi-v7a.apk deleted file mode 100644 index c0313c2..0000000 Binary files a/browser_apk/chrome_55.0.2883.91-288309100_min_android4.1_armeabi-v7a.apk and /dev/null differ diff --git a/browser_apk/chrome_55.0.2883.91_min_android4.1_x86.apk b/browser_apk/chrome_55.0.2883.91_min_android4.1_x86.apk deleted file mode 100644 index e92945a..0000000 Binary files a/browser_apk/chrome_55.0.2883.91_min_android4.1_x86.apk and /dev/null differ diff --git a/browser_apk/firefox_51.0-2015466281_min_android4.0.3_armeabi-v7a.apk b/browser_apk/firefox_51.0-2015466281_min_android4.0.3_armeabi-v7a.apk deleted file mode 100644 index bab89e1..0000000 Binary files a/browser_apk/firefox_51.0-2015466281_min_android4.0.3_armeabi-v7a.apk and /dev/null differ diff --git a/browser_apk/firefox_51.0-2015466284_min_android4.0.3_x86.apk b/browser_apk/firefox_51.0-2015466284_min_android4.0.3_x86.apk deleted file mode 100644 index c35055f..0000000 Binary files a/browser_apk/firefox_51.0-2015466284_min_android4.0.3_x86.apk and /dev/null differ diff --git a/browser_apk/source.txt b/browser_apk/source.txt deleted file mode 100644 index b9d77c3..0000000 --- a/browser_apk/source.txt +++ /dev/null @@ -1 +0,0 @@ -Browser apps are downloaded from http://www.apkmirror.com \ No newline at end of file diff --git a/example/android/python/msite_simple_chrome.py b/example/android/python/msite_simple_chrome.py deleted file mode 100644 index 61bab1e..0000000 --- a/example/android/python/msite_simple_chrome.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest - -from appium import webdriver - - -class MSiteChromeAndroidUITests(unittest.TestCase): - - def setUp(self): - desired_caps = { - 'platformName': 'Android', - 'deviceName': 'Android Emulator', - # For emulator type armeabi, please use browser apk : - # /root/browser_apk/chrome_55.0.2883.91-288309100_min_android4.1_armeabi-v7a.apk - 'app': '/root/browser_apk/chrome_55.0.2883.91_min_android4.1_x86.apk', - 'appPackage': 'com.android.chrome', - 'appActivity': 'com.google.android.apps.chrome.Main', - 'avd': 'nexus_5_5.0' - } - self.driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps) - - def test_open_url(self): - self.driver.get('http://targeturl.com') - - def tearDown(self): - self.driver.quit() - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(MSiteChromeAndroidUITests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/example/android/python/msite_simple_firefox.py b/example/android/python/msite_simple_firefox.py deleted file mode 100644 index 897c90b..0000000 --- a/example/android/python/msite_simple_firefox.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest - -from appium import webdriver - - -class MSiteFirefoxAndroidUITests(unittest.TestCase): - - def setUp(self): - desired_caps = { - 'platformName': 'Android', - 'deviceName': 'Android Emulator', - # For emulator type armeabi, please use browser apk : - # /root/browser_apk/firefox_51.0-2015466281_min_android4.0.3_armeabi-v7a.apk - 'app': '/root/browser_apk/firefox_51.0-2015466284_min_android4.0.3_x86.apk', - 'appPackage': 'org.mozilla.firefox', - 'appActivity': 'org.mozilla.gecko.LauncherActivity', - 'avd': 'nexus_5_5.0' - } - self.driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps) - - def test_open_url(self): - self.driver.get('http://targeturl.com') - - def tearDown(self): - self.driver.quit() - -if __name__ == '__main__': - suite = unittest.TestLoader().loadTestsFromTestCase(MSiteFirefoxAndroidUITests) - unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..358bc18 --- /dev/null +++ b/release.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Bash version should >= 4 to be able to run this script. + +IMAGE="butomo1989/docker-android" + +if [ -z "$1" ]; then + read -p "Environment (test|build|push|all) : " TASK +else + TASK=$1 +fi + +if [ -z "$2" ]; then + read -p "Android version: " GIVEN_VERSION +else + GIVEN_VERSION=$2 +fi + +if [ -z "$3" ]; then + read -p "Processor type (x86|arm): " PROCESSOR +else + PROCESSOR=$3 +fi + +function build_tool() { + declare -A build_tools=( + [5.0.1]=21.1.2 + [5.1.1]=22.0.1 + [6.0]=23.0.3 + [7.0]=24.0.3 + [7.1.1]=25.0.2 + ) + + # TODO: Need to be sorted + for key in "${!build_tools[@]}"; do + if [[ $key == *"$GIVEN_VERSION"* ]]; then + version=$key + fi + done + + # If version cannot be found in the list + if [ -z "$version" ]; then + echo "Version is not found in the list or not supported! Support only version 5.0.1, 5.1.1, 6.0, 7.0, 7.1.1" + exit 1 + fi + + echo "Android version: $version" + build_tools=${build_tools[$version]} + echo "Build tool: $build_tools" +} + +function api_level() { + declare -A levels=( + [5.0.1]=21 + [5.1.1]=22 + [6.0]=23 + [7.0]=24 + [7.1.1]=25 + ) + + level=${levels[$version]} + echo "Api level: $level" +} + +function system_image() { + case $PROCESSOR in + x86) + sys_img=x86_64 + ;; + arm) + sys_img=armeabi-v7a + ;; + *) + echo "Invalid processor! Valid options: x86, arm" + exit 1 + ;; + esac + echo "Processor: $PROCESSOR" + echo "System Image: $sys_img" +} + +function init() { + build_tool + api_level + system_image +} + +init +IMAGE_NAME="$IMAGE-$PROCESSOR-$version" +echo "Image tag: $TAG" + +function test() { + (export ANDROID_HOME=/root && export ANDROID_VERSION=$version && export API_LEVEL=$level \ + && export PROCESSOR=$PROCESSOR && export SYS_IMG=$sys_img && nosetests -v) +} + +function build() { + # Remove pyc files + find . -name "*.pyc" -exec rm -f {} \; + + docker build -t $IMAGE_NAME --build-arg ANDROID_VERSION=$version --build-arg BUILD_TOOL=$$build_tools \ + --build-arg API_LEVEL=$level --build-arg PROCESSOR=$PROCESSOR --build-arg SYS_IMG=$sys_img . +} + +function push() { + docker push $IMAGE_NAME +} + +case $TASK in + test) + test + ;; + build) + build + ;; + push) + push + ;; + all) + test + build + push + ;; + *) + echo "Invalid environment! Valid options: test, build, push, all" + ;; +esac diff --git a/requirements.txt b/requirements.txt index 21974b7..95aaf43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -autopep8==1.2.4 coverage==4.2 mock==2.0.0 nose==1.3.7 diff --git a/src/__init__.py b/src/__init__.py index 6d2f72b..c4ddef8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,7 +1,6 @@ import os ROOT = '/root' -ANDROID_PATH = os.getenv('ANDROID_HOME', '/root') WORKDIR = os.path.dirname(__file__) CONFIG_FILE = os.path.join(WORKDIR, 'nodeconfig.json') LOGGING_FILE = os.path.join(WORKDIR, 'logging.conf') diff --git a/src/android.py b/src/android.py deleted file mode 100644 index c1667b5..0000000 --- a/src/android.py +++ /dev/null @@ -1,126 +0,0 @@ -import logging -import os -import subprocess - -from src import ANDROID_PATH - -logger = logging.getLogger('android') - -EMULATOR = 'emulator' -TYPE_ARMEABI = 'armeabi' -TYPE_X86 = 'x86' -TYPE_X86_64 = 'x86_64' - -API_LEVELS = { - '2.1': 7, - '2.2': 8, - '2.3.1': 9, - '2.3.3': 1, - '3.0': 11, - '3.1': 12, - '3.2': 13, - '4.0': 14, - '4.0.3': 15, - '4.1.2': 16, - '4.2.2': 17, - '4.3.1': 18, - '4.4.2': 19, - '4.4W.2': 20, - '5.0.1': 21, - '5.1.1': 22, - '6.0': 23, - '7.0': 24, - '7.1.1': 25 -} - - -def get_api_level(android_version): - """ - Get api level of android version. - - :param android_version: android version - :type android_version: str - :return: api level - :rtype: int - """ - api_level = None - - try: - for key in sorted(API_LEVELS): - if android_version in key: - api_level = API_LEVELS.get(key) - except TypeError as t_err: - logger.error(t_err) - - return api_level - - -def install_package(emulator_file, api_level, sys_img): - """ - Install sdk package. - - :param emulator_file: emulator file that need to be link - :type emulator_file: str - :param api_level: api level - :type api_level: str - :param sys_img: system image of emulator - :type sys_img: str - """ - # Link emulator shortcut - emu_file = os.path.join(ANDROID_PATH, 'tools', emulator_file) - emu_target = os.path.join(ANDROID_PATH, 'tools', 'emulator') - os.symlink(emu_file, emu_target) - - # Install package based on given android version - cmd = 'echo y | android update sdk --no-ui -a -t android-{api},sys-img-{sys_img}-android-{api}'.format( - api=api_level, sys_img=sys_img) - logger.info('SDK package installation command: {install}'.format(install=cmd)) - titel = 'SDK package installation process' - subprocess.check_call('xterm -T "{titel}" -n "{titel}" -e \"{cmd}\"'.format(titel=titel, cmd=cmd), shell=True) - - -def create_avd(device, avd_name, api_level): - """ - Create android virtual device. - - :param device: name of device - :type device: str - :param avd_name: desire name - :type avd_name: str - :param api_level: api level - :type api_level: str - """ - # Create android emulator - cmd = 'echo no | android create avd -f -n {name} -t android-{api}'.format(name=avd_name, api=api_level) - if device != EMULATOR: - # Link emulator skins - from src import ROOT - skin_rsc_path = os.path.join(ROOT, 'devices', 'skins') - logger.info('Skin ressource path: {rsc}'.format(rsc=skin_rsc_path)) - - skin_dst_path = os.path.join(ANDROID_PATH, 'platforms', 'android-{api}'.format(api=api_level), 'skins') - logger.info('Skin destination path: {dst}'.format(dst=skin_dst_path)) - - for s in os.listdir(skin_rsc_path): - os.symlink(os.path.join(skin_rsc_path, s), os.path.join(skin_dst_path, s)) - - # Hardware and its skin - device_name_bash = device.replace(' ', '\ ') - skin_name = device.replace(' ', '_').lower() - logger.info('device name in bash: {db}, skin name: {skin}'.format(db=device_name_bash, skin=skin_name)) - - # For custom hardware profile - profile_dst_path = os.path.join(ROOT, '.android', 'devices.xml') - if 'samsung' in device.lower(): - # profile file name = skin name - profile_src_path = os.path.join(ROOT, 'devices', 'profiles', '{profile}.xml'.format(profile=skin_name)) - logger.info('Hardware profile resource path: {rsc}'.format(rsc=profile_src_path)) - logger.info('Hardware profile destination path: {dst}'.format(dst=profile_dst_path)) - os.symlink(profile_src_path, profile_dst_path) - - # append command - cmd += ' -d {device} -s {skin}'.format(device=device_name_bash, skin=skin_name) - - logger.info('AVD creation command: {cmd}'.format(cmd=cmd)) - titel = 'AVD creation process' - subprocess.check_call('xterm -T "{titel}" -n "{titel}" -e \"{cmd}\"'.format(titel=titel, cmd=cmd), shell=True) diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..644ee6a --- /dev/null +++ b/src/app.py @@ -0,0 +1,182 @@ +import json +import logging +import os +import subprocess + +from src import CONFIG_FILE, ROOT +from src import log + +log.init() +logger = logging.getLogger('app') + + +def get_or_raise(env): + """ + Check if needed environment variables are given. + + :param env: key + :type env: str + :return: value + :rtype: str + """ + env_value = os.getenv(env) + if not env_value: + raise RuntimeError('The environment variable {0:s} is missing.' + 'Please check docker image or Dockerfile!'.format(env)) + return env_value + + +def str_to_bool(str): + """ + Convert string to boolean. + + :param str: given string + :type str: str + :return: converted string + :rtype: bool + """ + try: + return str.lower() in ('yes', 'true', 't', '1') + except AttributeError as err: + logger.error(err) + + +ANDROID_HOME = get_or_raise('ANDROID_HOME') +ANDROID_VERSION = get_or_raise('ANDROID_VERSION') +API_LEVEL = get_or_raise('API_LEVEL') +PROCESSOR = get_or_raise('PROCESSOR') +SYS_IMG = get_or_raise('SYS_IMG') + +logger.info('Android version: {version} \n' + 'API level: {level} \n' + 'Processor: {processor} \n' + 'System image: {img}'.format(version=ANDROID_VERSION, level=API_LEVEL, processor=PROCESSOR, + img=SYS_IMG)) + + +def prepare_avd(device, avd_name): + """ + Create and run android virtual device. + + :param device: Device name + :type device: str + :param avd_name: Name of android virtual device / emulator + :type avd_name: str + """ + cmd = 'echo no | android create avd -f -n {name} -t android-{api} -b {sys_img}'.format( + name=avd_name, api=API_LEVEL, sys_img=SYS_IMG) + + # Link emulator skins + skin_rsc_path = os.path.join(ROOT, 'devices', 'skins') + logger.info('Skin ressource path: {rsc}'.format(rsc=skin_rsc_path)) + skin_dst_path = os.path.join(ANDROID_HOME, 'platforms', 'android-{api}'.format(api=API_LEVEL), 'skins') + logger.info('Skin destination path: {dst}'.format(dst=skin_dst_path)) + for s in os.listdir(skin_rsc_path): + os.symlink(os.path.join(skin_rsc_path, s), os.path.join(skin_dst_path, s)) + + # Hardware and its skin + device_name_bash = device.replace(' ', '\ ') + skin_name = device.replace(' ', '_').lower() + logger.info('Device name in bash: {db}, Skin name: {skin}'.format(db=device_name_bash, skin=skin_name)) + + # For custom hardware profile + profile_dst_path = os.path.join(ROOT, '.android', 'devices.xml') + if 'samsung' in device.lower(): + # profile file name = skin name + profile_src_path = os.path.join(ROOT, 'devices', 'profiles', '{profile}.xml'.format(profile=skin_name)) + logger.info('Hardware profile resource path: {rsc}'.format(rsc=profile_src_path)) + logger.info('Hardware profile destination path: {dst}'.format(dst=profile_dst_path)) + os.symlink(profile_src_path, profile_dst_path) + + # Append command + cmd += ' -d {device} -s {skin}'.format(device=device_name_bash, skin=skin_name) + logger.info('AVD creation command: {cmd}'.format(cmd=cmd)) + titel = 'AVD creation process' + subprocess.check_call('xterm -T "{titel}" -n "{titel}" -e \"{cmd}\"'.format(titel=titel, cmd=cmd), shell=True) + + +def appium_run(avd_name): + """ + Run appium server. + + :param avd_name: Name of android virtual device / emulator + :type avd_name: str + """ + cmd = 'appium' + + grid_connect = str_to_bool(str(os.getenv('CONNECT_TO_GRID', False))) + logger.info('Connect to selenium grid? {connect}'.format(connect=grid_connect)) + if grid_connect: + 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)) + create_node_config(avd_name, appium_host, appium_port, selenium_host, selenium_port) + cmd += ' --nodeconfig {file}'.format(file=CONFIG_FILE) + except ValueError as v_err: + logger.error(v_err) + titel = 'Appium Server' + subprocess.check_call('xterm -T "{titel}" -n "{titel}" -e \"{cmd}\"'.format(titel=titel, cmd=cmd), shell=True) + + +def create_node_config(avd_name, 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 avd_name: Name of android virtual device / emulator + :type avd_name: 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': avd_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 + } + } + logger.info('Appium node config: {config}'.format(config=config)) + with open(CONFIG_FILE, 'w') as cf: + cf.write(json.dumps(config)) + + +def run(): + """Run app.""" + device = os.getenv('DEVICE', 'Nexus 5') + logger.info('Device: {device}'.format(device=device)) + + avd_name = '{device}_{version}'.format(device=device.replace(' ', '_').lower(), version=ANDROID_VERSION) + logger.info('AVD name: {avd}'.format(avd=avd_name)) + + prepare_avd(device, avd_name) + appium = str_to_bool(str(os.getenv('APPIUM', True))) + if appium: + appium_run(avd_name) + +if __name__ == '__main__': + run() diff --git a/src/appium.py b/src/appium.py deleted file mode 100644 index 1ee9bbe..0000000 --- a/src/appium.py +++ /dev/null @@ -1,84 +0,0 @@ -import json -import logging -import os -import subprocess - -logger = logging.getLogger('appium') - - -def run(connect_to_grid, avd_name, android_version): - """ - Run appium server. - - :param connect_to_grid: option to connect with selenium grid - :type connect_to_grid: bool - :param avd_name: name of device - :type avd_name: str - :param android_version: android version - :type android_version: str - """ - cmd = 'appium' - if connect_to_grid: - from src import CONFIG_FILE - 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)) - create_node_config(CONFIG_FILE, avd_name, android_version, appium_host, appium_port, - selenium_host, selenium_port) - cmd += ' --nodeconfig {file}'.format(file=CONFIG_FILE) - except ValueError as v_err: - logger.error(v_err) - titel = 'avd name: {name}'.format(name=avd_name) - subprocess.check_call('xterm -T "{titel}" -n "{titel}" -e \"{cmd}\"'.format(titel=titel, cmd=cmd), shell=True) - - -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 - } - } - logger.info('appium node config: {config}'.format(config=config)) - with open(config_file, 'w') as cf: - cf.write(json.dumps(config)) diff --git a/src/logging.conf b/src/logging.conf index f779ca1..018340a 100644 --- a/src/logging.conf +++ b/src/logging.conf @@ -1,5 +1,5 @@ [loggers] -keys=root, android, appium, service +keys=root, app [handlers] keys=console @@ -11,23 +11,11 @@ keys=formatter level=INFO handlers=console -[logger_android] +[logger_app] level=INFO handlers=console propagate=0 -qualname=android - -[logger_appium] -level=INFO -handlers=console -propagate=0 -qualname=appium - -[logger_service] -level=INFO -handlers=console -propagate=0 -qualname=service +qualname=app [handler_console] class=StreamHandler diff --git a/src/service.py b/src/service.py deleted file mode 100644 index b5f220d..0000000 --- a/src/service.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging -import os - -from src import android, appium, log - -logger = logging.getLogger('service') - - -def start(): - """ - Installation of needed sdk package, creation of android emulator and execution of appium server. - - """ - # Device name - device = os.getenv('DEVICE', 'Nexus 5') - - # Android version - android_version = os.getenv('ANDROID_VERSION', '5.0') - logger.info('Android version: {version}'.format(version=android_version)) - - # Emulator type - emu_type = os.getenv('EMULATOR_TYPE', android.TYPE_ARMEABI).lower() - emu_type = android.TYPE_ARMEABI if emu_type not in [android.TYPE_ARMEABI, android.TYPE_X86] else emu_type - logger.info('Emulator type: {type}'.format(type=emu_type)) - emu_file = 'emulator64-x86' if emu_type == android.TYPE_X86 else 'emulator64-arm' - logger.info('Emulator file: {file}'.format(file=emu_file)) - - # Selenium grid connection - connect_to_grid = str_to_bool(str(os.getenv('CONNECT_TO_GRID', False))) - logger.info('Connect to selenium grid? {input}'.format(input=connect_to_grid)) - - # Install android sdk package - api_level = android.get_api_level(android_version) - # Bug: cannot use skin for system image x86 with android version < 5.0 - if emu_type == android.TYPE_X86: - if int(api_level) < android.get_api_level('5.0'): - sys_img = android.TYPE_X86 - device = android.EMULATOR - else: - sys_img = android.TYPE_X86_64 - else: - sys_img = '{type}-v7a'.format(type=android.TYPE_ARMEABI) - logger.info('System image: {sys_img}'.format(sys_img=sys_img)) - android.install_package(emu_file, api_level, sys_img) - - # Create android virtual device - logger.info('Device: {device}'.format(device=device)) - avd_name = '{device}_{version}'.format(device=device.replace(' ', '_').lower(), version=android_version) - logger.info('AVD name: {avd}'.format(avd=avd_name)) - android.create_avd(device, avd_name, api_level) - - # Run appium server - appium.run(connect_to_grid, avd_name, android_version) - - -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__': - log.init() - start() diff --git a/src/tests/__init__.py b/src/tests/__init__.py index e69de29..a6e320c 100644 --- a/src/tests/__init__.py +++ b/src/tests/__init__.py @@ -0,0 +1,49 @@ +"""Unit test to test app.""" +import os +from unittest import TestCase + +import mock + +from src import app + + +class TestApp(TestCase): + """Unit test class to test other methods in the app.""" + + def test_valid_env(self): + key = 'ENV_1' + os.environ[key] = 'test' + app.get_or_raise(key) + del os.environ[key] + + def test_invalid_env(self): + with self.assertRaises(RuntimeError): + app.get_or_raise('ENV_2') + + def test_valid_bool(self): + self.assertEqual(app.str_to_bool('True'), True) + self.assertEqual(app.str_to_bool('t'), True) + self.assertEqual(app.str_to_bool('1'), True) + self.assertEqual(app.str_to_bool('YES'), True) + + def test_invalid_bool(self): + self.assertEqual(app.str_to_bool(''), False) + self.assertEqual(app.str_to_bool('test'), False) + + def test_invalid_format(self): + self.assertEqual(app.str_to_bool(True), None) + + @mock.patch('src.app.prepare_avd') + def test_run_with_appium(self, mocked_avd): + with mock.patch('src.app.appium_run') as mocked_appium: + app.run() + self.assertTrue(mocked_avd.called) + self.assertTrue(mocked_appium.called) + + @mock.patch('src.app.prepare_avd') + def test_run_withhout_appium(self, mocked_avd): + with mock.patch('src.app.appium_run') as mocked_appium: + os.environ['APPIUM'] = str(False) + app.run() + self.assertTrue(mocked_avd.called) + self.assertFalse(mocked_appium.called) diff --git a/src/tests/android/__init__.py b/src/tests/android/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/tests/android/test_api_level.py b/src/tests/android/test_api_level.py deleted file mode 100644 index 462af49..0000000 --- a/src/tests/android/test_api_level.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Unit test for android.py.""" -from unittest import TestCase - -from src import android - - -class TestApiLevel(TestCase): - """Unit test class to test method get_api_level.""" - - def setUp(self): - self.android_version = '4.2.2' - - def test_get_api_level(self): - api_level = android.get_api_level('4.2') - self.assertEqual(api_level, 19) - - def test_wrong_type(self): - api_level = android.get_api_level(4) - self.assertRaises(TypeError) - self.assertEqual(api_level, None) diff --git a/src/tests/android/test_install_package.py b/src/tests/android/test_install_package.py deleted file mode 100644 index 77d74c2..0000000 --- a/src/tests/android/test_install_package.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Unit test for android.py.""" -from unittest import TestCase - -import mock - -from src import android - - -class TestInstallPackage(TestCase): - """Unit test class to test method install_package.""" - - def setUp(self): - self.emulator_file = 'emulator64-arm' - self.api_level = 21 - self.sys_img = 'armeabi-v7a' - - @mock.patch('os.symlink') - @mock.patch('subprocess.check_call') - def test_package_installation(self, mocked_sys_link, mocked_suprocess): - self.assertFalse(mocked_sys_link.called) - self.assertFalse(mocked_suprocess.called) - android.install_package(self.emulator_file, self.api_level, self.sys_img) - self.assertTrue(mocked_sys_link.called) - self.assertTrue(mocked_suprocess.called) diff --git a/src/tests/appium/__init__.py b/src/tests/appium/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/tests/appium/test_node_config.py b/src/tests/appium/test_node_config.py deleted file mode 100644 index aa3a591..0000000 --- a/src/tests/appium/test_node_config.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Unit test for appium.py.""" -import os - -from unittest import TestCase - -from src import CONFIG_FILE, appium - - -class TestAppiumConfig(TestCase): - """Unit test class to test method create_node_config.""" - - def test_config_creation(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) diff --git a/src/tests/appium/test_run.py b/src/tests/appium/test_run.py deleted file mode 100644 index c92219a..0000000 --- a/src/tests/appium/test_run.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Unit test for appium.py.""" -import os -from unittest import TestCase - -import mock - -from src import appium - - -@mock.patch('subprocess.check_call') -class TestAppiumConfig(TestCase): - """Unit test class to test method run.""" - - def setUp(self): - self.emulator_name = 'test' - self.android_version = '4.2.2' - - def test_without_selenium_grid(self, mocked_subprocess): - with mock.patch('src.appium.create_node_config') as mocked_config: - self.assertFalse(mocked_config.called) - self.assertFalse(mocked_subprocess.called) - appium.run(False, self.emulator_name, self.android_version) - self.assertFalse(mocked_config.called) - self.assertTrue(mocked_subprocess.called) - - def test_with_selenium_grid(self, mocked_subprocess): - with mock.patch('src.appium.create_node_config') as mocked_config: - self.assertFalse(mocked_config.called) - self.assertFalse(mocked_subprocess.called) - appium.run(True, self.emulator_name, self.android_version) - self.assertTrue(mocked_config.called) - self.assertTrue(mocked_subprocess.called) - - def test_invalid_integer(self, mocked_subprocess): - os.environ['APPIUM_PORT'] = 'test' - with mock.patch('src.appium.create_node_config') as mocked_config: - self.assertFalse(mocked_config.called) - self.assertFalse(mocked_subprocess.called) - appium.run(True, self.emulator_name, self.android_version) - self.assertFalse(mocked_config.called) - self.assertTrue(mocked_subprocess.called) - self.assertRaises(ValueError) - - def tearDown(self): - if os.getenv('APPIUM_PORT'): - del os.environ['APPIUM_PORT'] diff --git a/src/tests/test_appium.py b/src/tests/test_appium.py new file mode 100644 index 0000000..ccc1910 --- /dev/null +++ b/src/tests/test_appium.py @@ -0,0 +1,54 @@ +"""Unit test to test appium service.""" +import os +from unittest import TestCase + +import mock + +from src import app + + +class TestAppium(TestCase): + """Unit test class to test appium methods.""" + + def setUp(self): + os.environ['CONNECT_TO_GRID'] = str(True) + self.avd_name = 'test_avd' + + @mock.patch('subprocess.check_call') + def test_without_selenium_grid(self, mocked_subprocess): + os.environ['CONNECT_TO_GRID'] = str(False) + self.assertFalse(mocked_subprocess.called) + app.appium_run(self.avd_name) + self.assertTrue(mocked_subprocess.called) + + @mock.patch('subprocess.check_call') + def test_with_selenium_grid(self, mocked_subprocess): + with mock.patch('src.app.create_node_config') as mocked_config: + self.assertFalse(mocked_config.called) + self.assertFalse(mocked_subprocess.called) + app.appium_run(self.avd_name) + self.assertTrue(mocked_config.called) + self.assertTrue(mocked_subprocess.called) + + @mock.patch('subprocess.check_call') + def test_invalid_integer(self, mocked_subprocess): + os.environ['APPIUM_PORT'] = 'test' + with mock.patch('src.app.create_node_config') as mocked_config: + self.assertFalse(mocked_config.called) + self.assertFalse(mocked_subprocess.called) + app.appium_run(self.avd_name) + self.assertFalse(mocked_config.called) + self.assertTrue(mocked_subprocess.called) + self.assertRaises(ValueError) + + def test_config_creation(self): + from src import CONFIG_FILE + self.assertFalse(os.path.exists(CONFIG_FILE)) + app.create_node_config('test', '127.0.0.1', 4723, '127.0.0.1', 4444) + self.assertTrue(os.path.exists(CONFIG_FILE)) + os.remove(CONFIG_FILE) + + def tearDown(self): + del os.environ['CONNECT_TO_GRID'] + if os.getenv('APPIUM_PORT'): + del os.environ['APPIUM_PORT'] diff --git a/src/tests/android/test_create_avd.py b/src/tests/test_avd.py similarity index 60% rename from src/tests/android/test_create_avd.py rename to src/tests/test_avd.py index bd2d380..800d291 100644 --- a/src/tests/android/test_create_avd.py +++ b/src/tests/test_avd.py @@ -1,9 +1,10 @@ -"""Unit test for android.py.""" +"""Unit test for android virtual device creation.py.""" +import os from unittest import TestCase import mock -from src import android +from src import app @mock.patch('subprocess.check_call') @@ -13,15 +14,14 @@ class TestAvd(TestCase): def setUp(self): self.avd_name = 'test_avd' - self.api_level = 21 - def test_nexus_avd(self, mocked_suprocess, mocked_sys_link): + def test_nexus_avd_as_default(self, mocked_suprocess, mocked_sys_link): with mock.patch('os.listdir') as mocked_list_dir: mocked_list_dir.return_value = ['file1', 'file2'] self.assertFalse(mocked_list_dir.called) self.assertFalse(mocked_sys_link.called) self.assertFalse(mocked_suprocess.called) - android.create_avd('Nexus 5', self.avd_name, self.api_level) + app.prepare_avd('Nexus 5', self.avd_name) self.assertTrue(mocked_list_dir.called) self.assertTrue(mocked_sys_link.called) self.assertTrue(mocked_suprocess.called) @@ -32,16 +32,11 @@ class TestAvd(TestCase): self.assertFalse(mocked_list_dir.called) self.assertFalse(mocked_sys_link.called) self.assertFalse(mocked_suprocess.called) - android.create_avd('Samsung Galaxy S6', self.avd_name, self.api_level) + app.prepare_avd('Samsung Galaxy S6', self.avd_name) self.assertTrue(mocked_list_dir.called) self.assertTrue(mocked_sys_link.called) self.assertTrue(mocked_suprocess.called) - def test_default_avd(self, mocked_suprocess, mocked_sys_link): - with mock.patch('os.listdir') as mocked_list_dir: - mocked_list_dir.return_value = ['file1', 'file2'] - self.assertFalse(mocked_list_dir.called) - self.assertFalse(mocked_sys_link.called) - self.assertFalse(mocked_suprocess.called) - android.create_avd('emulator', self.avd_name, self.api_level) - self.assertFalse(mocked_list_dir.called) + def tearDown(self): + if os.getenv('DEVICE'): + del os.environ['DEVICE'] diff --git a/supervisord.conf b/supervisord.conf index d0a6eed..975f672 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -5,26 +5,26 @@ childlogdir=%(ENV_LOG_PATH)s [program:xvfb] command=/usr/bin/Xvfb %(ENV_DISPLAY)s -screen %(ENV_SCREEN)s %(ENV_SCREEN_WIDTH)sx%(ENV_SCREEN_HEIGHT)sx%(ENV_SCREEN_DEPTH)s -stdout_logfile=%(ENV_LOG_PATH)s/xvfb.stdout.log -stderr_logfile=%(ENV_LOG_PATH)s/xvfb.stderr.log +stdout_logfile=%(ENV_LOG_PATH)s/xvfb.log +redirect_stderr=true [program:openbox] command=/usr/bin/openbox-session -stdout_logfile=%(ENV_LOG_PATH)s/openbox.stdout.log -stderr_logfile=%(ENV_LOG_PATH)s/openbox.stderr.log +stdout_logfile=%(ENV_LOG_PATH)s/openbox.log +redirect_stderr=true [program:x11vnc] command=/usr/bin/x11vnc -display %(ENV_DISPLAY)s -nopw -ncache 10 -forever -stdout_logfile=%(ENV_LOG_PATH)s/x11vnc.stdout.log -stderr_logfile=%(ENV_LOG_PATH)s/x11vnc.stderr.log +stdout_logfile=%(ENV_LOG_PATH)s/x11vnc.log +redirect_stderr=true [program:novnc] command=./noVNC/utils/launch.sh --vnc localhost:%(ENV_LOCAL_PORT)s --listen %(ENV_TARGET_PORT)s -stdout_logfile=%(ENV_LOG_PATH)s/novnc.stdout.log -stderr_logfile=%(ENV_LOG_PATH)s/novnc.stderr.log +stdout_logfile=%(ENV_LOG_PATH)s/novnc.log +redirect_stderr=true [program:docker-appium] -command=python -m src.service +command=python -m src.app autorestart=false -stdout_logfile=%(ENV_LOG_PATH)s/docker-appium.stdout.log -stderr_logfile=%(ENV_LOG_PATH)s/docker-appium.stderr.log +stdout_logfile=%(ENV_LOG_PATH)s/docker-appium.log +redirect_stderr=true