Маленький, но очень полезный патч в Selenium

в 14:04, , рубрики: java, selenium grid, автоматизация тестирования, Блог компании Badoo, Тестирование IT-систем, Тестирование веб-сервисов, Тестирование мобильных приложений

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

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

дай ноду

На тот момент максимальное количество нод, разделенных между Firefox, Chrome, Internet Explorer и PhantomJS, было около 200. Один из вариантов решения проблемы, который мне пришел в голову — это отслеживать количество свободных нод перед запуском теста и «придерживать» тесты в методе setup(), пока свободных нод недостаточно.

В описаниях изменений Selenium в свое время проскакивал функционал получения информации от grid с помощью HTTP-запросов. Доступные команды можно посмотреть прямо в коде сервлета HubStatusServlet.java. Их всего три: configuration (конфигурация), slotCounts (количество слотов) и newSessionRequestCount (количество сессий в очереди на получение браузера).

Формат запроса достаточно хитрый: это GET request, но с телом. Для экспериментов воспользуемся cURL и проверим, что возвращают эти команды:

$ curl -XGET http://selenium1:5555/grid/api/hub -d '{"configuration":[]}'

{
    'success': true,
    'port': '5555',
    'hubConfig': '/usr/local/selenium-rc/grid.json',
    'host': 'selenium1.d3',
    'servlets': 'org.openqa.grid.web.servlet.HubStatusServlet',
    'cleanUpCycle': 5000,
    'browserTimeout': 120000,
    'newSessionWaitTimeout': 30000,
    'capabilityMatcher': 'org.openqa.grid.internal.utils.DefaultCapabilityMatcher',
    'prioritizer': null,
    'throwOnCapabilityNotPresent': true,
    'nodePolling': 5000,
    'maxSession': 5,
    'role': 'hub',
    'jettyMaxThreads': - 1,
    'timeout': 90000
}

$ curl -XGET http://selenium1:5555/grid/api/hub -d '{"configuration":["slotCounts"]}'

{
    'success': true,
    'slotCounts': {
        'free': 50,
        'total': 196
    }
}

curl -XGET http://selenium1:5555/grid/api/hub -d '{"configuration":["newSessionRequestCount"]}'

{
    'success': true,
    'newSessionRequestCount': 3
}

У нас все тесты для Selenium написаны на PHP, в нем подобный запрос будет выглядеть так:

<?php

$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, 'http://selenium1:5555/grid/api/hub');
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($curl, CURLOPT_POSTFIELDS, '{"configuration":["slotCounts"]}');
curl_exec($curl);

В принципе, запрашивая в setUp()-методе тестов общее количество слотов и количество ждущих сессий, можно начинать ждать. Но это не очень удобно в том случае, если у вас неравномерно выделены ресурсы на разные браузеры. Например, у нас количество нод для Firefox примерно на треть больше, чем Google Chrome. А Internet Explorer и MS Edge занимают всего около 10 нод (и то они могут делиться по версиям). Получается, что свободных нод именно для Chrome может уже и не быть, хотя Selenium Grid говорит, что свободные ноды еще есть.

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

diff --git a/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java b/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java
index 8b9c578..550c5db 100644
--- a/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java
+++ b/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java
@@ -29,10 +29,12 @@
 import org.openqa.grid.internal.Registry;
 import org.openqa.grid.internal.RemoteProxy;
 import org.openqa.grid.internal.TestSlot;
+import org.openqa.selenium.remote.CapabilityType;

 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -128,6 +130,11 @@ private JsonObject getResponse(HttpServletRequest request) throws IOException {
           paramsToReturn.remove("slotCounts");
         }

