Запилил руководство к побочному проекту (порт библиотеки для модульных тестов на Си/С++ на API Ардуино):
https://github.com/sadr0b0t/sput-ino Модульные тесты для проектов Ардуино:
https://habr.com/post/419445/ По результату получился эталонный срач на 170 коментов (для меня, кажется, это рекорд, на хабре точно) СТМ vs Ардуина (про тесты почти ничего не говорили), спасибо
Олегу Артамонову, без него бы ничего не получилось.
На следующий день товарищ Артамонов запилил свою статью про СТМ-ардуину (mbed) с более лучшим текстовым редактором (IDE!), более лучшим HAL, таймерами (в официальной поставке!) и без бесконечного цикла (пожалуй, за это можно правда зацепиться):
Быстрый старт с ARM Mbed: разработка на современных микроконтроллерах для начинающих Весь срач переехал туда (и быстро переплюнул мой по количеству коментов), по результату топик-стартера забанили на несколько дней.
автапати раз,
автапати два еще
https://www.facebook.com/yuri.panchul/posts/10156861305488392еще
https://www.facebook.com/olartam/posts/2018738911509522еще
https://mbr.livejournal.com/540016.html «Серьезные» разработчики встраиваемых систем (читай: стмщики) время от времени любят шпынять голозадых «ардуинщиков», у которых среда разработки, помимо всего прочего, не поддерживает даже аппаратные отладчики с точками останова и просмотром значений переменных под курсором мышки или в специальной табличке в реальном времени. Что ж, обвинение вполне справедливо, окошко Монитора последовательного порта (Serial Monitor) плюс Serial.println - не самый лучший инструмент отладки. Однако грамотный ардуинщик сможет с легкостью парировать атаку и поставить зарвавшегося стмщика на место в том случае, если он (ардуинщик) использует модульные тесты.
Кому лень
идти на Хабру, традиционно, все под катом (все равно все не умещается, поэтому режу где придется). Но за форматирование не ручаюсь, подсветку тоже восстанавливать лень.
Итак, модульные тесты (unit tests, юнит-тесты) облегчают жизнь при поиске проблемных мест приложения, предотвращают повторение уже найденных проблем (регрессий), дают измеримую уверенность в надежности написанного кода. Это тем более важно при разработке встраиваемых приложений и всевозможных мобильных роботов, для которых процесс отладки, отлова и воспроизведения (особенно, воспроизведения) ошибок особенно затруднителен по сравнению с классическими настольными, серверными или мобильными приложениями.
Однако переход к использованию автоматических тестов в проекте требует специальной внутренней дисциплины, особого подхода к написанию кода и организации рабочего пространства проекта.
При подготовке к внедрению в проект модульных тестов следует иметь ввиду:
- Тесты требуют дополнительного времени для написания кода (на самом деле, нет: время, потраченное на автоматические тесты, вполне сравнимо со временем, потраченным на ручную отладку того же участка, а на долгой дистанции оно еще многократно окупится), при этом код теста может превышать по размеру код тестируемого участка.
- В покрытом тестами проекте может быть сложно проводить глобальную реорганизацию кода (рефакторинг) - особенно актуально на начальном этапе разработки, когда кодовая база и внутренний API еще не достаточно устаканились (с другой стороны, рефактор проекта, не покрытого тестами, повлечет все те же регрессии, просто вы про них не узнаете)
- Нужно писать модули приложения так, чтобы их можно было запускать как в рамках приложения, так и внутри отдельных тестов
- Необходимо проработать структуру и связи внутри проекта так, чтобы в нем нашлось место коду основного приложения, исполняемой прошивке основного приложения, коду тестов, исполняемой прошивке («запускальщик»/ланчер) для запуска тестов.
Я более не буду распространяться про философию модульного тестирования, а просто покажу, как технически внедрить простые модульные тесты в ваш проект на Ардуино.
Далее рассмотрим:
- Несколько стратегий организации рабочего пространства проекта с модульными тестами с учетом особенностей платформы Ардуино.
- Вариант «все в одном» (и код и тесты в одном файле скетча),
- вынесение тестов в отдельный модуль в каталоге скетча,
- вынесение тестов в отдельный проект.
- Запуск тестов на устройстве,
- запуск этих же тестов на настольном компьютере без загрузки на устройство, заглушки для API Ардуино
Выбор библиотеки для модульного тестирования
Нам нужен фреймворк модульного тестирования:
- Для Си/С++
- Должен работать на устройствах семейства Ардуино
- Должен работать на настольных системах
- Люблю легковесные библиотеки (моё персональное предпочтение)
Для программирования Ардуино используется язык С++ вперемешку с Си, поэтому, теоретически, пойдет любой фреймворк модульного тестирования для С++, но мы хотим запускать тесты и на настольном компьютере и на устройстве. Дело в том, что для Ардуино реализованы кое-какие вызовы стандартной библиотеки libc, но далеко не все, поэтому не каждый фреймворк, работающий с libc, скомпилируется для Ардуино. Верно и в обратную сторону: если фреймворк сделан специально для Ардуино, то он может не заработать на настольной системе с libc.
Я просмотрел несколько фреймворков и остановился на 2х:
- ArduinoUnit: https://github.com/mmurdoch/arduinounit. В общем, он удовлетворяет ключевым исходным требованиям: работает как на Ардуино (очевидно из названия), так и на настольных системах (см раздел «En Vitro Testing» на сайте проекта), но на беглый взгляд показался тяжеловатым и я решил посмотреть другие варианты.
- Библиотека Sput (Sput Unit Testing Framework for C/C++) https://www.use-strict.de/sput-unit-testing/. Это библиотека легкая настолько, насколько это возможно: всего один заголовочный файл, даже без пары с исходником «.cpp» (все сделано на нескольких макросах). Однако вывод сообщений идет через std::out (что совершенно естественно для libc), который на Ардуино как раз не реализован.
И все-таки мои симпатии перевесили в пользу sput, а проблему с std::out удалось решить несколькими исправлениями (заменой printf на sprintf+Serial.print).
В итоге получился
проект sput-ino - порт библиотеки sput на платформу Ардуино с сохранением совместимости с настольными системами с libc
- пример однофайлового скетча с тестами
/sput-ino/examples/sput-ino-monolith/ - пример с разделением основного кода и тестов на модули
sput-ino/examples/sput-ino-modules/ - запуск тестов на настольной системе
sput-ino/example-desktop/ - пример с разделением основного кода и тестов на разные проекты - в отдельном репозитории
https://github.com/sadr0b0t/sput-ino-demo Установим библиотеку
Просто клонируйте репозиторий git
https://github.com/sadr0b0t/sput-ino.git в каталог $HOME/Arduino/libraries:
cd $HOME/Arduino/libraries/
git clone
https://github.com/sadr0b0t/sput-ino.git и перезапустите среду Ардуино IDE.
Или на странице проекта github
https://github.com/sadr0b0t/sput-ino/ нажмите кнопку Клонировать или скачать > Скачать ZIP (Clone or download > Download ZIP), после этого установите архив sput-ino-master.zip через меню установки библиотек Ардуино: Скетч > Подключить библиотеку > Добавить .ZIP библиотеку....
Примеры появятся в меню Файл > Примеры > sput-ino (File > Examples > sput-ino)
Простой вариант: однофайловый скетч с кодом и тестами
При внедрении тестов в проект Ардуино придется учитывать некоторые особенности её сборочной системы. В простейшем случае проект (скетч) состоит из одного файла с расширением «.ino». При сборке файл «.ino» с незначительными изменениями конвертируется в «.cpp» (подключается заголовок Arduino.h и еще кое-чего по мелочи), сгенерированный файл компилируется в прошивку.
Создаем новый скетч
sput-ino/examples/sput-ino-monolith/sput-ino-monolith.ino добавляем какой-то полезный код:
/**
* @return a плюс b
*/
int a_plus_b(int a, int b) {
return a + b;
}
/**
* @return a минус b
*/
int a_minus_b(int a, int b) {
return a - b;
}
/**
* Включить лампочку, если число четное
* @param pin номер ножки лапмочки
* @param num число
* @return true, если число num четное
*/
bool led_on_even(int pin, int num) {
if(num % 2 == 0) {
digitalWrite(pin, HIGH);
} else {
digitalWrite(pin, LOW);
}
return num % 2 == 0;
}
Пишем тесты с библиотекой sput (подробнее документация:
http://www.use-strict.de/sput-unit-testing/tutorial.html):
#include "sput.h"
/** Test a_plus_b call */
void test_a_plus_b() {
sput_fail_unless(a_plus_b(2, 2) == 4, "2 + 2 == 4");
sput_fail_unless(a_plus_b(-2, 2) == 0, "-2 + 2 == 0");
// this one would pass on 32-bit controllers and would fail on AVR with 16-bit int
sput_fail_unless(a_plus_b(34000, 34000) == 68000, "34000 + 34000 == 68000");
}
/** Test a_minus_b call */
void test_a_minus_b() {
sput_fail_unless(a_minus_b(115, 6) == 109, "115 - 6 == 109");
sput_fail_unless(a_minus_b(13, 17) == -4, "13 - 17 == -4");
}
/** Test test_led_on_even call */
bool test_led_on_even() {
pinMode(13, OUTPUT);
sput_fail_unless(led_on_even(13, 2), "num=2 => led#13 on");
// would pass on desktop, might fail or pass on difference devices
// (e.g.: Arduino Due - fail, ChipKIT Uno32 - pass)
sput_fail_unless(digitalRead(13) == HIGH, "num=2 => led#13 on");
sput_fail_unless(!led_on_even(13, 5), "num=5 => led#13 off");
sput_fail_unless(digitalRead(13) == LOW, "num=5 => led#13 off");
sput_fail_unless(led_on_even(13, 18), "num=18 => led#13 on");
sput_fail_unless(digitalRead(13) == HIGH, "num=18 => led#13 on");
}
Комплектуем наборы тестов (тест-сьюты).
Все тесты в одном наборе:
/** All tests in one bundle */
int mylib_test_suite() {
sput_start_testing();
sput_enter_suite("a plus b");
sput_run_test(test_a_plus_b);
sput_enter_suite("a minus b");
sput_run_test(test_a_minus_b);
sput_enter_suite("led on even");
sput_run_test(test_led_on_even);
sput_finish_testing();
return sput_get_return_value();
}
и по одному набору на каждый тест:
/** Test suite for a_plus_b call */
int mylib_test_suite_a_plus_b() {
sput_start_testing();
sput_enter_suite("a plus b");
sput_run_test(test_a_plus_b);
sput_finish_testing();
return sput_get_return_value();
}
/** Test suite for a_minus_b call */
int mylib_test_suite_a_minus_b() {
sput_start_testing();
sput_enter_suite("a minus b");
sput_run_test(test_a_minus_b);
sput_finish_testing();
return sput_get_return_value();
}
/** Test suite for led_on_even call */
int mylib_test_suite_led_on_even() {
sput_start_testing();
sput_enter_suite("led on even");
sput_run_test(test_led_on_even);
sput_finish_testing();
return sput_get_return_value();
}
Здесь я делаю по одному набору на каждый тест плюс один набор на все тесты вместе. На устройстве ограничен ресурс флеш-памяти, все тесты могут не уместиться разом в одну прошивку, поэтому одиночные наборы можно включать/выключать, комментируя отдельные вызовы в главном скетче. Всеобщий набор удобно пускать одной строчкой на настольном компьютере (ну, и на устройстве тоже, если он там все-таки уместится).
Запускаем тесты здесь:
void run_tests() {
Serial.println("#################### Start testing...");
// comment out specific test suites if firmware does not
// fit to device memory
// Test suite for a_plus_b call
mylib_test_suite_a_plus_b();
// Test suite for a_minus_b call
mylib_test_suite_a_minus_b();
// Test suite for led_on_even call
mylib_test_suite_led_on_even();
// All tests in one bundle
//mylib_test_suite();
Serial.println("#################### Finished testing");
}
Добавляем обычные setup/loop, запускаем тесты с run_tests в setup в самом начале, предварительно инициировав последовательный порт Serial.begin, чтобы тесты могли печатать сообщения:
void setup() {
Serial.begin(9600);
while (!Serial);
// run tests
run_tests();
// other code - kinda application business logic
Serial.println("Just show that we call functions from tested lib, nothing useful here");
pinMode(13, OUTPUT);
Serial.print("14+23=");
Serial.println(a_plus_b(14, 23));
Serial.print("14-23=");
Serial.println(a_minus_b(14, 23));
Serial.print("34000+34000=");
Serial.println(a_plus_b(34000, 34000));
}
void loop() {
static int i = 0;
led_on_even(13, i++);
delay(2000);
}
Здесь основной код приложения и тесты совмещены внутри одного скетча. Если хотите отключить запуск тестов, нужно закомментировать вызов run_tests, приложение будет работать в обычном режиме.
Компилируем, загружаем на устройство, смотрим результат в окошке монитора последовательного порта (Инструменты > Монитор порта / Tools > Serial monitor)
Результат выполнения на плате ChipKIT Uno32 (клон Ардуино с 32-битным чипом PIC32):
#################### Start testing...
== Entering suite #1, "a plus b" ==
[1:1] test_a_plus_b:#1 "2 + 2 == 4" pass
[1:2] test_a_plus_b:#2 "-2 + 2 == 0" pass
[1:3] test_a_plus_b:#3 "34000 + 34000 == 68000" pass
--> 3 check(s), 3 ok, 0 failed (0.00%)
==> 3 check(s) in 1 suite(s) finished after 0.00 second(s),
3 succeeded, 0 failed (0.00%)
[SUCCESS]
== Entering suite #1, "a minus b" ==
[1:1] test_a_minus_b:#1 "115 - 6 == 109" pass
[1:2] test_a_minus_b:#2 "13 - 17 == -4" pass
--> 2 check(s), 2 ok, 0 failed (0.00%)
==> 2 check(s) in 1 suite(s) finished after 0.00 second(s),
2 succeeded, 0 failed (0.00%)
[SUCCESS]
== Entering suite #1, "led on even" ==
[1:1] test_led_on_even:#1 "num=2 => led#13 on" pass
[1:2] test_led_on_even:#2 "num=2 => led#13 on" pass
[1:3] test_led_on_even:#3 "num=5 => led#13 off" pass
[1:4] test_led_on_even:#4 "num=5 => led#13 off" pass
[1:5] test_led_on_even:#5 "num=18 => led#13 on" pass
[1:6] test_led_on_even:#6 "num=18 => led#13 on" pass
--> 6 check(s), 6 ok, 0 failed (0.00%)
==> 6 check(s) in 1 suite(s) finished after 0.00 second(s),
6 succeeded, 0 failed (0.00%)
[SUCCESS]
#################### Finished testing
Just show that we call functions from tested lib, nothing useful here
14+23=37
14-23=-9
34000+34000=68000
запуск на обычной Arduino Uno (чип AVR 16 бит):
#################### Start testing...
== Entering suite #1, "a#################### Start testing...
== Entering suite #1, "a plus b" ==
[1:1] test_a_plus_b:#1 "2 + 2 == 4" pass
[1:2] test_a_plus_b:#2 "-2 + 2 == 0" pass
[1:3] test_a_plus_b:#3 "34000 + 34000 == 68000" FAIL
! Type: fail-unless
! Condition: a_plus_b(34000, 34000) == 68000
! Line: 14
--> 3 check(s), 2 ok, 1 failed (?%)
==> 3 check(s) in 1 suite(s) finished after ? second(s),
2 succeeded, 1 failed (?%)
[FAILURE]
== Entering suite #1, "a minus b" ==
[1:1] test_a_minus_b:#1 "115 - 6 == 109" pass
[1:2] test_a_minus_b:#2 "13 - 17 == -4" pass
--> 2 check(s), 2 ok, 0 failed (?%)
==> 2 check(s) in 1 suite(s) finished after ? second(s),
2 succeeded, 0 failed (?%)
[SUCCESS]
== Entering suite #1, "led on even" ==
[1:1] test_led_on_even:#1 "num=2 => led#13 on" pass
[1:2] test_led_on_even:#2 "num=2 => led#13 on" pass
[1:3] test_led_on_even:#3 "num=5 => led#13 off" pass
[1:4] test_led_on_even:#4 "num=5 => led#13 off" pass
[1:5] test_led_on_even:#5 "num=18 => led#13 on" pass
[1:6] test_led_on_even:#6 "num=18 => led#13 on" pass
--> 6 check(s), 6 ok, 0 failed (?%)
==> 6 check(s) in 1 suite(s) finished after ? second(s),
6 succeeded, 0 failed (?%)
[SUCCESS]
#################### Finished testing
Just show that we call functions from tested lib, nothing useful here
14+23=37
14-23=-9
34000+34000=2464
Обратим внимание на пару моментов:
- На PIC32 все тесты завершились успешно, а на AVR один тест со сложением провалился. 34000 + 34000 == 68000 только на 32-битном контроллере PIC32, на AVR размер int = 2 байта (16 бит), максимальное число, которое можно в него положить = 2^16-1=65536-1=65535 (в беззнаковом режиме unsigned). На AVR с 16-битным int происходит переполнение, а на 32-битном PIC32 (и на 64-битном десктопе с x86_64) все ок. Такие особенности платформы стоит учитывать там, где они могут себя проявить, и добавлять в тесты.
- Тест test_led_on_even (включить лампочку, если передано четное число) успешно проходит на обоих контроллерах, но, вообще говоря, использовать чтение digitalRead для проверки успешности записи digitalWrite на реальном железе - не самая хорошая идея.
Во-первых, digitalRead (прочитать значение GPIO в режиме ввода pinMode INPUT) совершенно не обязан выдавать значение, которое было отправлено в порт GPIO с digitalWrite в режиме вывода pinMode OUTPUT: в официальной документации на digitalRead про такое использование метода ничего не говорится, хотя на железке это и срабатывает.
Во-вторых, полагаясь на то, что digitalRead вернет нужное значение после вызова digitalWrite, мы встаем на скользкую дорожку тестирования не своего, но чужого кода. Успешность прохождения теста зависит не только от тестируемого кода, но и от того, как именно реализована связка digitalWrite/digitalRead на конкретном контроллере и нет ли в ней ошибок (кстати, на Arduino UNO с AVR тест провалится, если убрать строку перевода ножки в режим вывода pinMode(13, OUTPUT), на ChipKIT Uno32 с PIC32 тест проходит в любом случае).
Здесь мы не должны проверять, что digitalWrite ЗАПИСАЛ значение в порт GPIO так, что digitalRead смог его прочитать. Здесь мы проверяем, что digitalWriite БЫЛ ВЫЗВАН с нужными нам параметрами. При запуске тестов на реальном железе мы навряд ли сможем это сделать без построения каких-то некрасивых вспомогательных конструкций, но в режиме тестирования на настольной системе это будет легко реализовано при помощи заглушек (см ниже).
Тестируемый код и тесты в отдельные модули
дальше:
https://habr.com/post/419445/