» » Модульные тесты для проектов Ардуино

 

Модульные тесты для проектов Ардуино

Автор: admin от 15-08-2018, 21:10, посмотрело: 61

«Серьезные» разработчики встраиваемых систем (читай: стмщики) время от времени любят шпынять голозадых «ардуинщиков», у которых среда разработки, помимо всего прочего, не поддерживает даже аппаратные отладчики с точками останова и просмотром значений переменных под курсором мышки или в специальной табличке в реальном времени. Что ж, обвинение вполне справедливо, окошко Монитора последовательного порта (Serial Monitor) плюс Serial.println — не самый лучший инструмент отладки. Однако грамотный ардуинщик сможет с легкостью парировать атаку и поставить зарвавшегося стмщика на место в том случае, если он (ардуинщик) использует модульные тесты.



Скачайте демо-проект себе на компьютер.



Первым делом в каталоге $HOME/Arduino/libraries нужно создать символьную ссылку на каталог проекта



cd $HOME/Arduino/libraries/
ln -s /path/to/projects/dir/sput-ino-demo


или, если ваша операционная система не умеет в символьные ссылки, просто скопировать туда весь проект и дальше вести работу прямо в библиотеках.



получаем:



$HOME/Arduino/libraries/sput-ino-demo/


Структура этого проекта — структура библиотеки Ардуино.



здесь у нас исходники библиотеки — заголовочные файлы и код Си/С++:

