Немного предыстории


Мобильное приложение Badoo существует для основных «нативных» платформ (Android, iOS и Windows Phone) и для мобильного веба. Несмотря на то, что в разработке мы не используем никаких кроссплатформенных фрэймворков, подавляющая часть бизнес-логики в приложениях схожа, и чтобы не дублировать функциональные тесты для всех платформ, мы пишем кроссплатформенные тесты с помощью Cucumber, Calabash и Appium. Это позволяет нам выносить в общую часть и переиспользовать в тестах для всех платформ код, отвечающий за проверку этой самой бизнес-логики. Различной же остается лишь реализация взаимодействия с приложением (более подробно мы рассказывали об этом здесь).

Когда кроссплатформенная автоматизация только начиналась (на iOS и Android), было принято решение использовать в качестве серверов Mac Mini. Это позволило сделать каждую из 8 билд-машин универсальной: на ней можно было собирать и запускать функциональные и юнит-тесты как для приложений на iOS, так и на Android. Такое решение устраивало нас практически всем до тех пор, пока количество функциональных тестов не перевалило за пять сотен для каждой платформы, а прогоны не стали требовать все больше времени. Для того чтобы удержать время прогона в разумных границах, мы постоянно работаем над оптимизацией тестов, а также добавляем новые Android-устройства (для iOS мы добавляем симуляторы по-другому). Со временем у нас появились Mac Mini с более чем 8 смартфонами. Важно отметить, что мы подключаем устройства одной модели к одному серверу, чтобы прогоны тестов были консистентны на одном агенте.

По существу


У себя в Badoo мы решили перенести тестирование устройств Android на Linux-хосты — необходимое оборудование стоит дешевле, а кроме того, компьютеры Mac Mini, используемые для сборки, часто прерывают USB-подключения к устройствам Android, и те внезапно исчезают во время тестирования. Для управления серверами Linux мы в основном используем контейнеры Docker, поэтому решили попробовать создать контейнер для тестирования реальных устройств Android и клонировать его для каждой модели или группы телефонов, чтобы интегрировать контейнер в существующую конфигурацию серверов.

Небольшое замечание: одно из преимуществ Linux по сравнению с Mac заключается в том, что Linux — открытая система. Она показала нам, что причина таинственного исчезновения телефонов при тестировании кроется в разрывах соединений, длящихся доли секунды. Мы исправили тесты, добавив в них повторную попытку подключения, что в значительной степени решило проблему.

По существу: Docker


Docker — это система, которая содержит в себе методы сборки и распространения конфигураций ПО с инфраструктурой операционной системы, изолирующей каждый программный контейнер от остальных компонентов компьютера. Контейнер имеет собственную файловую систему, адресное пространство и пр. Контейнеры исполняются в одном экземпляре операционной системы, но поскольку система сильнее изолирует процессы, это все работает как набор виртуальных машин.

Поясняющие диаграммы, опубликованные на сайте Docker:

На хост-компьютере используется система виртуализации, в которой запущены гостевые экземпляры ОС:


Контейнеры Docker выполняются на одной ОС:



По существу: группировка adb/adbd


Каждый контейнер должен был управлять собственным набором телефонов. Чтобы реализовать это наиболее естественным способом, нужно сопоставить группы разъемов USB разным контейнерам. Устройства, подключенные к разъемам на передней панели компьютера, появляются в каталоге /dev/bus/usb/001, который доступен контейнеру 1; устройства, подключенные к разъемам на задней панели, появляются в каталоге /dev/bus/usb/002, который доступен контейнеру 2. Чтобы увеличить количество подключаемых устройств, была заказана дополнительная плата расширения.
Все это выглядит неплохо, однако команда adb взаимодействует с телефоном через демон, который использует порт по умолчанию 5037 и работает на уровне всего компьютера. Это означает, что первый контейнер, в котором выполняется команда adb, запускает и демон adb (adbd) — в результате остальные контейнеры, подключаемые к этому демону, видят телефоны первого контейнера. Эту проблему можно было бы решить с помощью сетевых возможностей Docker (каждый контейнер получает собственный IP-адрес, а, следовательно, и собственный набор портов), однако мы воспользовались другим механизмом. Для каждого контейнера было присвоено отдельное значение переменной окружения ANDROID_ADB_SERVER_PORT. Мы выделили порт каждому контейнеру, чтобы он запускал собственный демон adb, который видит только телефоны этого контейнера.

