Для тех, кто пришёл за кодом прокси (AVD Manager'a)

Держите ссылку на GitHub. Сверять контекст можно с этой статьёй – так будет удобнее.

Поехали!

Начнём с конкретики :-)
В статье будем рассматривать три основных вопроса:

  • Что даст вам такой переезд в цифрах, и на каком железе мы остановили свой выбор, проанализировав разные варианты;

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

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

А зачем переезжать на ARM?

Примерно год назад, предыдущая веха ресёрчей привела нас к Selenoid. Получилось заставить его работать с последними версиями Android, даже несмотря на то, что продукт уже долгое время не развивается. Но пришло двигаться дальше – и мы решили посмотреть варианты, что же можно нам закупить.

Xeon'ы – нестареющая классика для ЦОДов
Xeon'ы – нестареющая классика для ЦОДов

До этого у нас была, наверное, максимально классическая конфигурация машин, которая стоит во всех ЦОДах страны – это серверные процессоры Xeon серебряной серии. Одна такая машина после правильной настройки всех эмуляторов и сборки Docker-образов могла вывозить на себе шесть потоков Android 15 с полноценным Google Play, что уже является не самым плохим результатом. И это учитывая что у нас Х86 с 24 ядрами на борту, а приложение у нас довольно ресурсоёмкое – всё-таки у нас онлайн-кинотеатр.

А если рассматривать андроиды постарше и "пообрезаннее", то на такой конфигурации можно уже пускать около 10-ти потоков. Звучит неплохо, но...

Минусы X86

Разное поведение на ARM и x86

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

И ответ «почему так», на самом деле очень прост – попробуйте сейчас найти в магазине реальное Android-устройство на х86-архитектуре. Последний раз я видел такое в 2016-м, когда китайцы пихали в тв-приставки две операционки – Windows и Android в режиме "dual-boot". Сейчас такого уже нет даже в самых тёмных подвалах алиэкспресса.

Вот так они выглядели, кстати
Вот так они выглядели, кстати

По поводу отличающегося поведения расскажу яркий пример, так сказать, от первого лица. Как-то раз, перед новым годом, нашу разработку попросили внедрить в приложение красивую заставку при старте приложения – со снежинками и шариками. Заставка была нарисована в нестандартном формате, поэтому пришлось подключать библиотеку Lottie, которая могла бы красиво эту анимацию играть. Внедрение прошло гладко, но на стадии регресса релиза автотестами, выяснилось, что приложение стало крашиться при запуске – причем всё осложнялось тем, что логи при краше были вообще неоднозначными. В итоге все на нервах, сроки горят, проблему локализовали, и просто для архитектуры Х86 заставку вырубили, заменив на статику.

Что будет дальше с Android 16?

В приложениях такого типа нужны тесты поотзывчивее
В приложениях такого типа нужны тесты поотзывчивее

Но хорошо – может с поведением не повезло только нам. А вот что делать с тестами, где нужна высокая скорость выполнения, а железо уже и так работает на пределе? Например, у нас есть тест, где нужно открыть элементы управления в плеере, и пока они не исчезли за условные три секунды, перемотать видео, переключить субтитры и так далее. И так как наше приложение довольно высоконагруженное, всё-таки иногда на Х86-эмуляторах в таких ситуациях бывали подвисания, а тесты в итоге падали.

А если посмотреть в завтрашний день, и нам понадобится новый 16-й андроид, который требует еще больше железных ресурсов?

Обновление – через пересборку образов

Ну и последнее, в качестве вишенки – кто собирал докер-образы Android, а еще и под Selenoid, знает как это бывает больно и мучительно. И хочется чтобы вместо нас этим делом занимался сам Google – обновлял бинарники эмуляторов, например. Так что у нас пошла череда весёлых экспериментов.

Genuine ARM64 (AArch64)

64 ядра на ARM – это вам не шутки!
64 ядра на ARM – это вам не шутки!