[leech=https://github.com/sadr0b0t/sput-ino-demo/tree/master/src]sput-ino-demo/src/


sput-ino-demo/src/mylib.h

sput-ino-demo/src/mylib.cpp



Мы сможем подключать заголовочные файлы этой библиотеки из любого проекта Ардуино на текущем компьютере обычным:



#include "mylib.h"


Но чтобы это работало, в корень библиотеки нужно положить еще файл с информацией о библиотеке library.properties:

sput-ino-demo/library.properties



name=sput-ino-demo
version=0.0.1
author=sadr0b0t
maintainer=sadr0b0t
sentence=Demo project for sput-ino, Sput unit testing framework for C/C++ port to Arduino
paragraph=Demo project for sput-ino. Sput is an unit testing framework for C/C++ that focuses on simplicity of use and maximum portability. It is implemented as a single ANSI C compliant header file that provides all macros needed to start unit testing in nearly no time.
category=Other
url=https://github.com/sadr0b0t/sput-ino-demo
architectures=*


(Кстати, можно обойтись без library.properties, если положить все исходники .h, .c, .cpp не в src/, а в корень библиотеки sput-ino-demo/. Они так же будут подключаться/компилироваться с прошивками ссылающихся на них проектов, но мы так делать не будем, т.к. с src/, конечно, аккуратнее.)



Главный проект — скетч, теперь опять однофайловый:

sput-ino-demo/sput-ino-demo/sput-ino-demo.ino



Кстати-2, после установки проекта-библиотеки и перезапуска среды Ардуино этот скетч появится в меню Файл > Примеры > sput-ino-demo/sput-ino-demo, но он оттуда откроется только для чтения. Чтобы открыть скетч для редактирования, воспользуйтесь обычным Файл > Открыть и найдите его в файловой системе.



Кстати-3, файлы проекта-библиотеки mylib.h и mylib.cpp теперь не будут появляться в окне среды Arduino IDE (т.к. они находятся за пределами каталога скетча sput-ino-demo/), вам придется редактировать их в вашем любимом текстовом редакторе. Придется это принять как данность, кому к сожалению, а кому и к счастью.



Кстати-4, теперь у вас в проекте может быть более одного скетча «.ino».



Итак, с библиотекой и запускаемым скетчем разобрались, теперь к тестам.



Тесты мы разместим теперь в отдельном каталоге:

sput-ino-demo/test/



Запускаемый скетч для Ардуино и сами тесты:

sput-ino-demo/test/mylib-test-arduino/

sput-ino-demo/test/mylib-test-arduino/mylib-test-arduino.ino

sput-ino-demo/test/mylib-test-arduino/mylib-test.h

sput-ino-demo/test/mylib-test-arduino/mylib-test.cpp



Для настольной системы:

sput-ino-demo/test/mylib-test-desktop/



Тесты для настольной системы обсуждаем далее.



Запуск тестов на настольном компьютере



Итак, с запуском тестов на устройстве в целом разобрались. Теперь посмотрим, получится ли запустить эти же тесты на настольном компьютере. Для чего вообще запускать тесты на настольном компьютере? Во-первых, это удобно и быстро: поменяли в исходниках пару строк, быстро пересобрали, запустили тесты, здесь же в консольке посмотрели результат; в случае с устройством одна процедура прошивки может занять больше времени, чем все описанные выше действия. Во-вторых, некоторые ситуации, которые можно легко отработать в настольной симуляции (или, точнее, на макете, mock), на железке будет воспроизвести проблематичнее (например, отработать получение значения с одного или нескольких датчиков, отследить правильность ответной реакции). Так же существует мнение, что запускать тесты на микроконтроллерах вообще не правильно, а правильно их запускать только на настольных системах.



В общем, мы хотим:




  • запускать тесты на настольной системе без прошивки в устройство,

  • это должны быть те же самые тесты и те же самые тестируемые участки приложения, которые мы запускаем на устройстве.



Для того, чтобы решить эту задачу, во-первых, у нас должна быть библиотека для модульного тестирования, которая запустится одновременно и на железке с Ардуино и на настольной системе. Как было сказано в начале статьи, библиотека sput-ino по этому условию проходит: исходная библиотека sput работает на настольных системах с libc, sput-ino — порт библиотеки sput на платформу Ардуино с полным сохранением совместимости API, а также с поддержкой обеих платформ в одной библиотеке. Короче, тесты, использующие библиотеку sput-ino, можно компилировать как для настольных систем с libc, так и для платформы Ардуино.



Далее, условно разделим исходники на две части:




  • части приложения, которые не взаимодействуют с железом, не используют API Ардуино.

  • части приложения, которые взаимодействуют с железом, используют API Ардуино.



Части приложения НЕ используют API Ардуино



В первом случае (у нас это a_plus_b и a_minis_b) всё ясно — это части приложения, написанные на чистом Си/С++. Скорее всего это какие-то математические, алгоритмические или структурные блоки. Как они компилировались и запускались на Ардуино, точно так же они скомпилируются и запустятся с тестами на настольной системе без дополнительных телодвижений. Однако даже с ними не стоит забывать о различиях между платформами (выше мы уже рассмотрели случай с тестом, провалившимся из-за переполнения int на 16-битном чипе AVR, когда на 32-битном PIC32 и 64-битном настольном Intel/AMD все проходит). Такие отличия стоит учитывать при написании тестов и время от времени гонять тесты на целевом устройстве.



Части приложения используют API Ардуино



Во втором случае (у нас это led_on_even) ситуация кажется еще интереснее. Допустим, мы хотим протестировать функцию, которая помимо других действий обращается к железу контроллера через родные ардуинные digitalRead или digitalWrite. Совершенно очевидно, что никаких digitalRead и digitalWrite в стандартных библиотеках libc на настольной системе нет, этот блок приложения просто так не скомпилируется, тем более не запустится (и где у ноутбука пины GPIO?). Что делать? Неужели искать эмулятор или симулятор плат Ардуино и каким-то образом тащить все это счастье к себе в проект? Компилировать исходники Ардуино под x86? Писать симулятор чипа AVR со всей его внутренней регистровой кухней и драйверами самому?



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



Да, для каждого используемого вызова API Ардуино мы добавляем в проект собственную заглушку: объявляем функцию с таким же именем и сигнатурой (тип возвращаемого значения и аргументы), добавляем ей собственную реализацию. Нет, реализация заглушки не будет иметь никакого отношения к дереву исходников или к железу оригинальной Ардуино. В некоторых случаях заглушка может представлять пустую функцию вообще без кода.



sput-ino/example-desktop/



Вот заголовок фейкового Arduino.h

sput-ino/example-desktop/Arduino.h



#ifndef WPROGRAM_H
#define WPROGRAM_H

#define OUTPUT 1
#define INPUT 0

#define HIGH 1
#define LOW 0

unsigned long micros();

void pinMode(int pin, int mode);

void digitalWrite(int pin, int val);

int digitalRead(int pin);

#endif // WPROGRAM_H


а вот реализация заглушек Arduino.cpp:

sput-ino/example-desktop/Arduino.cpp



// saved values for pins
static int _pin_modes[64];
static int _pin_values[64];

// from Arduino.h

/**
 * micros stub
 */
unsigned long micros() {
    return 0;
}

/**
 * Set GPIO pin mode
 */
void pinMode(int pin, int mode) {
    _pin_modes[pin] = mode;
}

/**
 * Write GPIO pin value
 */
void digitalWrite(int pin, int val) {
    _pin_values[pin] = val;
}

/**
 * Read GPIO pin value
 */
int digitalRead(int pin) {
    return _pin_values[pin];
}


Здесь далеко не все вызовы API Ардуино, мы добавили только те функции и константы, которые необходимы для компиляции и запуска приведенных выше тестов (плюс еще парочка лишних вызовов для примера). Заглушки для остальных функций можно добавлять по мере необходимости.



В общем, этого уже достаточно, чтобы скомпилировать и запустить наши тесты на настольном компьютере. Добавляем главный исполняемый файл с main:

sput-ino/example-desktop/mylib-test-main.cpp



#include "mylib-test.h"

int main() {
    return mylib_test_suite();
}


собираем

sput-ino/example-desktop/build.sh



#!/bin/sh
# simple build script, feel free to modify or convert it
# to your favourite build system config

#gcc -c c_file_stub.c
#g++ -std=c++11 -c cpp_file_stub.cpp

g++ -std=c++11 -c 
    -I. -I../examples/sput-ino-modules -I$HOME/Arduino/libraries/sput-ino/src 
    Arduino.cpp 
    ../examples/sput-ino-modules/mylib.cpp 
    ../examples/sput-ino-modules/mylib-test.cpp 
    mylib-test-main.cpp
g++ *.o -o test_mylib


(видим тесты из модульной версии проекта Ардуино)



запускаем



./test_mylib


здесь же в консольке:



== 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%)

