У нас был небольшой бюджет и большие проблемы с рутинным тестированием в match3-игре, у которой накопилось более 1500 уровней. А вот чего у нас не было, так это идеально подходящего коробочного решения, работающего на лету и без пересборок. Поэтому мы нагородили собственную ферму с высаженной грядкой из десятка Xiaomi, отправкой статистики, отчетами в Slack, блекджеком и коровой.

Я Павел Щеваев, CTO студии BIT.GAMES, которая является частью международного игрового бренда MY.GAMES. Вы можете знать нас по RPG «Гильдия Героев», а ваши мамы — по «Домовятам» в Одноклассниках. Да, это были мы. :) Но сегодня речь пойдет о нашем новом проекте Storyngton Hall. Это головоломка «три в ряд» с сюжетом, по которому красивые леди разгадывают загадки, декорируют комнаты, примеряют платья, устраивают балы, и, в конце концов, выходят замуж.


Игра реализована на С# и Unity, доступна на iOS и Android. На сегодняшний день у неё 4 млн установок. Какие проблемы назрели?

Большие объемы. В игре более 1500 уровней, каждые две недели добавляется еще несколько десятков. И вот на 934 уровне ломается туториал.


Ребятам из QA практически нереально оперативно добраться до 934 уровня, до того как приложение окажется у игроков.

Бюджеты памяти. Не единожды происходило неконтролируемое потребление памяти, которое мы обнаруживали уже в консоли Google Play. Вот, например, график падений после того, как художник запушил несжатую текстуру; игра перестала помещаться в бюджет и начала чаще падать.


Что мы придумали


Нам захотелось получить автоматизированное решение, которое помогало бы ловить проблему ещё до того, как она ушла в билд. Идея была такова:

  • берем весь наш массив уровней;
  • делим их на батчи с наборами уровней, поделенные по количеству устройств;
  • прогоняем уровни на реальных устройствах.


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

Готовые решения на рынке


Изобретать что-то самим не особо хотелось, и мы начали смотреть, что предлагает рынок.


Требования были такие:

  • Надёжность: ферма должна чётко и стабильно выполнять тесты каждую ночь.
  • Простота: ферма должна использовать инструментарий, который нами изучен и понятен.
  • Масштабируемость: ферма должна автоматически определять новые устройства и распределять по ним уровни.
  • Скриптуемость: тесты должны быть написаны на скриптовом языке, чтобы была возможность запускать их без пересборки приложения, т.к. сборка приложения занимает много времени.

К сожалению, ни Device Farmer (бывший STF), ни Selenium, ни прочие варианты не подошли. Пришлось городить ферму самим.

Android Test Farm (ATF)


Ферма представляет собой хаб из десятка устройств, подключенного к Mac Mini. В этом видео я инициирую сессию тестирования со своей машины — загружаются тестовые скрипты, затем запускаю приложение:


Скрипты полностью эмулируют сессию игрока: открывают окошки, проходят туториалы, играют в уровни и проч. Все тапы и свайпы эмулируются в ускоренном режиме.

Что по железкам?



Аппаратный состав фермы:

  • старенький Mac Mini;
  • 10-портовый USB-хаб, найденный на Ozon (есть и на 30);
  • бесперебойник;
  • жаропрочный короб;
  • 10 одинаковых смартфонов Xiaomi 9A.

Почему 10?

Сначала смартфонов было 4, но оказалось, что для текущего объёма тестирования оптимально использовать 10. Если потребуется ускорить выполнение тестов, то добавим ещё.

Почему одинаковые?

Чтобы точно ориентироваться в просадках по памяти, нам нужны одинаковые устройства. Если смартфоны будут разные, то показатели потребления памяти будут сильно разниться из-за различий железа. Однажды попробовав использовать 5 разных устройств для этих целей, мы осознали бесполезность затеи.

Почему Xiaomi?