Сначала мы подумали – если у нас есть "промышленные Xeon’ы", то и ARM должен быть промышленный и корпоративный. Так мы почти закупили bare-metal систему от Gigabyte вот с такими характеристиками как на картинке, но нас спас очередной ресёрч.

А на него... ничего и нет

Если на x86 у нас просто были проблемы, но с ними ещё можно как-то жить, то на этой архитектуре какие-либо готовые решения формата «настроил и оно работает» отсутствуют как класс. Дело в том, что Google официально не поддерживает эту архитектуру в своих эмуляторах, если мы говорим не про телефоны.

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

Перспектива у этого всего, конечно, так себе. А самое смешное, что решение нашей проблемы оказалось буквально перед носом – надо было просто посмотреть на свой… макбук.

Apple Silicon

И вот мы плавно подошли к тому, что в итоге мы и выбрали. Понятно, что покупать макбуки в ЦОД это не очень спортивно. Но у Apple есть, что можно без проблем поставить в серверную стойку, чтобы потом на тебя не смотрели как на клоуна. Это Mac Pro.

А с M3 Ultra можно выжать ещё больше – но Mac Pro в таком конфиге ещё нет (на октябрь 2025 года)
А с M3 Ultra можно выжать ещё больше – но Mac Pro в таком конфиге ещё нет (на октябрь 2025 года)

Мы выбрали достаточно жирную версию по железу, и получили впечатляющие результаты – эта штука спокойно держит в параллели 12 эмуляторов 15-го андроида со всеми сервисами Google Play, и 20 потоков андроида попроще. И все они работают так, будто бы не замечают остальных. Но это не единственный плюс этого решения.

Максимальная консистентность

Первая, пожалуй, киллер-фича – это ARM, и это MacOS. Никаких эмуляций, виртуализаций – ваши автотесты гоняются ровно на той же конфигурации, на которой ваше приложение создаётся и отлаживается разработчиками. Это решает как неожиданности с отличающимся поведением, о которых я рассказывал выше, так и у вас просто не болит голова, когда к вам приходят Manual QA, и говорят: "а вот давай прогоним что-то на новом Android’e". А у вас образов либо нет, либо адекватно крутится на инфре в нужном количество потоков максимум 13-я версия.

Последние образы – из коробки

Кстати, про образы. Mac Pro – всё еще технически почти что ваш макбук. А это значит, что на него одной кнопкой ставится любой андроид, любой эмулятор прямо с Android Studio. Конечно, лучше в итоге это делать по взрослому через sdkmanager, но это я для примера – об общей простоте настройки.

60 FPS эмуляции для любых задач

И напоследок, о быстродействии. Сравнивать производительность даже самого вычищенного образа эмулятора Х86 и вот этот вариант – даже неловко. Он просто летает – и мы чуть позже убедимся с вами в этом в цифрах. Считайте, что он работает как ваш флагманский телефон, и при этом в абсолютно любых задачах.

А что с деньгами?

Чтобы статья была всегда актуальна – привожу цены "в условных единицах"
Чтобы статья была всегда актуальна – привожу цены "в условных единицах"

Но перед тем, как я расскажу о минусах, точнее минусе – использовании маков под автотесты, я обещал вам циферки, а именно – сколько будет в закупке стоить одна машина в пересчете на международные условные единицы. Начнём с не маков. Первые два кандидата в таблице посчитаны как комплект – то есть это не только процессор, но и необходимые память, накопитель, корпус... И всё остальное – чтобы это собрать и поставить в стойку под ключ.

И у нас получаются интересные цифры. Оказывается, что Mac Pro дороже нашего текущего X86-комплекта всего на 30%, но при этом, минимум, на бумаге в два раза быстрее. А если вам не принципиально ставить машину в стойку, то можно сэкономить еще пару тысяч и взять ещё более быструю мак-студию (не спрашивайте меня, почему она дешевле и при этом быстрее – это вопрос к ценовой политике Apple). А вот "промышленное" ARM-решение, если мы ещё и поставим туда пару Quadro, вообще выходит почти как два Mac Pro.