== Entering suite #2, "a minus b" ==

[2:1]  test_a_minus_b:#1  "115 - 6 == 109"  pass
[2:2]  test_a_minus_b:#2  "13 - 17 == -4"  pass

- 2 check(s), 2 ok, 0 failed (0.00%)

== Entering suite #3, "led on even" ==

[3:1]  test_led_on_even:#1  "num=2 => led#13 on"  pass
[3:2]  test_led_on_even:#2  "num=2 => led#13 on"  pass
[3:3]  test_led_on_even:#3  "num=5 => led#13 off"  pass
[3:4]  test_led_on_even:#4  "num=5 => led#13 off"  pass
[3:5]  test_led_on_even:#5  "num=18 => led#13 on"  pass
[3:6]  test_led_on_even:#6  "num=18 => led#13 on"  pass

- 6 check(s), 6 ok, 0 failed (0.00%)

==> 11 check(s) in 3 suite(s) finished after 0.00 second(s),
    11 succeeded, 0 failed (0.00%)

[SUCCESS]


Саксэс, саксэс, саксэс. На этой оптимистической ноте можно было бы закончить статью, но лучше разберем еще один обещанный выше случай.



Расширение API макета; тесты, которые получится запускать только на настольной системе



Выше мы отметили, что мы не должны проверять, что digitalWrite ЗАПИСАЛ значение в порт GPIO так, что digitalRead смог его прочитать. Мы проверяем, что digitalWriite БЫЛ ВЫЗВАН с нужными нам параметрами. Другими словами, мы хотим проверить, что digitalWrite был вызван с определенными параметрами, но мы не хотим использовать для этого digitalRead. Да, если говорить конкретно про пару digitalWrite/digitalRead, еще можно как-то рассуждать о целесообразности такого желания (ведь при запуске тестов на настольной системе digitalRead все равно является заглушкой и мы можем вставлять в нее любой удовлетворяющий нас код), но мы вполне можем захотеть проверить обращения и к другим вызовам API Ардуино, у которых нет даже такой пары (например, pinMode).



Короче, давайте добавим к заглушкам API Ардуино еще несколько расширенных вызовов и посмотрим, как будут выглядеть с ними наши старые тесты.



Для порядка объявим дополнительные вызовы для макета в отдельном заголовочном файле, я назвал его _Arduino.h (в начале нижнее подчеркивание):

sput-ino/example-desktop/_Arduino.h



#ifndef _ARDUINO_H
#define _ARDUINO_H

// Additional calls to get extended info from Arduino mocks

/** Get pin mode */
int _get_pin_mode(int pin);

/** Get pin value */
int _get_pin_value(int pin);

#endif // _ARDUINO_H


добавим реализацию в Arduino.cpp:

sput-ino/example-desktop/Arduino.cpp



// From _Arduino.h
// Calls to get extended info from Arduino mocks

/** Get pin mode */
int _get_pin_mode(int pin) {
    return _pin_modes[pin];
}

/** Get pin value */
int _get_pin_value(int pin) {
    return _pin_values[pin];
}


Как видим, реализация _get_pin_value идентична заглушке для digitalRead, но _get_pin_mode уже не имеет прямого аналога в API Ардуино.