В процессе разработки мы поняли, что нельзя выполнять команду adb на уровне хоста, не задав переменную ANDROID_ADB_SERVER_PORT, поскольку демон adbd уровня хоста, способный видеть все порты USB, «крадет» телефоны у контейнеров Docker (телефоны могут взаимодействовать только с одним демоном adbd в каждый момент времени).
Если бы мы использовали только эмуляторы, можно было бы обойтись отдельными процессами adbd, но поскольку мы работаем с реальными устройствами…

По существу: обновление контейнеров при горячем подключении устройств USB


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

Отслеживать добавление и удаление телефонов на хост-компьютере можно по файлам в каталоге /dev/bus/usb, в котором система создает и удаляет файлы, соответствующие телефонам:

while sleep 3; do
  find /dev/bus/usb > /tmp/a
  diff /tmp/a /tmp/b
  mv /tmp/a /tmp/b
 done

К сожалению, в контейнерах Docker телефоны не только не создаются и не удаляются подобным образом; если настроить создание и удаление узлов, то они на самом деле не взаимодействуют с телефонами!

Мы решили этот вопрос «в лоб»: поместили контейнеры в режим --privileged и открыли им доступ ко всему каталогу /dev/bus/usb.

Теперь понадобился другой механизм распределения телефонов по интерфейсным шинам. Я скачал исходный код Android и внес небольшие изменения в файл platform/system/core/adb/usb_linux.cpp

    std::string bus_name = base + "/" + de->d_name;

+    const char* filter = getenv("ADB_DEV_BUS_USB");
+    if (filter && *filter && strcmp(filter, bus_name.c_str())) continue;

    std::unique_ptr<DIR, int(*)(DIR*)> dev_dir(opendir(bus_name.c_str()), closedir);
    if (!dev_dir) continue;

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

Отступление: хотя исправление было совсем несложным, сборку adb пришлось выполнять методом проб и ошибок, поскольку большинство людей включает в сборку все подряд. Мое окончательное решение выглядело так (в чувствительной к регистру файловой системе — я работаю на Mac):

cd src/android-src
source build/envsetup.sh
lunch 6
vi system/core/adb/usb_linux.cpp
JAVA_NOT_REQUIRED=true make adb
out/host/linux-x86/bin/adb

По существу: мультиплексирование портов USB


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

Поскольку я уже влез в код adb, то решил просто добавить еще одну переменную окружения: переменная ADB_VID_PID_FILTER получает список пар идентификаторов vid:pid, и adb игнорирует любые несоответствующие устройства.

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

diff --git a/adb/usb_linux.cpp b/adb/usb_linux.cpp
index 500898a..92e15ca 100644
--- a/adb/usb_linux.cpp
+++ b/adb/usb_linux.cpp
@@ -115,6 +115,71 @@ static inline bool contains_non_digit(const char* name) {
     return false;
 }