Теперь, кажется, ни у кого не осталось сомнений, почему мы ввязались в эту авантюру с Apple.

Цены-ценами, а что насчёт прироста?

Туррелька – для привлечения внимания
Туррелька – для привлечения внимания

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

Что мы имеем и какие будут условия:

  • У нас есть пак на 200 очень жирных е2е-тестов – которые полностью выполняют один пользовательский сценарий от первого экрана приложения до последнего;

  • Мы используем самые последние стабильные образы, доступные для обеих систем – это прожорливый Android 15, да еще в полной комплектации – с Google Play магазином, сервисами и прочим. Обычно такие вещи выпиливают из образа, чтобы выжать потоков побольше, но мы сегодня будем сравнивать наши машинки в максимально схожих условиях;

  • И как вы уже догадались, сравнивать мы будем наше решение на MacOS с предыдущим же нашим решением на Х86.

ОКАК!
ОКАК!

Начнём с Х86 в его максимальной загрузке – 6 потоков 15-го андроида. Получаем результат почти в три часа. Печальное зрелище – останавливаться мы на нём не будем.

Теперь к интересному. Давайте сравним чистую производительность на один поток – но на двух разных архитектурах. Ага, время выполнения тестов у нас срезало на целый час. Почему так произошло, это ведь вроде бы нелогично?

Ответ здесь заключается в том, что если ваши UI-тесты написаны грамотно, то они завязаны не на абстрактных ожиданиях, а на так называемых explicit wait – это когда мы не просто в каких-то случаях ждём N секунд, а всегда строим логику теста, опираясь на происходящее на экране. И соответственно, выполняем шаги теста в зависимости от найденных (или не найденных) элементов. И так как на ARM у нас эмулятор в разы отзывчивее – то и приложение на нём работает быстрее, и коммуникация с Appium происходит оперативнее.

И в конце запустим этот же пак уже на взрослых настройках в 12 потоков, и здесь получаем ожидаемый развал кабины – тесты почти уложились в час, что в три раза быстрее, чем было у нас изначально. Такие дела.

А как этим рулить?

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

Обычно делают как – крутят эмуляторы в докере, иногда в кластерах на кубах, иногда в Selenoid или Selenium Grid, когда мы говорим про Appium. Там в целом есть проверенная годами схема – есть образы, и есть готовые решения по маршрутизации и управлению этими образами. А здесь – есть Mac, и? Как ни странно, на него тоже есть уже готовые решения, которые мы можем использовать. Расскажу про два из них.

Selenium Grid

Оказывается, ничто не мешает нам на Mac поставить нативный ARM Selenium Grid в любой редакции. Он вполне способен маршрутизировать Selenium-сессии, а значит и Appium-сессии. Казалось бы – вот он, успех. Но есть и проблемы, которые для нас оказались критичными. Мы же хотим много потоков, верно? Лучше 12.

И в Grid есть проблема, что он сделан так, что он не может запустить один эмулятор 12 раз: там одна нода – это один эмулятор. В целом, можно хранить 12 эмуляторов на машине, никто этого нам не запрещает – и связать 12 нод с 12-ю эмуляторами. Но… а если нам нужны разные версии Android? А если нам нужны планшеты на разных версиях Android? А если нам нужен Android TV? При этом, если вам потребуется преднастраивать эмулятор, а как правило, это требуется – сертификаты, отключение лишнего, предустановка нужных приложений… то просто представьте, как с этим зоопарком потом вам же и работать.

Так, падажжи – это всё теперь на мне?
Так, падажжи – это всё теперь на мне?

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

Восток Java - дело тонкое
Восток Java - дело тонкое

Appium Device Farm

Но как ферма девайсов – кайфовая штука!
Но как ферма девайсов – кайфовая штука!

Что у нас дальше? Appium Device Farm. Прикольная штука, которая уже подходит нам немного больше. Она умеет классно стримить экран девайса, а устройствами можно управлять удаленно в ручном режиме через их UI. В целом это решение довольно неплохо показывает себя именно как ферма реальных девайсов, а не менеджер сессий тестов – собственно, для чего она и задумывалась. И опять же, она умеет запускать эмулятор при своём старте – но только одну копию, а не несколько, что нужно нам. И не умеет ими управлять в процессе своей работы. Поэтому придется что-то колхозить, чтобы заставить это работать так как хотим мы. И тут вылезает еще один косяк.

Это – частично закрытый исходный код. Основная логика сервиса действительно лежит на гитхабе под лицензией MIT, а вот всё что касается управления и UI – проприетарное и закрытое, так что добавить туда что-то новое под себя у вас может и не получиться:

Мы – open source, но есть нюанс
Мы – open source, но есть нюанс

Пишем сами

Как говорится, если велосипеды нам не подошли, нужно сделать свой. К тому же, если знать, как работает Appium – это становится очень простой задачей. Сейчас всё расскажу.

Слишком просто, где же подвох?
Слишком просто, где же подвох?

Что мы знаем про Appium? Формально, это обычный HTTP-сервер, в котором мы дёргаем ручки по стандарту WebDriver Protocol – а значит мы можем работать с Appium как с простым REST-сервисом.

Сессия для автотестов создаётся элементарно – мы дёргаем ручку POST /session, передавая туда нужные параметры, называемые Capabilities, в формате JSON. Когда сессия создаётся успешно, мы получаем ответ 200 и уникальный ID созданной сессии. А затем можем получить состояние сессии через одноимённую ручку GET. Удалить же сессию можно, отправив на сервер DELETE /session. Элементарно, верно?

Всё еще слишком просто
Всё еще слишком просто

Получается, что нам нужно сделать некий менеджер, который смог бы:

  • При попытке создании сессии перехватывать этот запрос

  • Запускать нужный эмулятор и Appium-сервер для него

  • И далее маршрутизировать запросы уже на конкретную сессию

То есть, если посмотреть на схему выше, это будет некая прослойка между нашими автотестами и Appium. Хорошо. Допустим, мы немного знаем Python, и нам надо быстро и решительно написать на коленке эдакий реверс-прокси сервер, который сможет нам с этим помочь. Что же нам в этом поможет?

Правильно – конечно же FastAPI.

FastAPI 101

А теперь уже сложно? Ну, как сказать...
А теперь уже сложно? Ну, как сказать...

Чем же хорош FastAPI? Ну, например – он компактный, быстрый, как следует из названия, и простой в освоении. На картинке выше – пример из нашего будущего кода, как мы будем проксировать весь нужный нам трафик, при этом:

  • Учитывая изначальный нужный путь, чтобы забирать только нужное (1)

  • Перенаправлять трафик на необходимый порт с Appium (2)

  • Подкладывая при этом оригинальные заголовки и тела запросов и ответов – чтобы Appium не догадался, что он работает не напрямую с автотестами (3)

Выглядит несложно? А ведь это одна из самых сложных частей нашего самописного сервиса.

Рабочий прототип в две строки

Чтобы предыдущий код заработал, добавим наверх импорты, а вниз – код запуска основного цикла сервиса, и можно запускать. Так наш сервер превратится в настоящий прототип и уже будет работоспособен:

# наверх
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse

app = FastAPI()

# вниз
if __name__ == "__main__":
app.run()

Стартуем эмулятор

Хорошо. У нас есть сейчас максимально бесполезный прокси-сервер, и теперь нам надо научить его выполнять нужные нам штуки. И следующее по списку – научиться запускать эмуляторы. Прежде чем писать код, погружу вас в небольшие нюансы этого дела, куда же без них. Какие же моменты нужно знать?

Момент первый. Нет сложности запустить эмулятор через терминал, но если вы сделаете это дважды, то второй инстанс выкинет вам ошибку – что нельзя запускать две копии эмулятора одновременно. Но если мы добавим в параметры флаг –read-only, то мы получим сразу два плюса.

Во первых, мы сможем запускать теоретически бесконечное число эмуляторов из одного образа. А во-вторых, он не будет хранить своё состояние при уничтожении экземпляра, что для автотестов – лучшее поведение из возможных. Вы же знаете, что на маках идеально работают снапшоты из коробки, верно? Это так называемые, снимки системы состояния эмулятора, которые можно сохранять, и затем при старте за две секунды получать готовый к работе эмулятор в нужном стейте:

Это окошко знакомо всем
Это окошко знакомо всем

Второй пункт касается тех, кто тестирует на Android TV. Просто добавьте флаг -feature Vulkan в параметры запуска, чтобы и там тоже заработали снапшоты, поскольку иначе эмулятор не захочет сохранять своё состояние, ругаясь на отсутствие ускорения (это баг эмулятора) – хотя ускорение там есть и работает оно отлично.

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

Пишем код для запуска эмулятора

Ну, поехали. Список портов, о котором только что шла речь, у нас в самом верху кода. Обратите внимание, что они все чётные. Почему? При старте эмулятора он на самом открывает не один, а два порта – чётный и нечетный. По чётному он доступен как девайс, которым мы сможем управлять по adb, а другой – это внутренний консольный порт. И это захардкожено внутри эмулятора, так что если вы запустите два эмулятора на двух соседних портах, вы не сможете достучаться на нечётный из них. Это важно.

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

Далее мы просто стартуем подпроцесс, пока тупейшим образом, не получая id этого процесса – это всё сейчас у нас для тренировки. И в конце не забываем убрать из списка порт, на котором мы запустили эмулятор.

Стартуем Appium

Эмулятор запускать мы научились, теперь настала череда Appium. Продолжаем писать код:

Видели, наверное, когда-нибудь в adb devices заветную строчку emulator-5554? Вот, это оно и есть
Видели, наверное, когда-нибудь в adb devices заветную строчку emulator-5554? Вот, это оно и есть

Код будет очень похожий, расскажу про нюансы. Appium нам важно запустить на определенном порту, потому что мы потом будем перенаправлять на этот порт трафик с созданной сессии. В Capabilities, тобишь параметры сессии, мы обязательно прокидываем эмулятор, с которым Appium'у нужно работать, чтобы наши сессии не разъезжались по разным инстансам. И уникальный id этого эмулятора нам всегда известен: это надпись эмулятор, дефис, и его порт.

Перехватываем сессию

Теперь перейдем к ключевой части - перехвату сессии. В FastAPI такие вещи пишутся очень просто. Видите наверху декоратор, который с собачкой? В нём заданы все ключевые вещи - что на тип запроса POST с таким-то адресом, нужно отреагировать и выполнить код. Давайте же его выполним.

  1. Забираем с наших capabilities так называемый ключ browserVersion – обычно для веба туда кладётся тип браузера, или же в нашем случае, название эмулятора при запуске тестов – ровно такое, как вы укажете в Android Studio при создании эмулятора.

  2. Далее мы запускаем эмулятор, Appium, вызывая уже знакомые нам функции, и теперь важная часть – этот же запрос мы отправляем уже на нужный порт Appium'а – обратите внимание на URL, где мы создаём локальную сессию.

  3. И уже после этого отдаём результат создания сессии обратно автотестам. Если вы всё сделали правильно, и отключили флаг -no-window, можно будет уже лицезреть вот такие весёлости – значит всё у вас заработало правильно:

Deque и async.Lock()

Давайте теперь добавим в наш код всяких классных штуковин. Начнём с deque – типа данных из стандартной библиотеки питона. Эта штука работает как стэк или очередь в других языках программирования, и в отличии от списка, работает гораздо быстрее, и даёт нам нужные операции для наших портов. На картинке выше показан код, как можно легко забрать первый порт из нашей очереди, а затем вернуть его в конец. Это нужно для того, чтобы предотвратить сетевые ошибки, когда на сетевом уровне порт еще не успел освободиться, а его уже пытаются занять. Так мы можем использовать весь наш диапазон портов, что повышает надёжность нашего прокси.

Вторая штука называется asyncio.Lock(). Она гарантирует, что критическая секция в коде выполняется только одной корутиной за раз, предотвращая гонки данных в асинхронном коде. Сейчас было много умных слов, давайте по порядку.

Это должна быть гифка – достройте в голове анимацию сами :)
Это должна быть гифка – достройте в голове анимацию сами :)

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

Но как вы понимаете, вы такой в очереди можете быть не один, и когда данные у вас одни, а желающих много – возникает потасовка с криминалом, или как принято говорить, гонка данных:

Эта формула-1 очень деструктивно влияет на ваши нервы
Эта формула-1 очень деструктивно влияет на ваши нервы

Что это такое? Простой пример для понимания - если вы с другом решите одновременно записать в один файл разный текст с разных компьютеров, то результат может стать непредсказуемым. Поэтому нам важно, чтобы когда одна корутина забирала порт, этот же порт не забрали другие. Поэтому – делаем как на картинке с кодом :)

Готово!

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

Очереди и обработка ошибок

С первым всё довольно банально – можно добавить механизм очередей, чтобы сессия при отсутствующих свободных портах клалась в список ожидания. Это сделать довольно просто и быстро – с помощью ещё одного класса библиотеки asyncio, которую мы уже знаем - Future()

Все исключения важны, все исключения нужны
Все исключения важны, все исключения нужны

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

Оркестрация

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

Lerooooy... Gitlab CI
Lerooooy... Gitlab CI

Можно пойти в лоб, и привязать в CI к каждому агенту свою машину, и управлять очередями и нагрузкой со стороны CI. А можно написать похожий на наш родительский прокси, который раздаёт сессии на дочерние машины с уже тем софтом, что мы написали. Суть та же, но мы сейчас не будем подробно обсуждать нюансы реализации – оставляю это на ваш мозговой штурм, ведь только вы знаете свою инфраструктуру в компании, и как лучше будет это сделать в вашем случае.

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

Ферма девайсов и эмуляторов

Допустим, мы хотим не только гонять автотесты на всём, что нам теперь доступно по мощностям, но и передавать эти вещи в пользование другим отделам – разработчикам, QA, и так далее. Забегая вперёд, нашей фермой пользуются и аналитики, и даже ребята, работающие с контентом. Но как же превратить наш прокси в ферму? Давайте я расскажу про три самых главных момента для реализации такой штуки своими руками. Поехали.

Если начать думать, как же заполучить себе видеопоток с эмулятора или другого устройства, то оказывается, что всё уже давно придумано за нас. Смотрите – на стороне нашего прокси-менеджера мы уже можем запускать любые ADB-команды на девайсе и стартовать сессии. А почему мы не можем выполнять при этом скрипты через драйвер? Ведь UIAutomator сейчас из коробки может начать стрим с девайса, нужно только уметь его забирать и обрабатывать. Вся необходимая информация есть прямо в официальной документации Appium, а конкретно – в репозитории UIAutomator2-driver. И на экране – те самые команды, которые мы можем легко запускать.

Окей. Мы теперь умеем запускать и останавливать стрим – осталось получить видеопоток. Можно пойти двумя путями: проще всего использовать штатное решение из документации Appium – Gstreamer. Не забудьте создать еще один список заготовленных портов, и пробуйте выводить картинку через CV2, так как для него есть готовый пайплайн для Gstreamer.

Если Gstreamer вас не радует, есть еще один вариант – нативная для маков улилита по стриму android-экрана scrcpy. Если запустить её в режиме loopback, можно заставить её стримить экран на порт, а дальше – дело техники. Кстати, интересный факт – для iOS этого всего вообще не нужно делать, так как там при подключении к девайсу сразу же доступен MJPEG-порт из коробки, и нам остается только его рестримить.

Третий, самый сложный момент, который потребует от вас уже некоторых знаний HTML и JS. В чем идея: поверх вашего видео, которое вы выводите, вам нужно нарисовать некий холст – он так и называется, canvas. Через Appium мы легко можем получить разрешение экрана устройства, так что мы можем достаточно точно спозиционировать наш элемент. При клике или свайпе, мы забираем координаты нашего канваса, и отправляем в драйвер команду на соответствующую ручку, например на /tap. В общем-то, вот так оно и работает.

Если немного покурить API, то можно развить эту штуку в космос – поддержать системные кнопки, смену ориентации экрана, установку и удаление приложений, и что только еще не придумаете.

А что насчёт реальных девайсов?

Как работать с эмуляторами – мы разобрались. Теперь что касается реальных девайсов – всё что вам нужно, это подключить по USB ваше устройство, а затем… точно так же им управлять. Но! Дисклеймер!

NEW MISSION: REFUSE THIS MISSION!
NEW MISSION: REFUSE THIS MISSION!

Если вам важна именно ферма девайсов, а не всё то, что я выше рассказал – то вам будет быстрее и проще выбрать готовое решение типа того же плагина Device Farm для Appium, или даже STF. Поскольку там уже из коробки реализован удобный UI выбора девайсов, механизмы восстановления adb-сервера, если он прилёг, и другие приколы, которые вам предстоит пройти, если вы захотите написать всё своё. Ибо всё что я до этого рассказал, действительно просто, но вас будет ждать огромное количество нюансов. Но не расстраивайтесь заранее, потому что во-первых, вам никто не мешает стартануть, и итерационно, планомерно дорабатывать ваш новый инструмент. А во-вторых, я приготовил вам чуть-чуть мотивационных картинок – покажу, что в итоге получилось у нас.

Device Ranch – Ранчо

Я назвал нашу штуку – Ранчо, поскольку в одном месте были заботливо собраны фермы всех платформ, чтобы у ребят была единая точка входа в тестирование.

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

Сам интерфейс управления выглядит вот так – реализован мультитач, ввод текста с клавиатуры, смена ориентации дисплея девайса с подстройкой экрана под размер браузера, и так далее. Сам же UI фермы полностью адаптивен - это значит, что экран девайса всегда займёт максимально полезную площадь вашего экрана, какой бы он у вас ни был. Справа расположены менюшки с дополнительным функционалом - это логи, скриншоты, работа с приложением, и всё, что может понадобиться для тестирования.

Но можно не только управлять

Раз мы сделали маршрутизацию сессий тестов, почему бы их здесь же и не запускать? Запустить тесты может любой интересующийся, которому не нужно вникать в контекст UI-тестов. Поэтому у нас тут есть ободряющий лендинг, а далее мы задаём ему три вопроса – какой пак тестов он хочет запустить, на каких платформах, и на чём – на эмуляторах или девайсах:

После запуска тестов, нас перекидывает на окно результатов – там в реальном времени обновляются данные. Сделано это потому, что у многих нет либо доступов, либо навыка работы с нашей TMS (системой менеджмента тестов). Поэтому посмотреть их результаты можно сразу в Ранчо, что очень удобно:

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

Итого

И так, давайте немножко закругляться и подведём итоги того, что вы сегодня узнали:

  • Переезд с Х86 на ARM оправдан технически и экономически. Он даст вам в два раза больше потоков при схожей цене, решит возможные проблемы с разным поведением приложений, и избавит от проблем сборки и обновления докер-образов.

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

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

Хочу попробовать

И это прекрасно! Исходный код AVD Manager'a лежит на моём github – скачивайте и пробуйте! Не стесняйтесь писать в комментариях о своих вариантах доработок кода, которые я не учёл в статье.

Спасибо, что осилили – и новых вам эмоций в тестировании ;)

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