Далее пишем новую версию теста test_led_on_eventest_led_on_even_desktoponly, использующую новый вызов _get_pin_value вместо digitalRead. Этот тест уже не скомпилируется и не запустится на устройстве, поэтому мы его размещаем в отдельном модуле за пределами проекта Ардуино — в каталоге с исходными файлами для тестирования на настольном компьютере sput-ino/example-desktop/



заголовочный файл с наборами тестов:

sput-ino/example-desktop/mylib-test-desktoponly.h



#ifndef MYLIB_TEST_DESKTOPONLY_H
#define MYLIB_TEST_DESKTOPONLY_H

/** Test suite for led_on_even call */
int mylib_test_suite_led_on_even_desktoponly();

/** Desktop-only tests in one bundle */
int mylib_test_suite_desktoponly();

#endif // MYLIB_TEST_DESKTOPONLY_H


Код теста:

sput-ino/example-desktop/mylib-test-desktoponly.cpp



// http://www.use-strict.de/sput-unit-testing/tutorial.html
#include "sput.h"

#include "_Arduino.h"
#include "Arduino.h"
#include "mylib.h"

/** Test test_led_on_even call */
bool test_led_on_even_desktoponly() {
    // we do not use Arduino API calls here to get info about
    // moked chip state, use calls from _Arduino.h instead

    sput_fail_unless(led_on_even(13, 2), "num=2 => led#13 on");
    sput_fail_unless(_get_pin_value(13) == HIGH, "num=2 => led#13 on");

    sput_fail_unless(!led_on_even(13, 5), "num=5 => led#13 off");
    sput_fail_unless(_get_pin_value(13) == LOW, "num=5 => led#13 off");

    sput_fail_unless(led_on_even(13, 18), "num=18 => led#13 on");
    sput_fail_unless(_get_pin_value(13) == HIGH, "num=18 => led#13 on");
}

/*******************************************/
// test suites

/** Test suite for led_on_even call */
int mylib_test_suite_led_on_even_desktoponly() {
    sput_start_testing();

    sput_enter_suite("led on even (only desktop)");
    sput_run_test(test_led_on_even_desktoponly);

    sput_finish_testing();
    return sput_get_return_value();
}

/** All tests in one bundle */
int mylib_test_suite_desktoponly() {
    sput_start_testing();

    sput_enter_suite("led on even (only desktop)");
    sput_run_test(test_led_on_even_desktoponly);

    sput_finish_testing();
    return sput_get_return_value();
}


Немного поправим исполняемый файл — теперь у нас два набора тестов: кросс-платформенные тесты и тесты, которые запускаем только на десктопе.



sput-ino/example-desktop/mylib-test-main.cpp



#include "mylib-test.h"
#include "mylib-test-desktoponly.h"

int main() {
    return mylib_test_suite() | mylib_test_suite_desktoponly();
}


чуть правим сборочный скрипт (добавляем mylib-test-desktoponly.cpp)



#!/bin/sh
# simple build script, feel free to modify or convert it
# to your favourite build system config

#gcc -c c_file_stub.c
#g++ -std=c++11 -c cpp_file_stub.cpp

g++ -std=c++11 -c 
    -I. -I../examples/sput-ino-modules -I$HOME/Arduino/libraries/sput-ino/src 
    Arduino.cpp 
    ../examples/sput-ino-modules/mylib.cpp 
    ../examples/sput-ino-modules/mylib-test.cpp 
    mylib-test-desktoponly.cpp 
    mylib-test-main.cpp
g++ *.o -o test_mylib


собираем



./build.sh


запускаем



./test_mylib


== 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%)

== Entering suite #2, "a minus b" ==

[2:1]  test_a_minus_b:#1  "115 - 6 == 109"  pass
[2:2]  test_a_minus_b:#2  "13 - 17 == -4"  pass

- 2 check(s), 2 ok, 0 failed (0.00%)

== Entering suite #3, "led on even" ==

[3:1]  test_led_on_even:#1  "num=2 => led#13 on"  pass
[3:2]  test_led_on_even:#2  "num=2 => led#13 on"  pass
[3:3]  test_led_on_even:#3  "num=5 => led#13 off"  pass
[3:4]  test_led_on_even:#4  "num=5 => led#13 off"  pass
[3:5]  test_led_on_even:#5  "num=18 => led#13 on"  pass
[3:6]  test_led_on_even:#6  "num=18 => led#13 on"  pass

- 6 check(s), 6 ok, 0 failed (0.00%)

==> 11 check(s) in 3 suite(s) finished after 0.00 second(s),
    11 succeeded, 0 failed (0.00%)

[SUCCESS]

== Entering suite #1, "led on even (only desktop)" ==