Мы старались выбирать такие устройства, которые соответствуют среднему уровню смартфонов игроков: не супертоповые, но и не самые дешёвые. И если игра работает на них хорошо, то этот опыт можно экстраполировать на схожие и более дорогие устройства. Практика показала, что Xiaomi 9A — это относительно надежное устройство из средней ценовой категории, которое хорошо работает через ADB и подходит нам в полной мере.

Делать iOS-ферму не планировали изначально, потому что Apple не предоставляет открытых средств для низкоуровневого доступа к устройствам. К тому же игра написана на Unity и в ней минимум платформозависимого кода, поэтому всю массу уровней можно спокойно тестировать на Android-устройствах. Кроме того, эти десять смартфонов Xiaomi укладываются в стоимость одного приличного iPhone. А если нет разницы, зачем платить больше?

Стек технологий


Здесь мы довольно консервативны и используем то, что проверено временем: PHP, BHL, ADB, SSH, Slack, ClickHouse, Redash.


Из всего перечисленного вам точно не знаком BHL — это интерпретируемый, скриптуемый, строго типизируемый язык программирования, наша собственная разработка. В нём есть встроенные примитивы для удобного псевдораспараллеливания кода, поддерживается hot reload на лету. Благодаря этому мы можем компилировать скрипт в бинарный код, отправить на устройство и запустить без пересборки приложения.

Как устроены наши тестовые скрипты


Рассмотрим простейший пример.


Функция TestWaitUI является корутиной и выполняется в неблокирующем режиме на протяжении нескольких кадров. И пока она выполняется, не блокируется основной геймлей. В этом коде ожидается, что появится UI окно — UILogin. Как только оно появлятся, бот нажимает кнопку «close_btn», используя функцию TestTapUIButton, таким образом пытаясь его закрыть.

Еще пример реального кода с фермы:


Самое интересное происходит внутри конструкции рaral. Всё, что находится в ней — а именно несколько секций forever, — выполняется параллельно друг другу. Тестовый скрипт ожидает появления разных UI, и в зависимости от их типа выполняет те или иные действия. В основном всё сводится к тому, что мы ждём появления какой-нибудь кнопки вроде continue/close и нажимаем её. В свою очередь секция paral выполняется в цикле, пока не появится UI главного окна «UIMainScreen».

Полный цикл тестирования


Предположим у нас есть вышеупомянутый тестовый сценарий:


Давайте загрузим его на устройства через инструмент ATF, который предоставляет API для работы с фермой:


Инструмент ATF через SSH связывается с хостом, к которому подключен USB-хаб. Он же заводит тред в Slack…


… и впоследствии синхронизирует все необходимые данные с массивом устройств через интерфейс ADB:


В процессе тестирование устройства сами сообщают о прогрессе и обо всём, что на них происходит, через внешний файл.


С помощью механизма ADB мы получаем данные с устройств, важные события отсылаем в Slack и записываем статистику в ClickHouse. Позже по накопленной статистике можно построить различные графики и отчеты с помощью Redash.

Работа с устройствами через ADB



ADB — это низкоуровневый USB-интерфейс, с помощью которого можно подключаться к устройствам и делать с ними практически всё, что угодно. При помощи ADB мы:

  • записываем на устройства внешний файл с тестовым скриптом BHL;
  • туда же пушим специальную версию приложения, в которую добавлена тонкая надстройка, позволяющая выполнять тестовые скрипты BHL;
  • периодически считываем с устройств файл со статусом выполнения тестов.

Интеграция со Slack


Когда у нас начинается тестовая сессия, мы записываем её название с уникальным идентификатором и запускаем тестовые планы — это заданные разработчиком логические группы тестов, и их может быть сколько угодно. В данном случае у нас два тестовых плана:

  1. Прогон туториальных уровней. План выполняется первым, потому что проверке туториалов отдан наивысший приоритет.
  2. Прогон обычных уровней.

В каждый тестовый план включена такая информация:

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



Есть еще цветовая идентификация:

  • зелёный — всё хорошо;
  • оранжевый — что-то было, но не критичное;
  • красный — критичные ошибки.

Перейдя в тред тестового плана можно посмотреть подробности и понять, какие неприятности произошли.


О чём мы сообщаем в случае ошибки



  • Последний скриншот. В случае фатальной ошибки на устройстве игра останавливается, и мы можем увидеть, в каком состоянии была игра.
  • Стектрейс.
  • Код реплея — это текстовая строка, в которой закодированы все действия игрока.

ljPaAC9AbTMvbGV2ZWxzL2V4cGVyaW1lbnQvY3J5c3RhbC9sdmxfMTFfOV9zcXVhcmVfMs0Pc56VAZIFA5IFAkwAlQGSBQeSBQjMoQCVAZIGBJIGBczQAJUBkgUGkgUHzPoAlQKSBgeSBgfNARwAlQGSCAOSCATNAVUAlQGSBAGSBALNCqoAlQGSBACSBAHNCzwAlQGSBAKSAwLNC2AAlQGSAwKSAgLNDKUAlQGSAwWSAwTNDUQAlQGSAgKSAwLNDm4AlQGSAwOSAgPNDsgAlQGSAwSSAgTNDw8AAJA=


В нашей игре движок Match3 детерминирован, и, имея все инпуты от игрока, мы можем воспроизводить пользовательскую сессию в редакторе. Это позволяет понять, что человек делал.


Реплей загружаем в редактор, перематываем на последний ход, стартуем симуляцию — и бац! — вот она, ошибка. Мы видим её саму и действия, которые к ней привели.

Отчёты в Redash



В Redash мы выгружаем статистику:

  • потребления памяти;
  • падений;
  • предупреждений по перерасходу памяти.

Когда приложение начинает вести себя нехорошо, то Android и iOS шлют предупреждения, чтобы разработчики могли предпринять действия. Именно это мы и записываем в Redash.

Случай из практики



В отчёте из Redash вы можете видеть динамику потребления памяти. Здесь мы журналируем максимальное потребление (синий), среднее (красный) и медианное (зелёный). График идёт в обратном порядке времени: слева свежие данные, правее — старые. Особое внимание мы обращаем на максимальное потребление. Если у игроков превышен уровень потребление памяти, то с большой вероятностью это приведёт к падению.

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

Когда в продакшене было повышенное потребление, билд себя чувствовал не очень, и это можно заметить по отчету о падениях в консоли Google Play. После того, как мы залили сборку с более оптимальными текстурами, количество падений пошло вниз. Ферма подсказала нам, что дела стали намного лучше ещё до того, как мы стали что-то выпускать в продакшн.

Итог


На всё про всё ушло полгода разработки разной степени интенсивности. Ферма в рамках CI каждую ночь прогоняют 1500+ уровней примерно за 5 часов. По мере необходимости размер фермы будем увеличивать.

Маленькие рецепты для тех, кто захочет обзавестись собственной фермой



Как эмулировать пользовательский ввод


  1. Использовать новую подсистему Input System для Unity (в последней версии). Возможно, она даже неплохо работает, но нам не подошла, потому что проект Storyngton Hall разработан под те версии Unity, где подсистема недоступна. Переезжать на новую версию Unity нам из-за этого было бы неразумно. Тем не менее, рекомендую посмотреть в ту сторону — это самое гибкое решение.
  2. $ adb shell input tap x y — через низкоуровневые механизмы ADB тоже можно эмулировать ввод. Всё замечательно, но дико медленно: эмуляция каждого тапа занимает несколько секунд. Если бы мы пошли таким путём, то ферма проходила бы весь цикл тестов не за 5, а за 15 часов.
  3. Связка Java + Android Activity оказалась нашим спасением. Мы эмулируем ввод на уровне Android Activity, обращаясь к ней из кода приложения. В C# это выглядит примерно так:

public void Tap(Vector3 p) {
    var pt = World2Display(p);
#if UNITY_ANDROID
    const string bridge_name = "com.goplaytoday.utils.atf.ATFUtils";
    using(var bridge = new AndroidJavaClass(bridge_name))
    {
      bridge.CallStatic("EmulateTap", (int)pt.x, (int)pt.y);
    }
#endif
  }


Как эмулировать тапы


public static void EmulateTap(Activity activity, int x, int y) {
    PointerProperties[] properties = new PointerProperties[1];
    PointerProperties prop = new PointerProperties();
    prop.id = 0;
    prop.toolType = MotionEvent.TOOL_TYPE_FINGER;
    properties[0] = prop;
    PointerCoords[] coords = new PointerCoords[1];
    PointerCoords coord = new PointerCoords();
    coord.x = x;
    coord.y = y;
    coords[0] = coord;
    int source = InputDevice.SOURCE_TOUCHSCREEN;
    int deviceId = 0;
    final long ms = SystemClock.uptimeMillis();
    activity.dispatchTouchEvent(
      MotionEvent.obtain(ms, ms, MotionEvent.ACTION_DOWN, 1, properties, coords, 0, 0, 0, 0, deviceId, 0, source, 0));
    activity.dispatchTouchEvent(
      MotionEvent.obtain(ms, ms+200, MotionEvent.ACTION_UP, 1, properties, coords, 0, 0, 0, 0, deviceId, 0, source, 0));
}

Привожу работающий пример на Java для Unity — бился над ним целый день. Все берут примеры со Stack Overflow, которые должны работать, но они, естественно, не работают. Пришлось довольно долго итерировать и прорабатывать, пока наконец не получилось то, что нужно.

Как эмулировать свайпы


Свайпы отняли у меня еще полдня. Привожу пример работающего кода:

public static void EmulateSwipe(Activity activity, int x1, int y1, int x2, int y2, int durationMs) {
  PointerProperties[] props = new PointerProperties[1];
  PointerProperties prop = new PointerProperties();
  prop.id = 0; prop.toolType = MotionEvent.TOOL_TYPE_FINGER;
  props[0] = prop;
  PointerCoords[] coords = new PointerCoords[1]; PointerCoords coord = new PointerCoords();
  int source = InputDevice.SOURCE_TOUCHSCREEN; int deviceId = 0; int maxMoveSteps = 10;
  int sleepTime = durationMs / maxMoveSteps;
  activity.runOnUiThread(new Runnable() {
    public void run() {
      final long ms = SystemClock.uptimeMillis();
       coord.x = x1; coord.y = y1; coords[0] = coord;
       activity.dispatchTouchEvent(
         MotionEvent.obtain(ms, ms, MotionEvent.ACTION_DOWN, 1, props, coords, 0, 0, 0, 0, deviceId, 0, source, 0));
       for(int i=0;i<maxMoveSteps;++i) {
          float t = (float) i / (float) maxMoveSteps;
          coord.x = x1 + ((float) (x2 - x1) * t); coord.y = y1 + ((float) (y2 - y1) * t);
          coords[0] = coord;
          activity.dispatchTouchEvent(
            MotionEvent.obtain(ms, SystemClock.uptimeMillis(), MotionEvent.ACTION_MOVE, 1, props, coords, 0, 0, 0, 0, deviceId, 0, source, 0));
          try { Thread.sleep(sleepTime); } catch (InterruptedException e) { }
       }
       coord.x = x2; coord.y = y2; coords[0] = coord;
       activity.dispatchTouchEvent(
         MotionEvent.obtain(ms, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 1, props, coords, 0, 0, 0, 0, deviceId, 0, source, 0));
     }
  });
}

Как измерить потребление памяти


Существует много различного инструментария, тот же Unity Profiler, Android Studio и прочее. Но зачастую они не отражают объективного положения дел, особенно Unity Profiler. Вы думаете, что у вас потребляется одно количество памяти, а на деле — в разы больше. Мы столкнулись с тем, что механизм ADB даёт наиболее точные результаты. Команда $ adb shell dumpsys meminfo <бандл приложения> помогает отследить, сколько сейчас реально занимает ваша игра в памяти.


Как ускорить тесты



Мы ускоряем тесты в несколько раз там, где это уместно. Благодаря тому, что у нас симуляция match-3 изолирована от представления, мы смело можем ускорить тесты в четыре раза. В Unity есть довольно удобный механизм, который позволяет изменить масштаб времени: Time.timeScale. За подробностями обратитесь к документации.

Устройство может перестает отвечать в любой момент



В мире реальных и не самых топовых железок может произойти всё, что угодно: устройства могут зависнуть, проложения вдруг начнут вылетать и ещё по всякому сходить с ума. Если будете делать ферму, то готовьтесь к тому, что всё может пойти в разнос. Эту проблему мы решаем довольно просто:

  • Наши тестовые скрипты периодически записывают в файл особую строчку PING.
  • ATF считывает этот файл и анализирует, как давно не было пингов с устройств.
  • Если пингов не было давно, то устройство считается зависшим, мы его перегружаем и заново запускаем набор тестов.

Команда ADB может зависнуть



Команда ADB, конечно, замечательная и надёжная, но и она может войти в ступор. Мы решили проблему кардинально. Все наши shell-команды обёртываются в специальный таймаут-скрипт, у которого есть жёсткие ограничения по времени выполнения. В примере выше, если команда не выполняется за 10 секунд, то мы считаем это ошибкой и сигнализируем о ней в Slack.

Устройство может взорваться



Один наш смартфон вспучился, задняя крышка стала отходить — вздулся аккумулятор. Мы отложили его, и где-то через пару недель он реально взорвался. Именно поэтому необходимо нормальное охлаждение и жаропрочный корпус.

Резюмирую


Достоинства Android Test Farm


  • Оправдано для больших, долгоиграющих проектов от 1 года, которые находятся в активной разработке или на поддержке.
  • Снимает много рутины и облегчает жизнь тестерам.
  • Формирует сетку безопасности и придаёт разработчикам смелости для экспериментов.

Недостатки Android Test Farm


  • Нет смысла внедрять для маленьких проектов.
  • Решение не из коробки, придётся попотеть самим.
  • Требует постоянной поддержки и человека, который возьмёт ферму на контроль. Разработчики постоянно добавляют новый функционал, поэтому тестовые скрипты время от времени ломаются.
  • Не полностью защищает от ошибок, скорее это ещё один эшелон защиты, который добавляет уверенности, что всё работает более-менее нормально.
  • Ферма не покажет визуальные глюки, когда какой-то спрайтик съехал, или что-то неправильно отображается.
  • Иногда требуется физический доступ к устройствам, с удалёнки не поуправляешь.