+        if (paramsToReturn.contains("browserSlotsCount")) {
+          res.add("browserSlotsCount", getBrowserSlotsCount());
+          paramsToReturn.remove("browserSlotsCount");
+        }
+
         for (String key : paramsToReturn) {
           Object value = allParams.get(key);
           if (value == null) {
@@ -169,6 +176,53 @@ private JsonObject getSlotCounts() {
     return result;
   }

+  private JsonObject getBrowserSlotsCount() {
+    int freeSlots = 0;
+    int totalSlots = 0;
+
+    Map<String, Integer> freeBrowserSlots = new HashMap<>();
+    Map<String, Integer> totalBrowserSlots = new HashMap<>();
+
+    for (RemoteProxy proxy : getRegistry().getAllProxies()) {
+      for (TestSlot slot : proxy.getTestSlots()) {
+        String
+          slot_browser_name =
+          slot.getCapabilities().get(CapabilityType.BROWSER_NAME).toString().toUpperCase();
+        if (slot.getSession() == null) {
+          if (freeBrowserSlots.containsKey(slot_browser_name)) {
+            freeBrowserSlots.put(slot_browser_name, freeBrowserSlots.get(slot_browser_name) + 1);
+          } else {
+            freeBrowserSlots.put(slot_browser_name, 1);
+          }
+          freeSlots += 1;
+        }
+        if (totalBrowserSlots.containsKey(slot_browser_name)) {
+          totalBrowserSlots.put(slot_browser_name, totalBrowserSlots.get(slot_browser_name) + 1);
+        } else {
+          totalBrowserSlots.put(slot_browser_name, 1);
+        }
+        totalSlots += 1;
+      }
+    }
+
+    JsonObject result = new JsonObject();
+
+    for (String str : totalBrowserSlots.keySet()) {
+      JsonObject browser = new JsonObject();
+      browser.addProperty("total", totalBrowserSlots.get(str));
+      if (freeBrowserSlots.containsKey(str)) {
+        browser.addProperty("free", freeBrowserSlots.get(str));
+      } else {
+        browser.addProperty("free", 0);
+      }
+      result.add(str, browser);
+    }
+
+    result.addProperty("total", totalSlots);
+    result.addProperty("total_free", freeSlots);
+    return result;
+  }
+
   private JsonObject getRequestJSON(HttpServletRequest request) throws IOException {
     JsonObject requestJSON = null;
     BufferedReader rd = new BufferedReader(new InputStreamReader(request.getInputStream()));

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

Накладываем патч на локальную копию исходников Selenium, собираем собственную сборку Selenium-grid (тут есть подробная инструкция по сборке). Если нет желания возиться со сборкой, можете попробовать уже собранное мной: https://github.com/leipreachan/misc_scripts/tree/master/blob/selenium

Теперь перезапускаем selenium-grid и смотрим, какие значения он возвращает:

curl -XGET http://selenium1:5555/grid/api/hub -d '{"configuration":["browserSlotsCount"]}'

и результат:

{
    'success': true,
    'browserSlotsCount': {
        'IEXPLORER': {
            'total': 4,
            'free': 3
        },
        'FIREFOX': {
            'total': 95,
            'free': 50
        },
        'MICROSOFTEDGE': {
            'total': 1,
            'free': 1
        },
        'PHANTOMJS': {
            'total': 20,
            'free': 20
        },
        'CHROME': {
            'total': 76,
            'free': 75
        },
        'total': 196,
        'total_free': 149
    }
}

Итак, теперь мы знаем, какие свободные браузеры и в каком количестве у нас представлены в Selenium Grid. Осталось немного поправить метод setup() (или аналогичный):

  • реализовать проверку на количество свободных нод;
  • в этой проверке добавить небольшой период ожидания (например, две минуты) перед тем, как тест упадёт с таймаутом;
  • не забыть, что не надо запрашивать эти параметры каждую секунду :)

Лично для нас это стало выглядеть так, что selenium-тесты в час пик идут немного медленнее, зато гораздо, гораздо стабильнее. Учитывая, что у нас несколько сотен тестов запускаются автоматически, это существенно упростило жизнь всем, кто с связан с тестированием.


Артём Солдаткин
Lead QA Engineer, Badoo

Автор: Badoo

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js