[1:1]  test_led_on_even_desktoponly:#1  "num=2 => led#13 on"  pass
[1:2]  test_led_on_even_desktoponly:#2  "num=2 => led#13 on"  pass
[1:3]  test_led_on_even_desktoponly:#3  "num=5 => led#13 off"  pass
[1:4]  test_led_on_even_desktoponly:#4  "num=5 => led#13 off"  pass
[1:5]  test_led_on_even_desktoponly:#5  "num=18 => led#13 on"  pass
[1:6]  test_led_on_even_desktoponly:#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]


Ну и на десерт



Хороший пример: потестируем обработчик прерываний



Допустим, у нас есть небольшой проект с модулем управления шаговым мотором:




  • Мотор шагает на фронте HIGH > LOW,

  • модуль проверяет выход за границы с концевых датчиков и

  • программно считает сделанные шаги.



Мотор шагает в фоне по сигналам из программного обработчика прерываний от таймера, несколько тысяч (или десятков тысяч) раз в секунду. Один шаг — 3 тика таймера: тик 1 — проверяем границы (концевые датчики), тик 2 — взводим ножку STEP в HIGH, тик 3 — делаем шаг: сбрасываем STEP в LOW, увеличиваем счетчик.



Код управления мотором может выглядеть примерно так:



#define ACTION_STOP 0
#define ACTION_CHECK_BOUNDS 1
#define ACTION_GO_HIGH 2
#define ACTION_STEP 3

int step_count = 0;
int action = ACTION_STOP;

void timer_handle_interrupts() {
    // мы можем пропустить несколько вызовов,
    // чтобы двигаться с нужной скоростью
    if(!timeForStep()) return;

    // время делать шаг
    if(ACTION_CHECK_BOUNDS == action) {
        // проверяем границы - концевые датчики
        if(checkBounds()) {
            // сработал концевик - останавливаемся
            action = ACTION_STOP;
        } else {
            // все ок, на следующий тик готовим шаг
            action = ACTION_GO_HIGH;
        }
    } else if(ACTION_GO_HIGH == action) {
        // взводим ножку STEP, шаг на следующий тик таймера
        digitalWrite(STEP_PIN, HIGH);
        action = ACTION_STEP;
    } else if(ACTION_STEP == action) {
        // шагаем
        digitalWrite(STEP_PIN, LOW);
        step_count++;

        if(step_count < max_steps) {
            // готовим новый шаг
            action = ACTION_CHECK_BOUNDS;
        } else {
            // нашагались
            action = ACTION_STOP;
        }
    }
}


Вызов timer_handle_interrupts — обработчик прерывания от таймера, вызывается на каждый тик таймера определенное заранее количество раз в секунду (как запустить таймер на Ардуино: arduino-timer-api).



Теперь представьте, что код загружен на контроллер, мотор подключен, крутится, но что-то не в порядке: может вращается слишком быстро, может не докручивает часть предполагаемого пути, может что-то еще. Подключение электроники в порядке, проверено на простых тестах, проблема явно в программе. Как бы вы стали отлавливать ошибку? Допустим, у вас есть полноценный аппаратный отладчик с просмотром памяти и переменных, точками останова и красивой поддержкой в IDE. Будем ставить брейкпоинт в обработчик прерывания и проверять значения переменных все 100500 тиков? Ставить точку останова с динамическим условием в надежде поймать проблему в середине цикла? Возможно какой-то из этих или других приемов поможет отловить и исправить проблему.



Но посмотрим, как будет выглядеть процедура отладки этого участка при помощи автоматических тестов:



void test_timer_handle_interrupts() {
    // тик 1
    test_timer_handle_interrupts();
    // проверка 1
    // проверка 2
    // проверка 3

    // тик 2
    test_timer_handle_interrupts();
    // проверка 1
    // проверка 2
    // проверка 3

    // тик 3
    test_timer_handle_interrupts();
    // проверка 1
    // проверка 2
    // проверка 3

    // тик 100500
    for(long i = 0; i < 100500 - 3; i++) {
    }
    // проверка 1
    // проверка 2
    // проверка 3

    // тик 100500+1
    test_timer_handle_interrupts();
    // проверка 1
    // проверка 2
    // проверка 3

    // ...
    // и так далее
}


Прерывания от таймера мы симулируем элементарным ручным вызовом обработчика test_timer_handle_interrupts. Как видим, таким образом можно легко контролировать каждый тик: 1й, 2й, 3й, 103й, предпоследний, последний, — и после каждого тика спокойно делать любые нужные проверки.



Источник: Хабр / Интересные публикации

Категория: Железо

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *