Ключ к качественному и постоянному тестированию - автоматизация, которая позволяет проводить тесты множество раз за день одновременно на нескольких устройствах с разными процессорами и архитектурами.
Для реализации этой автоматизации мы используем несколько Jenkins Declarative Pipeline'ов и обширный набор инструментов, о которых рассказывается в статье.
Тестовый стенд и удаленный доступ
Так как наша ОС (ЗОСРВ "Нейтрино") поддерживает большое количество процессорных платформ, тестирование необходимо проводить на как можно большем количестве устройств. Для этого в отдельном помещении собран тестовый стенд. К стенду обеспечен полный удаленный доступ: все устройства подключены через RS-232 (или USB, если COM-порт отсутствует) к стоящему рядом с ними компьютеру под управлением Ubuntu, к которому можно подключиться, например, по ssh, и использовать minicom для полного доступа.
Каждое устройство также доступно по сети, к ним можно подключаться через ssh или telnet, и для контроля происходящего на них направлены камеры, например:
Для жесткой перезагрузки используется блок управляемых розеток, через который запитаны все устройства.
Функционирование тестового стенда и тестирование в целом состоят из множества элементов. Общая схема их взаимодействия может быть представлена так:
В данной схеме упоминаются загрузочный и файловый сервера. Первый хранит загрузочные образы устройств: каждое устройство загружается по сети и берет свой образ для загрузки с этого сервера. На файловом сервере располагаются архивы с компонентами ОС.
Обновление устройств
Для тестирования важно иметь одно и то же окружение. Полагается, что на устройстве нет никаких сторонних пакетов, приложений, неверных путей и прочего, а также там стоит последняя версия ОС. Для гарантированного получения этого окружения в Jenkins реализована полуавтоматическое обновление. Полуавтоматическим оно здесь названо потому, что запускается в двух случаях - по таймеру/по завершению другого конвейера и вручную.
Каждую ночь происходит пересборка ОС, и после этого устройства стенда обновляются. Ручное обновление оставлено на случай, если захочется провести какие-то тесты вне тестовой системы, но без обновления уверенность в том, что ОС действительно можно доверять, отсутствует: кто-то из коллег вполне мог подключиться к одному из устройств, пошаманить там с заменой каких-то драйверов, например, и не прибраться за собой.
Обновление ОС на target-устройствах реализовано следующим образом:
Обновление можно запускать через любой из двух конвейеров в зависимости от желаемого количества параметров: основной конвейер более общий и необходим для удобного обновления нескольких устройств сразу. При необходимости выбрать одно устройство из тех, что не входят в перечень регулярно используемых, или настроить обновление более гибко, можно запустить дочерний конвейер. Тут сразу становится очевиден недостаток такого запуска: отсутствие синхронизации доступа. Это в планах на исправление :)
Основной конвейер имеет несколько параметров, которые можно выбрать при запуске: версию ОС, опциональное обновление докер-контейнера для сборки загрузочного образа, сборочный сервер и нужные устройства. Также можно просто перезагрузить устройство, игнорируя другие действия. Эта опция осталась со времен, когда блока розеток не было. Сейчас перезагрузка через конвейер используется крайне редко.
Дочерний конвейер имеет больше параметров, но при его запуске можно выбрать только одно устройство. Зато можно указать дополнительные пакеты для развертывания или ветку в репозитории с BSP девайса или тестами для включения в образ. Также среди его параметров есть версия сборки ОС: естественно, обычно проверяется последняя, но иногда нужно откатиться на более старый билд.
Все конвейеры разделены на несколько этапов, причем Upgrading: <Устройство> выполняются параллельно.
В дочернем конвейере все этапы выполняются последовательно. Их названия говорят сами за себя и не требуют пояснений.
Для синхронизации доступа к устройствам используется Lockable Resource Plugin, в блок которого заносится вызов дочернего конвейера. Таким образом запрещается доступ к девайсу из других джобов, пока обновление не завершится:
lock( "lock_testing_device_${DEVICE}" ) {
def result = build job: 'upgrade',
parameters: [extendedChoice(name: 'RTOS', value: params.RTOS),
extendedChoice(name: 'SERVER', value: params.SERVER),
extendedChoice(name: 'DEVICE', value: "${DEVICE}"),
extendedChoice(name: 'SHUTDOWN_ONLY', value: "${SHUTDOWN_ONLY}"),
extendedChoice(name: 'PACKAGES', value: "picocom,Python"),
booleanParam(name: 'PULL_IMAGE', value: params.PULL_IMAGE)]
}
Также написана дополнительная библиотека т.н. "профилей устройств", куда заносится информация о каждом девайсе. Некоторые примеры инициализации параметров:
/* GIT */
dev[ 'sabre' ][ "BSP" ] = "ssh://...BSP/bsp-kpda-imx6q-sabre.git" // URL
dev[ 'sabre' ][ "BRANCH" ] = "autotest" // Ветка
/* BSP */
dev[ 'sabre' ][ "BOOTIMAGE" ] = "ifs-mx6q.raw" // Загрузочный образ устройства
dev[ 'sabre' ][ "MAKECMD" ] = "make all" // Команда сборки BSP
dev[ 'sabre' ][ "BUILDFILE" ] = "sabresm.build" // Build-файл образа
/* Профиль доступа по SSH */
dev[ 'sabre' ][ "SSH" ] = "tst_armv7" // Jenkins-credential
dev[ 'sabre' ][ "TIMEOUT" ] = 60 // Таймаут перезагрузки
/* Особенности аппаратуры */
dev[ 'sabre' ][ "HAS_SMP" ] = "4" // Число процессоров
dev[ 'sabre' ][ "HAS_DC" ] = "y" // Наличие контроллера дисплеев
dev[ 'sabre' ][ "HAS_SCREEN" ] = "y" // Наличие композитной графики
dev[ 'sabre' ][ "HAS_2D" ] = "y" // Наличие аппаратного 2D
dev[ 'sabre' ][ "HAS_3D" ] = "y" // Наличие аппаратного 3D
dev[ 'sabre' ][ "HAS_OPENCL" ] = "y" // Наличие GPGPU
dev[ 'sabre' ][ "HAS_PYTHON" ] = "y" // Наличие порта Python
dev[ 'sabre' ][ "HAS_2ndHDD" ] = "/dev/hd0" // Наличие тестового HDD/SDD
dev[ 'sabre' ][ "HAS_NET_IF" ] = "fec0" // Имя сетевого интерфейса
dev[ 'sabre' ][ "HAS_USB" ] = "y" // Наличие тестового USB
...
Эта библиотека используется при обновлении устройств, а все параметры из профиля доступны тестам через переменные окружения.
Тестирование
Само автоматизированное тестирование аналогично обновлению с его двухконвейерной структурой:
Внимательный читатель здесь заметит упоминание обновления.
Среди параметров тестирования есть галочка опционального обновления. При ее выборе получается более интересная схема.
Возвращаясь к тестированию: здесь также используется Lockable Resource Plugin, в блок которого обернут вызов дочернего конвейера. Аналогичной обновлению проблемы с отсутствием синхронизации при запуске второго конвейера отдельно от первого нет, т.к. такой сценарий запуска не разрешен.
Вид основного конвейера, где Test: <Устройство> и Analysys: docs/disr (анализ документации и составление таблицы, где сопоставляются бинарники дистрибутива и теста) выполняются параллельно.
Вид дочернего конвейера:
Во время тестирования устройства используют дополнительные переменные из упомянутой ранее библиотеки профилей, которые экспортируются перед запуском тестов. Сюда входят в основном названия вторых дисков/сетевых интерфейсов и проч.
Сами тесты пишутся на C, Shell и Python. Выбор одного из них производится на основе здравого смысла:
Если нужен какой-то пользовательских ввод для утилит командной строки в автоматизированном режиме - выбираем Python.
Если проверяются функции языка Си - логично, что нужно выбрать Си.
Если проверяется работа с командной строкой - выбираем Shell (В нашем случае используется ksh).
Тесты или пишутся с нуля, или портируются извне, в зависимости от конкретной задачи.
Все тесты собираются с помощью рекурсивной сборочной системы, о которой можно почитать тут. Она довольно проста и удобна в использовании. Иерархия тестов в соответствующем репозитории выглядит так:
Так как операционная система - очень обширное понятие и тестировать нужно много всего и сразу, для логического разделения используются модули и сабмодули.
В корне репозитория также находится директория tools, куда положены все требуемые тестам библиотеки (в основном туда вынесены функции работы с памятью, получения размера свободной памяти, создания пользователей без взаимодействия с соответствующей утилитой и т.д. - все то, что нужно во многих тестах).
После сборки тестов иерархия репозитория сохраняется. Во время работы конвейера тестирования собранные тесты запаковываются в архив, передаются на устройство с помощью sftp и распаковываются от корня.
Для вызова тестов используется шелл-скрипт, который также перенаправляет их вывод в лог-файлы. После завершения работы тестов формируется общий отчет с синтаксисом нашей системы генерации документации, куда в том числе попадают и записанные логи. Из этого отчета затем формируется pdf-файл.
На странице конвейера для скачивания доступны артефакты:
- Логи сборки тестов,
- Общий отчет в формате pdf,
- Исходник отчета для генерации pdf.
Структура pdf-отчета при выполнении части тестов имеет вид:
- Содержание
- Сводная таблица с сопоставлением бинарника/библиотеки из состава ОС с ее тестом (составляется, если вызваны все тесты и нам нужно не просто собрать результаты, а еще и провести анализ покрытия),
- Сводная таблица с результатами,
- Блок логов.
В отчете реализована навигация с помощью якорных ссылок, которые также поддерживаются системой генерации документации. Ссылки содержания ведут на соответствующие параграфы, а строки таблицы - на нужные логи. Пример нескольких страниц отчета:
В качестве заключения
Естественно, главная цель всего происходящего - сама автоматизация, а Jenkins - просто инструмент реализации на пути к этому.
Централизованное и автоматизированное тестирование привносит больше плюсов, чем минусов: тесты проводятся регулярно, одновременно на всем стенде и каждый раз в одном и том же ожидаемом окружении.
Добавление тестов и устройств в стенд строго регламентируется, что ограничивает в творчестве, но позволяет добиться единообразия. Ну и очевидно, что автотесты - это просто быстрее, чем ручное тестирование.
К минусам же можно отнести необходимость обеспечивать безотказную постоянную работу всех элементов схемы тестирования, начиная от каждого устройства в стенде и заканчивая самим Jenkins'ом.