Постановка задач

В предыдущей статье наш коллега писал про Selenoid с Android-эмуляторами. Однако это решение было пробой пера и проверкой работоспособности Selenoid. Применение данного решения выявило несколько проблем:

  • Эмулятор — это не реальное устройство, возможен пропуск различных дефектов в нашем приложении. Есть много Android-устройств с различными версиями ОС, экранами, процессорами и другими характеристиками. Тестирование на реальных устройствах помогает убедиться, что приложение работает корректно на максимально возможном количестве конфигураций.

  • Загруженность процессора хоста значительно выше из-за необходимости эмуляции аппаратных и программных компонентов устройства. Это может привести к снижению общей производительности системы, особенно при запуске большого количества эмуляторов.

  • Не учитываются все особенности работы устройства, включая производительность, энергопотребление и особенности сетевого соединения.

Если кратко, лучше реальность, чем симуляция.

Однако не всё так гладко. У развёртывания Selenoid на реальном железе, есть некоторые проблемы:

  1. Сейчас нет универсального и готового к использованию решения для развёртывания сервера, которое бы автоматизировано обновляло конфигурацию Selenoid хаба в зависимости от подключения/отключения устройств Android в USB-порт. В большинстве случаев инженеры по тестированию вынуждены вручную настраивать сервер и конфигурации для каждого нового устройства, что требует значительных временных и трудовых затрат.

  2. После подключения/отключения новых устройств конфигурационные файлы необходимо обновлять вручную, что увеличивает риск ошибок и требует дополнительных ресурсов. Для автоматизации процесса подключения и конфигурирования новых устройств требуются кастомные скрипты, которые необходимо разрабатывать и поддерживать. Внедрение таких решений может значительно улучшить качество тестирования и сократить затраты на его проведение.

Решение трудностей — развернуть Selenoid без Докера на сервере для работы с реальными устройствами, а также обеспечить полную автоматизацию при создании конфигураций для подключаемых Android девайсов через USB-порт.

Об этой и других задачах развёртывания инфраструктуры для запуска тестов на реальных Android-устройствах на Linux и Mac расскажу в этой статье. А также продемонстрирую реализованные нами кастомные скрипты для генерации конфигурационных файлов для подключенных устройств.

Начнём с виртуализации.

Шаг 1. Виртуализация на сервере

В первую очередь необходимо включить виртуализацию на нашем Linux-сервере.

№1. Перезагружаем компьютер. При включении постоянно нажимаем кнопку подсказки, чтобы попасть в BIOS. Если подсказки нет, жмём «F9» или «F10».

№2. Выбираем «Security» — «System Security».

№3. Находим «Intel Virtual Technology». Выбираем «Enabled» с помощью клавиш «→», а затем жмём «F10».

№4. Вернёмся в меню «File» и выберем «Save Changes and Exit».

Шаг 2. Настройка окружения сервера

На следующем этапе перейдем к установке требуемого ПО для настройки сервера перед запуском автотестов.

№1. Создаём папку на сервере, где будут находится Selenoid и Selenoid UI, а также скрипты для автоматизированного создания конфигурационных файлов для Selenoid и Appium.

Пример:

mkdir selenoid

№2. Переходим в директорию, которая была создана на предыдущем шаге.

cd selenoid

№3. Переходим в репозиторий Selenoid. Копируем ссылку на бинарник и скачиваем бинарник на сервер (может отличаться от того, что указано в примере).

wget -o selenoid бинарник

№4. Переходим по ссылке в репозиторий Selenoid UI, копируем ссылку на бинарник. Также скачиваем бинарник на сервер.

wget -o selenoid-ui бинарник

№5. Устанавливаем nvm.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash export NVM_DIR="NVM_DIR/nvm.sh" ] && . "NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"  # This loadsnvm bash_completion

№6. Устанавливаем Node.js.

sudo apt-get install -y nodejs

№7. Устанавливаем npm.

sudo apt install -y npm

№8. Устанавливаем Appium.

npm install -g appium

№9. Устанавливаем драйвер uiautomator.

appium driver install uiautomator2

№10. Устанавливаем необходимую версию Java Azul. Подробные инструкции об установке.

№11. Установим Android SDK.

wget https://dl.google.com/android/repository/tools_r25.2.3-linux.zip unzip tools_r25.2.3-linux.zip -d sdk cd /sdk/tools ./android update sdk --no-ui

№12. Создаём файл с переменными средами в домашней директории пользователя.

touch $HOME/.bash_profile

№13. В текстовом редакторе nano прописываем следующие переменные:

№14.  Экспортируем настройки переменных энвайронмента из файла.

source .bash_profile

№15. Устанавливаем adb.

sudo apt-get install adb

№16. Инициализируем локальный репозиторий и связываем его с центральным.

git init
git remote add origin <Ссылка на ваш репозиторий> 
git pull origin master

Шаг 3. Листинг bash скриптов

После реализации и отладки скриптов в нашем репозитории получилась следующая структура:

healthCheck.sh

Этот скрипт предназначен для создания новой сессии Appium с использованием команды curl. Служит для проверки развернутого Selenoid.

  #!/bin/bash
   
   HOST=$1
   PORT=$2
   PLATFORM_NAME=$3
   DEVICE_NAME=$4
   
   if [ "$PLATFORM_NAME" = "android" ]; then
     APP_PATH=""
   else
     APP_PATH=""
   fi
   
   REQUEST_BODY=$(echo '{
   "capabilities": {
   "alwaysMatch": {
   "browserVersion": "deviceNameToReplace",
   "selenoid:options": {
   "name": "Session started using curl command...",
   "sessionTimeout": "1m"
   },
   "appium:deviceName": "platformNameToReplace",
   "appium:app": "appPathToReplace"
   }
   }
   }')
   
   REQUEST_BODY=${REQUEST_BODY/deviceNameToReplace/$DEVICE_NAME}
   REQUEST_BODY=${REQUEST_BODY/appPathToReplace/$APP_PATH}
   REQUEST_BODY=${REQUEST_BODY/platformNameToReplace/$PLATFORM_NAME}
   
   curl -H'Content-Type: application/json' http://$HOST:$PORT/wd/hub/session -d"$REQUEST_BODY"

Этот скрипт выполняет следующие действия:

  • Определяет уникальные идентификаторы для поиска скриптов.

  • Экспортирует переменные окружения, необходимые для работы Selenoid и Appium.

  • Ищет и определяет абсолютные пути к скриптам и конфигурациям.

  • Экспортирует дополнительные переменные окружения.

  • Выводит значения переменных в режиме отладки, если скрипт запускается с аргументом --debug .

Таким образом, скрипт настраивает окружение для работы с Selenoid и Appium, обеспечивая возможность поиска и использования необходимых скриптов и конфигураций.

Листинг:


  #!/bin/bash
   
   get_realpath_from_egrep() {
   search_pattern=$1
   search_dir=$2
   DIRS=$(egrep -r --include=*.sh --exclude-dir=$HOME/Library --exclude-dir=$HOME/.Trash "$search_pattern" "$search_dir")
   echo $DIRS | sed "s/:/\\n/" | head -n1 | xargs realpath
   }
   
   get_last_segment_from_pattern() {
   search_pattern=$1
   echo "$search_pattern" | sed "s/\//\\n/g" | tail -n1
   }
   
   FILE_NAME=".zshrc"
   
   if [ -f "$HOME/$FILE_NAME" ]; then
   #for mac os
   source $HOME/$FILE_NAME
   else
   
   FILE_NAME=".bash_profile"
   #for linux
   if [ -f "$HOME/$FILE_NAME" ]; then
   source $HOME/$FILE_NAME
   fi
   fi
   
   APPIUM_SCRIPT_FIND_BY="5cc2bd0d-96c5-4567-b82a-a896695af033"
   DEVICES_SCRIPT_FIND_BY="94843c3a-f128-4bb6-8819-4644156699d9"
   SELENOID_SCRIPT_FIND_BY="1b6f0b38-04e2-421c-b122-a54ab8a68bbd"
   SELENOID_CONFIG_SCRIPT_FIND_BY="660368f2-f0cc-49d8-bf2e-4a7d0f2c84d8"
   
   export SELENOID_PORT=4444
   export CRON_SETTINGS="*/15 * * * *"
   export SELENOID_UI_PORT=8080
   export SELENOID_CONFIG_NAME=devices.json
   
   SELENOID_LIMITS="-disable-docker -limit 20 -retry-count 1000"
   SELENOID_TIMEOUTS="-max-timeout 20m -session-attempt-timeout 15m -timeout 10m -service-startup-timeout 10m"
   
   SELENOID_PATH=$(get_realpath_from_egrep "$SELENOID_SCRIPT_FIND_BY" "$HOME")
   SELENOID_HOME=$(echo $SELENOID_PATH | xargs dirname)
   APPIUM_CONFIG_PATH=$(get_realpath_from_egrep "$APPIUM_SCRIPT_FIND_BY" "$SELENOID_HOME")
   SELENOID_CONFIG_PATH=$(get_realpath_from_egrep "$SELENOID_CONFIG_SCRIPT_FIND_BY" "$SELENOID_HOME")
   
   export SELENOID_HOME=$SELENOID_HOME
   export SELENOID_LOGS_DIR=$SELENOID_HOME/logs
   export COMMON_CONFIG_DIR=$(echo $SELENOID_CONFIG_PATH | xargs dirname)
   export APPIUM_CONFIG_DIR=$(echo $APPIUM_CONFIG_PATH | xargs dirname)
   export DEVICES_WATCHER_PATH=$(get_realpath_from_egrep "$DEVICES_SCRIPT_FIND_BY" "$SELENOID_HOME")
   export SELENOID_SCRIPT_NAME=$(get_last_segment_from_pattern "$SELENOID_PATH")
   export APPIUM_CONFIG_CREATER=$(get_last_segment_from_pattern "$APPIUM_CONFIG_PATH")
   export SELENOID_CONFIG_CREATER=$(get_last_segment_from_pattern "$SELENOID_CONFIG_PATH")
   
   CONFIG=$COMMON_CONFIG_DIR/$SELENOID_CONFIG_NAME
   
   export SELENOID_ARGS="$SELENOID_LIMITS -listen :$SELENOID_PORT -conf $CONFIG $SELENOID_TIMEOUTS -log-output-dir $SELENOID_LOGS_DIR"
   export SELENOID_UI_ARGS="-listen :$SELENOID_UI_PORT --selenoid-uri=http://localhost:$SELENOID_PORT"
   
   if [ "$1" = "--debug" ]; then
   
       echo $SELENOID_LOGS_DIR
       echo $COMMON_CONFIG_DIR
       echo $APPIUM_CONFIG_DIR
       echo $DEVICES_WATCHER_PATH
       echo $SELENOID_CONFIG_NAME
       echo $SELENOID_SCRIPT_NAME
       echo $APPIUM_CONFIG_CREATER
       echo $SELENOID_CONFIG_CREATER
   
   fi

Этот скрипт автоматизирует процесс мониторинга подключенных Android-устройств и перенастраивает Selenoid в случае изменений в списке устройств. Он загружает переменные окружения, проверяет текущий список устройств, сравнивает его с предыдущим состоянием и, при необходимости, перезапускает Selenoid, обновляя конфигурацию.

#!/bin/bash
#94843c3a-f128-4bb6-8819-4644156699d9 - don't delete
   
   DIR_TO_SCRIPT=$(realpath "$0" | xargs dirname)
   
   if [ "$#" != "1" ]; then
     echo "Необходимо передать путь до .env"
     exit 120
   fi
   
   if [ -f $1 ]; then
     cd $(dirname $1)
     source $1
   else
     exit 120
   fi
   
   DEVICES_FILE="devices"
   DEVICES_REGISTRY_PREV=""
   
   cd $DIR_TO_SCRIPT
   
   if [ -f $DEVICES_FILE ]; then
     DEVICES_REGISTRY_PREV=$(cat $DEVICES_FILE)
   else
     touch $DEVICES_FILE
   fi
   
   DEVICES=$(adb devices -l| grep -Eo "[a-zA-Z0-9-]{4,}\s{2,}" | xargs -n1 echo)
   DEVICES_COUNT=$(echo $DEVICES | wc -w)
   
   for ((DEVICE_INDEX=1; DEVICE_INDEX <= $((DEVICES_COUNT)); DEVICE_INDEX++))
   do
     DEVICE=$(echo $DEVICES | cut -d' ' -f$DEVICE_INDEX)
     DEVICES_TO_LINE=$DEVICE,$DEVICES_TO_LINE
   done
   
   DEVICES_TO_LINE=${DEVICES_TO_LINE:0:$((${#DEVICES_TO_LINE}-1))}
   IS_RECONFIGURE_SELENOID="false"
   
   if [ -z "$DEVICES_REGISTRY_PREV" ]; then
   
     IS_RECONFIGURE_SELENOID="true"
   
   else
   
     IFS=',' read -r -a DEVICE_UDIDS <<< "$DEVICES_REGISTRY_PREV"
   
     #previously registry device was disconnected
     for UDID in "${DEVICE_UDIDS[@]}"
     do
       DEVICE_LINE=$(echo $DEVICES | grep $UDID)
   
       if [ -z "$DEVICE_LINE" ]; then
          IS_RECONFIGURE_SELENOID="true"
       fi
     done
   
     #registry new device was connected
     for ((DEVICE_INDEX=1; DEVICE_INDEX <= $((DEVICES_COUNT)); DEVICE_INDEX++))
     do
        DEVICE=$(echo $DEVICES | cut -d' ' -f$DEVICE_INDEX)
        DEVICE_LINE=$(echo $DEVICES_REGISTRY_PREV | grep $DEVICE)
   
        if [ -z "$DEVICE_LINE" ]; then
           IS_RECONFIGURE_SELENOID="true"
        fi
     done
   fi
   
   if [ "$IS_RECONFIGURE_SELENOID" = "true" ]; then
   
     echo "Reconfigure selenoid"
     $SELENOID_HOME/"$SELENOID_SCRIPT_NAME" reconfigure &
   
     DEVICES_TO_LINE=""
   
     for ((DEVICE_INDEX=1; DEVICE_INDEX <= $((DEVICES_COUNT)); DEVICE_INDEX++))
     do
        DEVICE=$(echo $DEVICES | cut -d' ' -f$DEVICE_INDEX)
        APPIUM_CONFIG=$(cat $APPIUM_CONFIG_DIR/$DEVICE.json 2> /dev/null | grep $DEVICE)
        SELENOID_CONFIG=$(cat $COMMON_CONFIG_DIR/$SELENOID_CONFIG_NAME 2> /dev/null | grep $DEVICE)
   
        if [ -z "$APPIUM_CONFIG" ]; then
           continue
        fi
   
        if [ -z "$SELENOID_CONFIG" ]; then
           continue
        fi
   
        DEVICES_TO_LINE=$DEVICE,$DEVICES_TO_LINE
     done
   
     DEVICES_TO_LINE=${DEVICES_TO_LINE:0:$((${#DEVICES_TO_LINE}-1))}
     echo -n "$DEVICES_TO_LINE" > $DEVICES_FILE
   fi
   
   exit 0

Отслеживание новой версии Selenoid и Selenoid UI. Загрузка и обновление бинарников в случае появления новой версии.

#!/bin/bash
   
   function getLatestVersion() {
   
       echo $(curl -s $1 | grep "/aerokube/$2/releases/tag" | grep -Eo "[0-9]{1,}[.][0-9]{1,}[.][0-9]{1,}" | head -n1)
   }
   
   function getCurrentVersion() {
   
       echo $($SELENOID_HOME/$1 --version 2> /dev/null | grep -Eo "[0-9]{1,}[.][0-9]{1,}[.][0-9]{1,}" || echo $BAD_CODE)
   }
   
   function downloadBinary() {
   
       echo "Download $3"
   
       DOWNLOAD_URL=$1
       DOWNLOAD_URL=${DOWNLOAD_URL/"{os}"/$OS}
       DOWNLOAD_URL=${DOWNLOAD_URL/"{osPlatform}"/$OS_PLATFORM}
       DOWNLOAD_URL=${DOWNLOAD_URL/"{latest_version}"/$2}
   
       cd "$SELENOID_HOME" || exit $BAD_CODE
       rm -f $3
       curl -s -L -o $SELENOID_HOME/$3 $DOWNLOAD_URL || exit $BAD_CODE
       chmod 766 $3
       cd "$CURRENT_DIR_PATH" || exit $BAD_CODE
   }
   
   if [ "$#" != "1" ]; then
     echo "Необходимо передать путь до .env"
     exit 120
   fi
   
   CURRENT_DIR_PATH=$(realpath "$0" | xargs dirname)
   PATH_TO_ENV=$(realpath "$1")
   
   if [ -f $PATH_TO_ENV ]; then
     DIR=$(dirname PATH_TO_ENV)
     cd $DIR || exit $BAD_CODE
     source $PATH_TO_ENV
   else
     echo "Неверно передан путь до .env"
     exit 120
   fi
   
   OS=$(uname | tr A-Z a-z)
   OS_PLATFORM=$(uname -m)
   BAD_CODE="126"
   SELENOID_URL="https://github.com/aerokube/selenoid/releases"
   SELENOID_UI_URL="https://github.com/aerokube/selenoid-ui/releases"
   
   SELENOID_DOWNLOAD_URL="https://github.com/aerokube/selenoid/releases/download/{latest_version}/selenoid_{os}_{osPlatform}"
   SELENOID_UI_DOWNLOAD_URL="https://github.com/aerokube/selenoid-ui/releases/download/{latest_version}/selenoid-ui_{os}_{osPlatform}"
   
   SELENOID_LATEST_VERSION=$(getLatestVersion $SELENOID_URL "selenoid")
   SELENOID_UI_LATEST_VERSION=$(getLatestVersion $SELENOID_UI_URL "selenoid-ui")
   CURRENT_SELENOID_VERSION=$(getCurrentVersion "selenoid")
   CURRENT_SELENOID_UI_VERSION=$(getCurrentVersion "selenoid-ui")
   
   if [ "$CURRENT_SELENOID_VERSION" = "$BAD_CODE" ]; then
       CURRENT_SELENOID_VERSION="0.0.0"
   fi
   
   if [ "$CURRENT_SELENOID_UI_VERSION" = "$BAD_CODE" ]; then
       CURRENT_SELENOID_UI_VERSION="0.0.0"
   fi
   
   OS_PLATFORM=${OS_PLATFORM/"x86_64"/"amd64"}
   IS_START_SELENOID="false"
   
   if [[ "$SELENOID_LATEST_VERSION" != *"$CURRENT_SELENOID_VERSION"* ]]; then
     downloadBinary $SELENOID_DOWNLOAD_URL $SELENOID_LATEST_VERSION "selenoid"
     IS_START_SELENOID="true"
   else
     echo "Selenoid version latest: $SELENOID_LATEST_VERSION"
   fi
   
   if [[ "$SELENOID_UI_LATEST_VERSION" != *"$CURRENT_SELENOID_UI_VERSION"* ]]; then
     downloadBinary $SELENOID_UI_DOWNLOAD_URL $SELENOID_UI_LATEST_VERSION "selenoid-ui"
     IS_START_SELENOID="true"
   else
     echo "Selenoid UI version latest: $SELENOID_UI_LATEST_VERSION"
   fi
   
   if [ "$IS_START_SELENOID" = "true" ]; then
     echo "Start selenoid and selenoid ui"
     eval "$SELENOID_HOME/$SELENOID_SCRIPT_NAME restart"
   fi
   
   exit 0

Этот скрипт автоматически генерирует конфигурационные файлы для Appium на основе подключённых Android-устройств. Он извлекает список UDID устройств с помощью adb, создаёт для каждого устройства конфигурационный файл с соответствующими параметрами и сохраняет его в формате JSON.

#!/bin/bash
#5cc2bd0d-96c5-4567-b82a-a896695af033 - don't delete
   
   IFS=" "
   IDS=$(adb devices -l | grep -v 'List of devices attached' | grep -Eo '[0-9a-zA-Z-]{8,}\s')
   IDS=$(echo "${IDS}" | tr -d "\r\n")
   
   echo "Удаляем конфигурационные файлы:"
   ls *.json
   rm -f *.json
   
   read -ra UDIDS <<< "$IDS"
   for UDID in "${UDIDS[@]}"
   do
       FILE_NAME=$UDID".json"
       FILE_CONTENT=$(echo '{
       "server": {
       "address": "127.0.0.1",
       "allow-cors": true,
       "allow-insecure": [
           "get_server_logs",
           "adb_shell"
       ],
       "base-path": "/wd/hub",
       "debug-log-spacing": true,
       "default-capabilities": {
           "platformName": "android",
           "appium:androidNaturalOrientation": true,
           "appium:deviceName": "android",
           "appium:udid": "udidToReplace",
           "appium:automationName": "UiAutomator2",
           "appium:enforceAppInstall": true,
           "appium:newCommandTimeout": 90,
           "appium:autoGrantPermissions": false,
           "appium:noReset": noResetToReplace,
           "appium:ignoreHiddenApiPolicyError": true,
           "appium:appActivity": "ru.alfabank.mobile.android.splash.presentation.activity.SplashActivity",
           "appium:appPackage": "ru.alfabank.mobile.android.feature"
       },
       "log-level": "debug",
       "log-no-colors": true
       }
     }')
   
     XIAOMI_LINE=$(adb -s $UDID shell getprop ro.vendor.build.fingerprint | sed 's/\//\n/g' | head -n1 | grep "Xiaomi")
   
       if [ -z "$XIAOMI_LINE" ]; then
         NO_RESET_ENABLE="false";
       else
         NO_RESET_ENABLE="true";
       fi
   
       FILE_CONTENT=${FILE_CONTENT/udidToReplace/$UDID}
       FILE_CONTENT=${FILE_CONTENT/noResetToReplace/$NO_RESET_ENABLE}
   
       echo "создаем конфигурационный файл "${FILE_NAME}
       touch $FILE_NAME
       echo "настройки конфигурационного файла:"
       FILE_CONTENT_PRETTY=$(echo $FILE_CONTENT | json_reformat 2> /dev/null)
   
       if [ "$?" = 0 ]; then
         echo $FILE_CONTENT_PRETTY > $FILE_NAME
       else
         echo $FILE_CONTENT > $FILE_NAME
       fi
   
       cat $FILE_NAME
   done
   
   exit 0
createSelenoidConfig.sh

Этот скрипт на языке Bash предназначен для создания конфигурационного файла Selenoid на основе подключенных Android-устройств.

Листинг:

#!/bin/bash
#660368f2-f0cc-49d8-bf2e-4a7d0f2c84d8 - don't delete
   
   if [ "$#" != "1" ]; then
     echo "Необходимо передать путь до .env"
     exit 120
   fi
   
   if [ -f $1 ]; then
     cd $(dirname $1)
     source $1
     cd $COMMON_CONFIG_DIR
   else
     exit 120
   fi
   
   FILE_CONTENT=$(echo '{
     "android": {
       "default": "defaultToReplace",
       "versions": {
         versionsToReplace
       }
     }
   }')
   VERSION=$(echo '
   "deviceNameToReplace": {
     "image": ["pathToAppium", "--config", "configNameToReplace"]
   }')
   VERSIONS=""
   DEVICES_COUNT=$(adb devices -l | wc -l)
   DEVICES_DEFAULT=""
   APPIUM_PATH=$(echo $APPIUM_HOME)appium
   
   for ((DEVICE_INDEX=2; DEVICE_INDEX <= $((DEVICES_COUNT-1)); DEVICE_INDEX++))
   do
   
     ADB_LINE=$(adb devices -l | sed $DEVICE_INDEX'!D')
     USB=$(echo $ADB_LINE | grep -Eo "usb:[0-9-]{1,}" | grep -Eo "[0-9-]{1,}" )
     UDID=$(echo $ADB_LINE | grep -Eo "[0-9A-Za-z-]+\s" | head -n1 | xargs -n1 echo | head -n1)
   
     if [ -z "$USB" ]; then
         #for emulators
         MODEL=$(adb -s $UDID shell getprop ro.boot.qemu.avd_name | sed 's/.*/&/')
         DEVICE=$(adb -s $UDID shell getprop ro.product.vendor.manufacturer | sed 's/.*/&/')
     else
         #for real devices
         MODEL=$(adb -s $UDID shell getprop ro.product.model | sed 's/.*/\u&/')
         DEVICE=$(adb -s $UDID shell getprop ro.vendor.build.fingerprint | sed 's/\//\n/g' | head -n1 | sed 's/.*/\u&/')
     fi
   
     DEVICES_DEFAULT="${DEVICE//_/ } ${MODEL//_/ }"
     PATH_TO_APPIUM_CONFIG_FILE="$APPIUM_CONFIG_DIR/$UDID.json"
     VER=$(echo $VERSION)
     VER=${VER/pathToAppium/$APPIUM_PATH}
     VER=${VER/deviceNameToReplace/$DEVICES_DEFAULT}
     VER=${VER/configNameToReplace/$PATH_TO_APPIUM_CONFIG_FILE}
     VERSIONS=$(echo -e "$VER,$VERSIONS")
   done
   
   LENGTH=$(echo ${#VERSIONS})
   LENGTH=$((LENGTH-1))
   
   if [ "$LENGTH" != "-1" ]; then
   
     echo "Удаляем файл конфигурации: "$SELENOID_CONFIG_NAME
     rm -f $SELENOID_CONFIG_NAME
   
     VERSIONS=${VERSIONS:0:$LENGTH}
     FILE_CONTENT=${FILE_CONTENT/versionsToReplace/$VERSIONS}
     FILE_CONTENT=${FILE_CONTENT/defaultToReplace/$DEVICES_DEFAULT}
     FILE_CONTENT_PRETTY=$(echo $FILE_CONTENT | json_reformat 2> /dev/null)
   
     EXIT_CODE=$?
     echo "Создаем файл конфигурации: "$SELENOID_CONFIG_NAME
   
     if [ "$EXIT_CODE" = 0 ]; then
       echo $FILE_CONTENT_PRETTY > $SELENOID_CONFIG_NAME
     else
       echo $FILE_CONTENT > $SELENOID_CONFIG_NAME
     fi
   
     echo "Настройки конфигурационного файла:"
     cat $SELENOID_CONFIG_NAME
   fi
   exit 0
selenoid.sh

Скрипт верхней оркестрации, управляющий скриптами, описанными выше. Позволяет запуска, останавливать, перенастраивать Selenoid с генерацией необходимых конфигурационных файлов.

#!/bin/bash
#1b6f0b38-04e2-421c-b122-a54ab8a68bbd
   
   function get_pids(){
   
     PIDS=$(ps -ax | grep "$1" | grep -v grep | grep -v ggr | grep -Eo "^\s{0,6}[0-9]+\s" | grep -Eo "[0-9]+" | grep -v "$$")
   }
   
   function kill_procs(){
   
     if [ -n "$1" ]; then
        echo "$1" | xargs kill $2 > /dev/null 2> /dev/null
     fi
   }
   
   function kill_appium_procs(){
   
      get_pids "appium"
      kill_procs "$PIDS" -9
   }
   
   function kill_selenoid_procs(){
   
      get_pids "./selenoid $SELENOID_ARGS"
      kill_procs "$PIDS" -9
   }
   
   function kill_selenoid_ui_procs(){
   
      get_pids "./selenoid-ui $SELENOID_UI_ARGS"
      kill_procs "$PIDS" -9
   }
   
   function terminate() {
   
     kill_appium_procs
     kill_procs "$SELENOID_PID" -TERM
     kill_procs "$SELENOID_UI_PID" -TERM
   }
   
   function start_selenoid(){
   
     cd $SELENOID_HOME
     chmod 766 selenoid
   
     if [ "$VERBOSE" = "true" ]; then
       clear_crontab
       echo "Запускаем selenoid:"
       trap terminate SIGINT SIGTERM
       ./selenoid $SELENOID_ARGS
       SELENOID_PID=$!
       wait
     else
       SELENOID_LOG="$SELENOID_LOGS_DIR/selenoid-output-$DATE_TIME.log"
       touch $SELENOID_LOG
       ./selenoid $SELENOID_ARGS > $SELENOID_LOG 2> $SELENOID_LOG &
     fi
   }
   
   function start_selenoid_ui(){
   
     cd $SELENOID_HOME
     SELENOID_UI_LOG="$SELENOID_LOGS_DIR/selenoid-ui-output-$DATE_TIME.log"
     touch $SELENOID_UI_LOG
     chmod 766 selenoid-ui
     $(sleep 5; ./selenoid-ui $SELENOID_UI_ARGS > $SELENOID_UI_LOG 2> $SELENOID_UI_LOG)&
   }
   
   function createConfigs() {
   
       cd $APPIUM_CONFIG_DIR
       chmod 766 $APPIUM_CONFIG_CREATER
       ./$APPIUM_CONFIG_CREATER
       cd $COMMON_CONFIG_DIR
       chmod 766 $SELENOID_CONFIG_CREATER
       ./$SELENOID_CONFIG_CREATER $SELENOID_HOME/.env
   }
   
   function start() {
   
     createConfigs
     start_selenoid_ui
     start_selenoid
   }
   
   function full_kill_procs(){
     kill_appium_procs
     kill_selenoid_ui_procs
     kill_selenoid_procs
   }
   
   function help() {
     echo "Ожидается два аргумента:"
     echo "1) Обязательный команды: help|stop|start|restart"
     echo "2) Необязательный флаг: --debug"
   }
   
   function clear_crontab(){
     echo "Удаляем запись из crontab:"
     crontab -l
     crontab -r 2> /dev/null
   }
   
   function add_to_crontab(){
   
     CRONTAB_CMD="$CRON_SETTINGS $DEVICES_WATCHER_PATH"
     CRONTAB_LINE=$(crontab -l 2> /dev/null)
   
     if [[ "$CRONTAB_LINE" == *"$CRONTAB_CMD"* ]]; then
       echo "Уже был настроен crontab:"
       crontab -l
       exit 0
     fi
   
     if [[ "$CRONTAB_LINE" == *"$DEVICES_WATCHER_PATH"* ]]; then
       clear_crontab
     fi
   
     chmod 766 $DEVICES_WATCHER_PATH
     echo "Добавляем запись в crontab:"
     TEMP_FILE="usbd"
     echo "$CRON_SETTINGS $DEVICES_WATCHER_PATH $SELENOID_HOME/.env > $SELENOID_LOGS_DIR/cron.log" | tee $TEMP_FILE
     crontab $TEMP_FILE
     rm -f $TEMP_FILE
   }
   
   cd $(realpath "$0" | xargs dirname)
   source .env
   
   DATE_TIME=$(date +%Y_%m_%d_%H_%M_%S)
   HELP_COMMAND=$(echo $@ | grep help)
   
   if [ -z "$HELP_COMMAND" ]; then
   
     IS_ARG_USE=false
     DEBUG_MODE=$(echo $@ | grep debug)
   
     if [ -z "$DEBUG_MODE" ]; then
       export VERBOSE=false
     else
       export VERBOSE=true
     fi
   
     mkdir -p $SELENOID_LOGS_DIR
   
     for flag in "$@"
     do
       case "${flag}" in
   
         stop)
   
           full_kill_procs
           rm -f $APPIUM_CONFIG_DIR/*.json
           rm -f $COMMON_CONFIG_DIR/$SELENOID_CONFIG_NAME
           clear_crontab
   
           exit 0
         ;;
   
         start)
   
           start
           IS_ARG_USE=true
         ;;
   
         restart)
   
           full_kill_procs
           start
           IS_ARG_USE=true
         ;;
   
         reconfigure)
   
           createConfigs
           get_pids "./selenoid $SELENOID_ARGS"
           kill -HUP $PIDS
   
           exit 0
         ;;
       esac
     done
   
     if [ "$VERBOSE" = "false" ]; then
   
       if [ "$IS_ARG_USE" = "false" ]; then
         help
         exit 120;
       fi
   
       sleep 6
   
       IS_SELENOID_STARTED=$(ps -x | grep " :$SELENOID_PORT")
   
       if [ -z "$IS_SELENOID_STARTED" ]; then
         echo "Selenoid не был запущен:"
         cat $SELENOID_LOG
         exit 120
       fi
   
       IS_SELENOID_UI_STARTED=$(ps -x | grep " :$SELENOID_UI_PORT")
   
       if [ -z "$IS_SELENOID_UI_STARTED" ]; then
         echo "Selenoid UI не был запущен:"
         cat $SELENOID_UI_LOG
         exit 120
       fi
   
       add_to_crontab
     fi
   else
     help
   fi
   
   exit 0

Шаг 4. Запуск

Наконец, мы приблизились непосредственно к запуску Selenoid и тестированию жизнеспособности нашего окружения.

№1. Запускаем скрипт выполняющий загрузку и обновление бинарников в случае появления новой версии ./updateSelenoidVersion.sh.

./selenoid.sh start --debug

№2. В другом окне терминала запускаем наш health_check  ./check/health_check.sh 127.0.0.1 4444 <имя устройства из команды adb devices>.

Наблюдаем следующие логи:

2024/06/24 22:21:34 [-] [INIT] [Loading configuration files...]
2024/06/24 22:21:34 [-] [INIT] [Loaded configuration from config/browsers.json] 
2024/06/24 22:21:34 [-] [INIT] [Logs Dir: /home/am_user2/selenoid/logs] 
2024/06/24 22:21:34 [-] [INIT] [Timezone: Local] 
2024/06/24 22:21:34 [-] [INIT] [Listening on :4444] 
2024/06/24 22:22:18 [-] [NEW_REQUEST] [unknown] [127.0.0.1] 
2024/06/24 22:22:18 [-] [NEW_REQUEST_ACCEPTED] [unknown] [127.0.0.1] 
2024/06/24 22:22:18 [7] [LOCATING_SERVICE] [android] [Google Pixel 5] 
2024/06/24 22:22:18 [7] [USING_DRIVER] [android] [Google Pixel 5] 
2024/06/24 22:22:18 [7] [ALLOCATING_PORT] 
2024/06/24 22:22:18 [7] [ALLOCATED_PORT] [35659] 
2024/06/24 22:22:18 [7] [STARTING_PROCESS] [[appium --config /home/am_user2/selenoid/config/appium/08221FDD4006R1.json --port=35659]] 
2024/06/24 22:22:20 [7] [PROCESS_STARTED] [258659] [1.69s] 
2024/06/24 22:22:20 [7] [PROXY_TO] [http://127.0.0.1:35659] 
2024/06/24 22:22:20 [7] [SESSION_ATTEMPTED] [http://127.0.0.1:35659] [1] 
2024/06/24 22:22:20 [7] [SESSION_ATTEMPTED] [http://127.0.0.1:35659/wd/hub] [2] 
2024/06/24 22:22:31 [7] [SESSION_CREATED] [8bf1b30e-1b37-423b-9271-5423c652ba18] [2] [12.41s]

Строка говорит о том, что сессия создана успешно:

2024/06/24 22:22:31 [7] [SESSION_CREATED] [8bf1b30e-1b37-423b-9271-5423c652ba18] [2] [12.41s]

Нюансы

Во время подготовки окружения столкнулись со следующими особенностями — с ошибками при попытке запуске на Xiaomi Mi Mix. Для китайских девайсов могут встречаться проблемы с доступом к управлению приложениями через Appium.

04-19 21:09:59.581   927   927 E libc    : Access denied finding property "ro.hardware.fp.fod"
04-19 21:09:59.581   927   927 E libc    : Access denied finding property "ro.hardware.fp.sideCap"
04-19 21:09:59.573   927   927 W surfaceflinger: type=1400 audit(0.0:3966956): avc: denied { read } for name="u:object_r:vendor_fp_prop:s0" dev="tmpfs" ino=22608 scontext=u:r:surfaceflinger:s0 tcontext=u:object_r:vendor_fp_prop:s0 tclass=file permissive=0
04-19 21:09:59.588  1716 11879 I Timeline: Timeline: App_transition_ready time:704191047 
04-19 21:09:59.588  1716 11879 I Timeline: Timeline: App_transition_stopped time:704191048 
04-19 21:09:59.588  5313  5313 D EventBus: [5313, u0] send(AppTransitionFinishedEvent) 
04-19 21:09:59.588  5313  5313 D EventBus: [5313, u0]  -> ForcedResizableInfoActivityController [0x17a1178, P1] onBusEvent(AppTransitionFinishedEvent)
04-19 21:09:59.588  5313  5313 D EventBus: [5313, u0] onBusEvent(AppTransitionFinishedEvent) duration: 8 microseconds, avg: 20 
04-19 21:09:59.591 24287 24287 D Launcher.Lifecycle: onResume:UserHandle{0},67eca74,true 
04-19 21:09:59.591 24287 24287 D ScreenElementRoot: resume 
04-19 21:09:59.591  1716  2230 E Pm      : install msg : Failure [INSTALL_CANCELED_BY_USER

Для решения проблемы в настройках Xiaomi необходимо включить режим разработчика и разрешить отладку по USB. Однако даже после включения этих настроек, устройства могут периодически отключать отладку или требовать повторного подтверждения.

Оптимизация USB: MIUI может включать функции оптимизации USB, которые могут прерывать соединение ADB или ограничивать доступ к устройству.

Также необходимо в конфигах Appium прописать следующие capabilities:

autoGrantPermission=false, noReset=true

Выводы

Эмуляторы подходят для масштабируемого тестирования, когда требуется запускать большое количество тестов параллельно, но при этом увеличивается нагрузка на процессор хоста и может снижаться точность тестов.

А реальные устройства подходят для тестирования с минимальной нагрузкой на процессор хоста и для получения более точных результатов, соответствующих реальным условиям использования.

Выбор между реальными устройствами и эмуляторами зависит от конкретных целей тестирования, доступных ресурсов и необходимой точности результатов. В идеале, лучше использовать комбинированный подход, чтобы воспользоваться преимуществами обоих методов.


Статья написана:
ДАРРП, клиентский путь «Платежи и переводы»:
@wanro,@ILeonteva — подготовка и отладка окружения для тестирования на Android устройствах, написание скриптов и отладка Selenoid.
ДАРРП, клиентский путь «Самозанятые:
@pbezpal— установка и настройка операционной системы.

Комментарии (0)