+static int iterate_numbers(const char* list, int* rejects) {
+  const char* p = list;
+  char* end;
+  int count = 0;
+  while(true) {
+    long value = strtol(p, &end, 16);
+//printf("%d, %p ... %p (%c) = %ld (...%s)\n", count, p, end, *end, value, p);
+    if (p == end) return count;
+    p = end + 1;
+    count++;
+    if (rejects) rejects[count] = value;
+    if (!*end || !*p) return count;
+  }
+}
+
+int* compute_reject_filter() {
+    char* filter = getenv("ADB_VID_PID_FILTER");
+    if (!filter || !*filter) {
+        filter = getenv("HOME");
+        if (filter) {
+            const char* suffix = "/.android/vidpid.filter";
+            filter = (char*) malloc(strlen(filter) + strlen(suffix) + 1);
+            *filter = 0;
+            strcat(filter, getenv("HOME"));
+            strcat(filter, suffix);
+        }
+    }
+    if (!filter || !*filter) {
+        return (int*) calloc(sizeof(int), 1);
+    }
+    if (*filter == '.' || *filter == '/') {
+        FILE *f = fopen(filter, "r");
+        if (!f) {
+            if (getenv("ADB_VID_PID_FILTER")) {
+                // Only report failure for non-default value
+                printf("Unable to open file '%s'\n", filter);
+            }
+            return (int*) calloc(sizeof(int), 1);
+        }
+        fseek(f, 0, SEEK_END);
+        long fsize = ftell(f);
+        fseek(f, 0, SEEK_SET);  //same as rewind(f);
+        filter = (char*) malloc(fsize + 1);  // Yes, it's a leak.
+        fsize = fread(filter, 1, fsize, f);
+        fclose(f);
+        filter[fsize] = 0;
+    }
+    int count = iterate_numbers(filter, 0);
+    if (count % 2) printf("WARNING: ADB_VID_PID_FILTER contained %d items\n", count);
+    int* rejects = (int*)malloc((count + 1) * sizeof(int));
+    *rejects = count;
+    iterate_numbers(filter, rejects);
+    return rejects;
+}
+
+static int* rejects = 0;
+static bool reject_this_device(int vid, int pid) {
+    if (!*rejects) return false;
+    for ( int len = *rejects; len > 0; len -= 2 ) {
+//printf("%4x:%4x vs %4x:%4x\n", vid, pid, rejects[len - 1], rejects[len]);
+        if ( vid == rejects[len - 1] && pid == rejects[len] ) return false;
+    }
+    return true;
+}
+
 static void find_usb_device(const std::string& base,
         void (*register_device_callback)
                 (const char*, const char*, unsigned char, unsigned char, int, int, unsigned))
@@ -127,6 +192,8 @@ static void find_usb_device(const std::string& base,
         if (contains_non_digit(de->d_name)) continue;

         std::string bus_name = base + "/" + de->d_name;
+        const char* filter = getenv("ADB_DEV_BUS_USB");
+        if (filter && *filter && strcmp(filter, bus_name.c_str())) continue;

         std::unique_ptr<DIR, int(*)(DIR*)> dev_dir(opendir(bus_name.c_str()), closedir);
         if (!dev_dir) continue;
@@ -176,6 +243,12 @@ static void find_usb_device(const std::string& base,
             pid = device->idProduct;
             DBGX("[ %s is V:%04x P:%04x ]\n", dev_name.c_str(), vid, pid);

+            if(reject_this_device(vid, pid)) {
+                D("usb_config_vid_pid_reject");
+                unix_close(fd);
+                continue;
+            }
+
                 // should have config descriptor next
             config = (struct usb_config_descriptor *)bufptr;
             bufptr += USB_DT_CONFIG_SIZE;
@@ -574,6 +647,7 @@ static void register_device(const char* dev_name, const char* dev_path,
 static void device_poll_thread(void*) {
     adb_thread_setname("device poll");
     D("Created device thread");
+    rejects = compute_reject_filter();
     while (true) {
         // TODO: Use inotify.
         find_usb_device("/dev/bus/usb", register_device);

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

Tim Baverstock,
QA automation engineer
Поделиться с друзьями
-->

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


  1. Nomad1
    03.09.2016 11:03

    А нельзя взять прошивку с реального телефона, сделать из нее образ ВМ и гонять через QEMU? Ясное дело, что целиком аппаратную часть эмулировать не выйдет, но при некоторых усилиях система останется работоспособной (замена libEGL на софтварную, обход или перекомпиляция падающих pixelflinger и пр.).


    1. bayandin
      05.09.2016 19:46

      Мы не пробовали, но я думаю, в целом можно, только вот силы и время потраченные на то, чтобы сделать её рабочей могут оказаться несовоставимо велики.
      В качестве эмулятора андроида мы сейчас активно исследуем Genymotion. Прошлый раз пробовали запускать на нём функциональные тесты около полутора лет назад и тогда он показал себя не случшей стороны — было много крашей и не совсем адекватного поведения в целом. Сейчас же ситуация с ним стала гораздо лучше. Поэтому с большой вероятностью мы переведём (существенную) часть прогонов тестов на него.