Я являюсь участником проекта по разработке ОСРВ Embox для встроенных систем. Чаще всего ОС для встроенных систем поддерживает множество аппаратных платформ, и мы не исключение. Также в проекте имеется множество сервисов и библиотек: ssh, telnet, Qt и т.д. Все эти сервисы и библиотеки хотелось бы иметь в рабочем состоянии на различных платформах.
Я хорошо помню то время, когда именно мне приходилось поддерживать в рабочем состоянии Qt. Это был ужас! Вот я пришел днем на работу, что-то опять сломано. Начинаю разбираться. Оказывается, что кто-то пофиксил багу в сетевом стеке и теперь Qt не может создать сокет. Короче говоря, Qt ломалось практически ежедневно и по самым неожиданным причинам.
Естественно, напрашивалось решение внедрить в проект некоторое автоматизированное тестирование различных сервисов. В чем же проблема сделать сервер, который будет все это тестировать?
Основная проблема заключается в специфике встроенных систем. А именно, в отличие от систем общего назначения, тестам приходится выполняться в среде со специфической аппаратной поддержкой. Например, у них мало памяти, и поставить средство интеграционного тестирования внутрь такой железки не представляется возможным. То есть нужно тестировать «снаружи». Итак, давайте ближе к делу.
Сборка и запуск
Как я уже говорил мы поддерживаем несколько архитектур. Поэтому, чтобы поддерживать проект в рабочем состоянии, прежде всего, необходимо собирать его под различные платформы. Для этой цели мы используем компилятор gcc, который как известно умеет генерировать код для разных архитектур. Но делать это в ручную конечно не стоит. К счастью, для решения проблемы автоматизации сборки существует множество различных средств под общим названием Continuous Integration — Jenkins/Hudson, Integrity, Buildbot и пр. Берем один из этих тулов, и настраиваем его так, чтобы он собирал проект под различные аппаратные архитектуры по мере поступления новых коммитов в репозиторий. Мы используем Buildbot. Когда какая-то конфигурация не собирается, это отмечается на билдсервере. На самом деле еще можно автоматически посылать гневные письма тому кто сломал, но мы в основном находимся в одной комнате и справляемся посредством голосовой связи по воздуху.
Следующей проблемой, вытекающей опять же из за кросс-платформенности проекта, является запуск на целевой платформе или хотя бы архитектуре. Тут нам на помощь пришел очередной проект с открытым кодом QEMU, он поддерживает все имеющиеся у нас в наличие процессорные архитектуры и довольно широкий перечень периферии.
Лирическое отступление. Изначально в QEMU для процессора Leon 3 была бага, и мы пользовались эмулятором tsim. Но хотелось единообразия как минимум чтобы наладить автоматизированное тестирование. И поскольку проект QEMU открытый, мы поправили его исходники для поддержки процессора Leon3. В более поздних версиях наши правки были внесены в исправлены.
QEMU позволяет при запуске задавать различные параметры (размер доступной физической памяти, используемые сетевая и видео карты, подключаемый диск и т. д.) и нам традиционно не хотелось это делать вручную, мы создали скрипт который анализирует конфигурацию нашего проекта и запускает эмулятор с нужными параметрами.
Unit тестирование
Как всем наверное известно тестирование бывает разных типов: Unit, регрессионное, интеграционное и другие. Unit-тестирование применяется самими разработчиками и позволяет оперативно проверить не сломалась ли функциональность после внесения изменений.
Достаточно на ранней стадии развития проекта мы поняли, что далеко не уедем без этого типа тестирования. И мы разработали, естественно после исследования существующих решений, небольшой легковесный фреймворк для Unit-тестирования на языке С. googletestКогда мы изучали существующие фреймворки, наибоый синтаксис, на наш взгляд, был у фреймворка googletest. К сожалению, этот фреймворк для языка C++, а нам хотелось иметь возможность писать именно Си-шные тесты, поэтому придерживаясь похожего синтаксиса, мы разработали аналог для Си.
Этот фреймворк получился очень удачным, ведь с одной стороны он фактически ничего не требует от платформы, на которой вызывается, единственное, это функции setjmp и longjmp для используемой архитектуры, а с другой стороны ― имеет удобный синтаксис. Из за маленьких требований данный фреймворк легко можно применять на самых ранних стадиях разработки встроенных систем при фактически полном отсутствии работоспособного окружения. Поэтому мы достаточно активно используем тесты не только для разрабатываемых программных модулей, но и для тестирования аппаратной составляющей, например, аппаратных таймеров, а также проверки системных функций, например, создания потока в ядре.
Пример проверки работоспособности функций создания и выполнения потока:
TEST_CASE("thread_create should return -EINVAL if thread function is NULL") {
struct thread *t;
t = thread_create(0, NULL, NULL);
test_assert_equal(err(t), -EINVAL);
}
static void test_timer_handler(sys_timer_t* timer, void *param) {
*((int *) param) = 1;
}
TEST_CASE("testing timer_set function") {
unsigned long i;
sys_timer_t * timer;
volatile int tick_happened;
/* Timer value changing means ok */
tick_happened = 0;
if (timer_set(&timer, TIMER_ONESHOT, TEST_TIMER_PERIOD, test_timer_handler,
(void *) &tick_happened)) {
test_fail("failed to install timer");
}
i = -1;
while (i-- && !tick_happened) {
}
timer_close(timer);
test_assert(tick_happened);
}
Я не буду сильно вдаваться в описание нашего фреймворка, это выходит за рамки данного топика, к тому же я только использовал его, а разрабатывал abusalimov, но если кому-то будет интересно, об этом можно будет написать отдельную статью. Я только отмечу, что включенные тесты у нас запускаются прямо при старте в автоматическом режиме.
Интеграционное тестирование
После выполнения модульных тестов переходим к интеграционным. Как уже говорилось выше, выполнять интеграционные тесты внутри встроенной системы довольно проблематично. Начал разбираться какой бы инструмент начать использовать для тестирования снаружи. Исследовал несколько фреймворков для интеграционного тестирования — TETware RT, OpenTest, tcltest, autotestnet, DejaGnu. Выяснились следующие проблемы:
- Некоторые инструменты тулы требуют unix окружение — утилиты grep, awk и тд. Иными словами, что-то типа Linux. Проблема в том, что не на любую плату все это влезет (TETware RT, OpenTest);
- Отсутствие встроенных средств для проверки вывода и результата исполнения тестируемой программы (DejaGnu);
- Отсутствие средств для взаимодействия со встраиваемой системой, то есть нет возможности легко проверить, что результат выполнения программы на удаленной системе совпадает с ожидаемым (TclTest и его улучшения — PTL, TTXN, New Test Package).
В то время я уже был знаком с googletest для С++, и мне нравился их синтаксис для написания модульных тестов. Тесты пишутся в месте их объявления и нигде отдельно не регистрируются, имеется множество assert’ов, есть возможность определять обработчики, которые вызываются до и после выполнения каждого теста. И я подумал, а почему бы не перенести это удобство в сферу интеграционного тестирования для встроенных систем?
Опираясь на знания о недостатках существующих решений, я поставил себе задачу максимально совместить легковесность DejaGnu, мощь TETware RT и эстетичность googletest.
В качестве технологий для разработки я выбрал язык Tcl c расширением Expect для автоматизации тестирования. Expect позволяет достаточно просто описать процесс подключения ко встраиваемой системе, посылать команды и обрабатывать результат.
Схематично принцип работы фреймворка можно описать, как показано ниже. БОльшая его часть работает на хосте, а по сети посылаются команды, которые исполняются на интерпретаторе встроенной системы, после чего результат передается обратно и анализируется на хосте.
Hello embedded tester!
Давайте рассмотрим как выглядит простейший тест.
package require autotest
namespace import autotest::*
TEST_CASE {echo “hello” print hello} {
test_assert_regex_equal “echo “hello”" hello
}
Первые две строчки означают то, что мы подключаем реализованную мной библиотеку autotest, которая представляет собой package в Tcl. Далее объявляется тест кейз. Суть теста проста — встраиваемая система должна правильно исполнить команду echo “hello”, напечатав hello. Давайте разберемся как он работает.
Сразу отмечу, что все тесты запускаются только на хостовой системе, а со встроенной мы взаимодействуем по TELNET. Процедура TEST_CASE сначала устанавливает соединение со встраиваемой системой. Конечно же для этого внутри встроенной системы должен быть предварительно запущен сервис telnetd. То есть схема такая — запускаем QEMU с подготовленным образом нашей ОС, и ждем пока запустится telnetd. Далее можно подключиться и запускать тесты.
Итак, после того как соединение установлено исполняется строчка test_assert_regexp_equal “echo “hello”" hello. В ней посылается команда “echo “hello”” встроенной системе. Встроенная система исполняет команду и весь ее вывод автоматически попадает на хост (это же telnet!). Хост в свою очередь получает результат и сравнивает его со строкой “hello”.
Обзор возможностей фреймворка
Фреймворк содержит набор библиотечных процедур test_assert_*. Все они работают по одному сценарию — послать команду с хоста встраиваемой системе, а затем получить вывод команды и сравнить его с ожидаемым. Ниже приведен код процедуры test_assert_regex_equal из примера выше:
proc test_assert_regexp_equal {cmd success_output} {
send $cmd
set cmd_name [lindex [split $cmd " "] 0]
expect {
timeout { puts "$cmd_name timeoutn"; exit 1 }
-regexp "$cmd_name:.*" { puts "$expect_out(buffer)n"; exit 1 }
"$success_output" { }
}
}
Если одна из процедур test_assert_* завершилась по exit 1, то выводится информация о том, что тест не пройден (печатается имя файла и номер строки) и управление передается следующему набору тест кейзов.
Проникшись удобством googletest я реализовал процедуры TEST_SETUP и TEST_SUITE_SETUP (аналогично для TEARDOWN). Хочу обратить внимание на одну особенность. Эти процедуры существуют как для хоста так и для встроенной системы — TEST_SETUP_HOST и TEST_SETUP_TARGET. Вся разница в том, что первая из них будет исполняться на хосте, а вторая на встроенной системе.
Пошаговая отладка тестов
Давайте на минуту представим, что наш “Hello embedded tester!” тест не прошел. То есть при его запуске фреймворк выдал ошибку, что в строке 5 не сработала проверка test_assert_regex_equal. Хочется выяснить, что в нашей встроенной системе пошло не так. Для этого давайте модифицируем наш тест следующим образом, добавив символ “b” в конец строки, на которой произошла ошибка.
package require autotest
namespace import autotest::*
TEST_CASE {echo “hello” print hello} {
test_assert_regex_equal “echo “hello”" hello b
}
Теперь тест будет исполняться до строки с символом «b». Это означает, что сразу после входа в процедуру она сообщает об этом, печатая номер строки, название файла с точкой останова и саму строку, а затем входит в режим ожидания нажатия клавиши ввода. Иными словами, это такой своеобразный брейкпоинт. Теперь когда тест находится в режиме ожидания можно поставить брейкпоинт уже внутри встроенной системы и после этого продолжить исполнение теста.
Кстати говоря, стандартные средства отладки в Tcl не позволяют ставить точки останова на произвольную строку внутри процедуры, и предлагается обходиться отладочной печатью.
Что исполнять “внутри”, а что “снаружи”?
Итак, я удовлетворил свое любопытство и реализовал фреймворк, приделал к нему красивый вывод о результатах тестов, логи, и написал первый тест на пинг. Теперь предстояло начать его использовать в боевых условиях.
Ах да, важный момент состоит в том, что не все интеграционные тесты имеет смысл выполнять “снаружи” встроенной системы. Например, тест на пинг с целевой платформы на хост прекрасно можно выполнить и “изнутри”. Для этого достаточно выполнить команду пинг и проверить результат исполнения последней команды (в unix это ‘echo $?’). Это реализовано при помощи стартового скрипта. Он выполняет настройку системы после ее загрузки — например, выполняется настройка сети. В конце стартового скрипта добавлены, условно говоря, простые интеграционные тесты, которые можно выполнить внутри встраиваемой системы.
"ifconfig eth0 10.0.2.16 netmask 255.255.255.0 hw ether AA:BB:CC:DD:EE:02 up",
"route add 10.0.2.0 netmask 255.255.255.0 eth0",
"route add default gw 10.0.2.10 eth0",
"export PWD=/",
"export HOME=/",
"mkdir /mnt",
"mkdir /mnt/fs_test",
"test -t fs_test_read",
В конце скрипта есть строчка “test -t fs_test_read”, которая запускает тест на файловую систему.
Появляется закономерный вопрос: какие тесты так выполнить нельзя и как с этим быть? Ответ на этот вопрос простой. Например, для выполнения теста может понадобиться обработать вывод программы при помощи утилит grep и awk. Естественно, такие утилиты запихивать внутрь встраиваемой системы не хочется. Поэтому такие тесты у нас выполняются “снаружи” с использованием реализованного фреймворка.
В качестве примера я приведу тест на ntpdate. Ntpdate — это программа, которая выставляет дату и время через протокол NTP. Она реализована в нашей ОС. В тесте проверяется, что время выставляемое внутри встроенной системы совпадает с текущим временем на хосте.
namespace import autotest::*
set host_date ""
proc get_host_date {} {
global host_date
spawn date -u --rfc-3339=date
expect -regexp {.{10}}
set host_date $expect_out(0,string)
}
TEST_SETUP_HOST {get_host_date}
TEST_CASE {ntpdate sets current date in UTC format correctly} {
variable host_ip
global host_date
test_assert_regexp_equal "ntpdate $host_ipr" ":/#"
test_assert_regexp_equal "dater" "$host_date"
return 0
}
Функция get_host_date получает текущее время на хосте в формате UTC. Для этого она регистрируется при помощи процедуры TEST_SETUP_HOST, то есть она будет вызываться на хосте. Ниже расположен TEST_CASE. В нем сначала выполняется на встраиваемой системе команда “ntpdate $host_ip”, а затем команда “date” и проверяется, что $host_date содержится в выводе команды.
Итоги
В итоге описанный выше процесс тестирования схематично можно представить следующим образом:
А вот так выглядит наш тестовый полигон:
Буду рад, если кто-нибудь поделится своим опытом в организации процесса тестирования встроенных систем.
Автор: alexkalmuk