Если решитесь делать такую ферму, то реализуйте её как можно раньше, еще до релиза проекта. Так вы сможете замерить потребление памяти, увидеть регрессии и принять нужное решение ещё до того, как опубликуетесь в сторах. Мы внедрили ферму не сразу и упустили момент с потреблением памяти, хотя могли отловить это перед тем, как уже нагнали рекламы и привлекли много пользователей. Так что если планируется серьёзный проект на много денег — внедряйте как можно раньше.

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


  1. Markscheider
    22.09.2021 15:15

    Тема жаропрочного короба не раскрыта. Вы в него прятали работающие тестовые смартфоны? Он с принудительным охлаждением?

    Если на все ответы "да", то непонятно - с чего вспучило один аппарат. Может, брак попался?


    1. pachanga Автор
      22.09.2021 15:20

      На данный момент в качестве "жаропрочного короба" выступает бывший системый блок от старого PC, из которого изъяли все внутренности. Да, в нем просто находится хаб с работающими телефонами. Охлаждение в нем пассивное, но если кто-то соберется делать что-то посерьезнее, рекомендую присмотреться к более интересным вариантам.

      Почему вспучило - сказать конкретно сложно, может и брак. Тем не менее, прецедент был довольно тревожный.


      1. Markscheider
        22.09.2021 15:39
        +3

        Охлаждение в нем пассивное

        Не воспринимайте, плз, как менторство, но вы довольно смелые люди :). Упрятать десяток работающих в загруженном режиме смартов в коробку с чисто символическими дырочками для вентиляции... Перегрева процессора и тротлинга, думаю, не дождемся, но постоянное нахождение лития при высокой температуре - такое себе удовольствие. По паспорту у этих батарей максималка +60С, но 10 смартфонов в закрытом коробе могут и посильнее нагреться.

        выступает бывший системый блок от старого PC

        Там же есть штатные посадочные места? Поставьте хотя бы один вытяжной вентилятор, а то мы за вас переживаем :)


        1. pachanga Автор
          22.09.2021 15:44

          Спасибо :) Данный "короб" - временное решение.


          1. DarkTiger
            22.09.2021 23:27
            +2

            Временное, говорите? Ну вот держите фото постоянных. Шэнчжэнь, 2018


  1. amedvedjev
    23.09.2021 11:35
    +1

    Мне нравится. У нас похоже:

    • 12 iPhone

    • 12 Android (Nokia где чистый Андроид)

    • 1 macMini (недавно добавили еще один)

    • Java + Appium + TestNG + Maven

    • 1500 тестов за 2,5 часа

    Самое интересное - даже один макМини легко держит это все хозяйство. А мы еще видео всех тестов делаем 24 потока...


    1. pachanga Автор
      23.09.2021 11:37

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


      1. amedvedjev
        23.09.2021 11:53
        +1

        Appium поддержал видео не так давно(год или чуть больше).

        Для Android нативными ADB командами (хотя мы так и не перешли еще. используем те же команды сами).

        Для iOS написал Appium свой видео стрим делают. Мы забираем фактически его. Переменный битрейт делает iOS видео с маленьким размером. В Android я быстро пережимаю ffmpeg командами.

        Ну а вообще Appium удобная штука. И именно им и поддержали все.

        Slack, TestRail report это уже просто на Java.

        ЗЫ я тут если есть вопросы -> https://discuss.appium.io/u/Aleksei на официальном их форуме или тут тоже.


        1. pachanga Автор
          23.09.2021 12:15

          Когда я говорил про видео, я имел в виду реальную стороннюю камеру, которая бы записывала работу всей фермы :) С самих телефонов не хочется снимать видео, т.к это потенциально может негативно влиять на процесс тестирования.

          В свое время смотрели на Appium, но так и не придумали, как его прикрутить к Unity приложению, поэтому "нагородили" свое.


          1. amedvedjev
            23.09.2021 14:39

            Интересно как вы tap по id сделали.

            adb exec-out uiautomator + adb shell input tap ?


            1. pachanga Автор
              23.09.2021 14:55

              Мы так пробовали, но не стали. Я в статье описал способ: мы эмулируем ввод на уровне Activity. Т.е сами тестовые скрипты пробрасывают через C# в Activity события ввода. Но для приложения все происходит так, как если бы реально пользователь тапал и свайпил по экрану.


              1. amedvedjev
                23.09.2021 15:20

                Блин сорри. Вы это написали....

                Я как-то пропустил.


  1. lim14
    29.09.2021 14:15

    Не пробовали подключаться к гугловым фермам?


    1. pachanga Автор
      30.09.2021 12:29
      +1

      Мы поизучали немного фермы на Amazon. Как-то нас и цены напугали, и какой-то не сразу очевидный API....больше смотреть не стали. Но, кто знает, если понадобится более 50 девайсов...