Pull to refresh
0
Embox
Открытая и свободная ОС для встроенных систем

Ещё один способ автоматического вызова unit-тестов на языке Си

Reading time 11 min
Views 13K
На Хабре уже есть несколько статей о том, как разрабатывать модульные тесты на языке Си. Я не собираюсь критиковать описанные подходы, а лишь предложу ещё один — тот, которым мы пользуемся в проекте Embox. Пару раз мы уже ссылались на него на Хабре.

Кому интересно, прошу подкат! Но предупреждаю: там много портянок из макросов и «линкерской» магии.

Статические массивы переменной длины


Немного погрузимся в проблематику вопроса. Причиной сложности разработки модульных тестов на языке Си является отсутствие статических конструкторов в синтаксисе. Это значит, что вам необходимо в явном виде описывать вызовы всех функций с тестами, которые вы хотите исполнить, а это, согласитесь крайне неудобно.
С другой стороны, когда речь заходит о вызове большого количества функций, у меня сразу возникают мысли о массиве указателей. То есть, чтобы вызвать все требуемые функции, необходимо взять массив указателей на эти функции, обратиться к каждому его элементу и вызвать соответствующую функцию. Таким образом, имеется конструкция типа этой:
void test_func_1(void) {
}
void test_func_2(void) {
}

int main(void){
    int i;
    void (*all_tests[])(void) = {test_func_1, test_func_2};

    for(i = 0; i < sizeof(all_tests)/sizeof(all_tests[0]); i ++) {
        all_tests[i]();
    }
    return 0;
}

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

Такого механизма в языке Си не существует, но давайте пофантазируем над синтаксисом. Это могло бы выглядеть так:
<type> arr[];
<type> a(array(arr));
<type> b(array(arr));

Или, если использовать механизм расширений в gcc, который выражается с помощью __attribute__
<type> arr[];
<type> a __attribute__(array_member(arr)));
<type> b __attribute__(array_member(arr)));

Теперь остаётся вспомнить, что массив в языке Си — это константный указатель на первый элемент в этом массиве, а элементы располагаются последовательно и имеют одинаковый размер. Значит, если мы сможем указать компилятору, что нужно уложить определённые переменные в памяти последовательно, мы сможем организовать свой собственный массив. По крайней мере, обращаться с этими переменными мы сможем так же, как с элементами настоящего массива.

За размещение переменных отвечает не компилятор, а линкер, и в линкер-скриптах указвыается, как именно он должен это делать. Из синтаксиса этих скриптов понятно, что линкер группирует данные по секциям, и если в определённой секции будут переменные лишь одного типа, это и будет массивом по сути, и останется только как-то определить метку массива.

Когда мы определяем массив, мы указываем тип его элементов. Значит, можно определить первый элемент и ссылку на него можно использовать как массив. А ещё лучше ввести пустой массив указанного типа, так как он всё равно понадобиться для корректного синтаксиса.
Получится что-то вроде этого:
<type> arr[] __attribute__((section(“array_spread.arr”))) = {};

Для того, чтобы метка указывала на начало секции, можно воспользоваться скриптами из линкера. По умолчанию линкер помещает данные в произвольном порядке, но если воспользоваться функцией SORT(“section_name”), линкер будет сортировать символы в секции в лексикографическом порядке. Таким образом, для того, чтобы символ массива указывал на начало секции, нам нужно, чтобы имя подсекции лексикографически шло раньше остальных частей массива. Для этого достаточно приписать “0_head” к началу массива, а для всех переменных — “1_body”. Конечно, достаточно было бы просто “0” и “1”, но тогда текст программы стал бы менее читаемым.

Таким образом, объявление массива будет выглядеть так:
<type> arr[] __attribute__((section(“array_spread.arr_0_head.rodata”))) = {};

Сам линкер скрипт выглядит следующим образом:
SECTIONS {
    .rodata.array_spread : {
        *(SORT(.array_spread.*.rodata))
    }
}
INSERT AFTER .rodata;

Подключить его можно с помощью ключа gcc -T

Для того, чтобы указать, что какую-то переменную нужно поместить в ту или иную секцию, нужно добавить соответствующий атрибут:
<type> a __attribute__((section(“array_spread.arr_1_body.rodata”)));

Таким образом, мы сформируем массив, но останется ещё одна проблема: как получить размер этого массива? Если с обычными массивами, мы просто брали его размер в байтах и делили на размер первого элемента, то в данной ситуации компилятор не знает ничего о размере секции. Чтобы решить эту проблему, давайте, как и с началом массива, добавим такую же метку в конец, опять же помня об алфавитной сортировке.
Итак, получим следующее:
<type> arr_tail[] __attribute__((section(“array_spread.arr_9_tail.rodata”))) = {};

Теперь, когда у нас есть вся необходимая информация о том, как создать массив, давайте попробуем переписать предыдущий пример:
#include <stddef.h>
#include <stdio.h>

void test_func_1(void) {
    printf("test 1\n");
}
void test_func_2(void) {
    printf("test 2\n");
}

void (*all_tests_item_1)(void) __attribute__((section(".array_spread.all_tests_1_body.rodata"))) = test_func_1;
void (*all_tests_item_2)(void) __attribute__((section(".array_spread.all_tests_1_body.rodata"))) = test_func_2;
void (*all_tests[])(void) __attribute__((section(".array_spread.all_tests_0_head.rodata"))) = {};
void (*all_tests__tail[])(void) __attribute__((section(".array_spread.all_tests_9_tail.rodata"))) = {};

int main(void){
    int i;

    printf("%zu tests start\n", (size_t)(all_tests__tail - all_tests));

    for(i = 0; i < (size_t)(all_tests__tail - all_tests); i ++) {
        all_tests[i]();
    }
    return 0;
}

Если запустить эту программу, указав приведённый выше линкер-скрипт, мы получим тот же результат, что и с обычными массивами. Но при этом вы можете не только создать статический массив переменной длины в одном файле, но и массив, распределённый по разным файлам, так как линкер работает на последней стадии сборки, собирая все объектные файлы в один, и это порой очень полезно.

Конечно, линкер не проверяет тип и размер объектов, которые вы поместили в секцию, и если вы разместите в одной секции объекты разных типов, то будете самому себе злобным Буратино. Но если всё делать аккуратно, вы получите довольно интересный механизм создания статических массивов переменной длины на языке Си.

Конечно, данный подход не очень удобен в плане синтаксиса, поэтому стоить спрятать всю магию в макросы.

Для начала, упростим себе жизнь и введём пару вспомогательных макросов, которые вводят имена массивов, секций и переменных.
Первый упрощает название секций:
#define __ARRAY_SPREAD_SECTION(array_nm, order_tag) \
    ".array_spread." #array_nm order_tag ".rodata,\"a\",%progbits;#"

Второй определяет внутреннюю переменную (описанную выше метку конца массива)
#define __ARRAY_SPREAD_PRIVATE(array_nm, private_nm) \
    __array_spread__##array_nm##__##private_nm

Теперь определим макрос, который заводит массив.
#define ARRAY_SPREAD_DEF(element_type, name) \
        element_type volatile const name[] __attribute__ ((used,        \
        /* Some versions of GCC do not take into an account section     \
         * attribute if it appears after the definition. */             \
            section(__ARRAY_SPREAD_SECTION(name, "0_head")))) =         \
            { /* Empty anchor to the array head. */ };                  \
        element_type volatile const __ARRAY_SPREAD_PRIVATE(name,tail)[] \
        __attribute__ ((used,                                           \
        /* Some versions of GCC do not take into an account section     \
         * attribute if it appears after the definition. */             \
            section(__ARRAY_SPREAD_SECTION(name, "9_tail")))) =         \
            { /* Empty anchor at the very end of the array. */ }

Собственно, это использованный ранее код, обёрнутый в макрос.
Сначала мы вводим метку начала массива — пустой массив, и помещаем его в секцию “0_head”. Затем мы вводим ещё один пустой массив и помещаем его в секцию “9_tail”, это уже конец массива. Для метки конца массива стоит выдумать какое-нибудь хитрое неиспользуемое имя, для чего уже введен макрос __ARRAY_SPREAD_PRIVATE. Собственно, всё! Теперь мы можем помещать элементы в правильную секцию и обращаться к ним как к элементам массива.

Давайте введем макрос для этих целей:
#define ARRAY_SPREAD_ADD(name, item)                                       \
    static typeof(name[0]) name ## __element[]  \
        __attribute__ ((used,                                               \
                section(__ARRAY_SPREAD_SECTION(name, "1_body")))) = { item }

Точно так же, как и с метками, объявляем массив и помещаем его в секцию. Отличием является имя подсекции “1_body” и то, что это не пустой массив, а массив с единственным элементом, переданным в качестве аргумента. Кстати, с помощью лёгкой модификации можно добавлять произвольное количество элементов в массив, но чтобы не загружать статью, не буду её здесь приводить. Дополненный вариант можно найти у в нашем репозитории.

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

Итак, введём пару вспомогательных макросов.
Макрос конкатенирует две строки:
#define MACRO_CONCAT(m1, m2) __MACRO_CONCAT(m1, m2)
#define __MACRO_CONCAT(m1, m2) m1 ## m2

Макрос добавляющий к символу _at_line_ и номер строки:
#define MACRO_GUARD(symbol) __MACRO_GUARD(symbol)
#define __MACRO_GUARD(symbol) MACRO_CONCAT(symbol ## _at_line_, __LINE__)

И, наконец, макрос добавляющий нам уникальное имя для данного файла, точнее, не уникальное, но оооочень редкое :)
#define __ARRAY_SPREAD_GUARD(array_nm) \
    MACRO_GUARD(__ARRAY_SPREAD_PRIVATE(array_nm, element))

Перепишем макрос для добавления элемента:
#define ARRAY_SPREAD_ADD(name, item)                                       \
    static typeof(name[0]) __ARRAY_SPREAD_GUARD(name)[]  \
        __attribute__ ((used,                                               \
                section(__ARRAY_SPREAD_SECTION(name, "1_body")))) = { item }

Чтобы получить размер массива, нужно взять адрес-маркер последнего элемента и вычесть из него маркер начала массива, размер элементов при этом можно не учитывать, так как операция выполняется в адресной арифметике из-за того, что метка определена как массив данного типа.
#define ARRAY_SPREAD_SIZE(array_name) \
((size_t) (__ARRAY_SPREAD_PRIVATE(array_name, tail) - array_name))

Для красоты добавим синтаксического сахара в виде макроса foreach
#define array_spread_foreach(element, array) \
for (typeof(element) volatile const *_ptr = (array),         \
_end = _ptr + (ARRAY_SPREAD_SIZE(array));       \
(_ptr < _end) && (((element) = *_ptr) || 1); ++_ptr)


Синтаксис unit-тестов


Вернёмся к unit-тестам. У нас в проекте эталонным синтаксисом для unit-тестов считается синтаксис googletest. Что в нём важно:
  • Присутствует объявление наборов тестов
  • Присутствуют объявления отдельных тестов
  • Присутствуют функции пред- и пост- вызовов как для отдельных тестов, так и для наборов тестов
  • Присутствуют всевозможные проверки условий правильности прохождения тестов

Давайте попробуем сформулировать синтаксис на языке Си с учётом массивов переменной длины, описанных в предыдущем разделе. Объявление набора тестов — это объявление массива.
ARRAY_SPREAD_DEF(test_routine_t,all_tests);

static int test_func_1(void) {
    return 0;
}
ARRAY_SPREAD_ADD(all_tests, test_func_1);

static int test_func_2(void) {
    return 0;
}
ARRAY_SPREAD_ADD(all_tests, test_func_2);

Соответственно, вызов тестов можно записать так:
array_spread_foreach(test, all_tests) {
        if (test()) {
            printf("error in test 0x%zu\n", (uintptr_t)test);
            return 0;
        }
        printf(".");
    }
    printf("OK\n");

Естественно, пример сильно упрощён, но уже сейчас видно, что если в тесте произойдёт ошибка, выведется адрес функции, что не очень информативно. Можно, конечно, помудрить с таблицей символов, раз уж мы по-жесткому используем линкер, но ещё приятнее будет, если синтаксис объявления теста будет иметь вид:
TEST_CASE(“test1 description”) {
};

Проще читать развернутый комментарий, чем название функции. Чтобы сделать поддержку этого, введём структуру описания теста. Кроме функции вызова она должна содержать и поле описания:
struct test_case_desc {
    test_routine_t routine;
    char desc[];
};

Тогда вызов всех тестов будет выглядеть следующим образом:
    printf("%zu tests start", ARRAY_SPREAD_SIZE(all_tests));

    array_spread_foreach(test, all_tests) {
        if (test->routine()) {
            printf("error in test 0x%s\n", test->desc);
            return 0;
        }
        printf(".");
    }
    printf("OK\n");

А для того, чтобы ввести отдельный тест, воспользуемся еще раз макросом __LINE__.
Тогда тест, объявленный на данной строке, будет объявлять функцию теста как test_## __LINE__, а весь макрос можно будет записать так:
#define TEST_CASE(desc) \
    __TEST_CASE_NM("" desc, MACRO_GUARD(__test_case_struct), \
                    MACRO_GUARD(__test_case))

#define __TEST_CASE_NM(_desc, test_struct, test_func) \
    static int test_func(void);                      \
    static struct test_case_desc test_struct = { \
            .routine = test_func, \
            .desc = _desc,         \
    };                             \
    ARRAY_SPREAD_ADD(all_tests, &test_struct); \
    static int test_func(void)

Получается довольно красиво. Внутренний макрос введен исключительно для повышения читаемости кода.

Теперь попробуем ввести понятие набора тестов — TEST_SUITE.

Пойдем по проверенному пути. Для каждого набора тестов объявим массив переменной длины, в котором будут храниться структуры с описанием тестов.

Теперь запускать будем не отдельный тест, а набор тестов, который в свою очередь будет вызывать отдельные тесты. Тут мы сталкиваемся с ещё одной проблемой: необходимо объявить все массивы компилируемых тестов, так как нам необходимо знать длину каждого массива. Длину массива можно узнать и без его объявления, если, например, воспользоваться маркером конца массива, как это делается для строк.

Статические массивы переменной длины с терминирующим элементом


Вернёмся к массивам переменной длины. Что же нужно добавить для того, чтобы у нас получился вариант с терминирующим элементом? Давайте поступим так, как мы уже не раз поступали, и добавим терминирующий элемент в специальную подсекцию, которую разместим после элементов массива, но перед маркером конца массива — “8_term”.
То есть, немного перепишем наш предыдущий макрос объявления массива:
#define ARRAY_SPREAD_DEF(element_type, name) \
        ARRAY_SPREAD_TERM_DEF(element_type, name, /* empty */)

#define ARRAY_SPREAD_TERM_DEF(element_type, name, _term) \
        element_type volatile const name[] __attribute__ ((used,        \
        /* Some versions of GCC do not take into an account section     \
         * attribute if it appears after the definition. */             \
            section(__ARRAY_SPREAD_SECTION(name, "0_head")))) =         \
            { /* Empty anchor to the array head. */ };                  \
        element_type volatile const __ARRAY_SPREAD_PRIVATE(name,term)[] \
        __attribute__ ((used,                                           \
        /* Some versions of GCC do not take into an account section     \
         * attribute if it appears after the definition. */             \
            section(__ARRAY_SPREAD_SECTION(name, "8_term")))) =         \
            { _term };                                                    \
        element_type volatile const __ARRAY_SPREAD_PRIVATE(name,tail)[] \
        __attribute__ ((used,                                           \
        /* Some versions of GCC do not take into an account section     \
         * attribute if it appears after the definition. */             \
            section(__ARRAY_SPREAD_SECTION(name, "9_tail")))) =         \
            { /* Empty anchor at the very end of the array. */ }

Добавим макрос foreach() для терминированных нулём массивов
#define array_spread_nullterm_foreach(element, array) \
    __array_spread_nullterm_foreach_nm(element, array,  \
            MACRO_GUARD(__ptr))

#define __array_spread_nullterm_foreach_nm(element, array, _ptr) \
    for (typeof(element) volatile const *_ptr = (array);         \
            ((element) = *_ptr); ++_ptr) 

Наборы тестов


Теперь можно вернуться к наборам тестов.
Они тоже очень простые. Давайте введём структуру для набора тестов:
struct test_suite_desc {
    const struct test_case_desc *volatile const *test_cases;
    char desc[];
};

По сути дела, нам нужны только текстовый дескриптор и указатель на массив тестов.
Давайте введём макрос для объявления набора тестов.
#define TEST_SUITE(_desc) \
    ARRAY_SPREAD_TERM_DEF(static const struct test_case_desc *, \
            __TEST_CASES_ARRAY, NULL /* */);                         \
    static struct test_suite_desc test_suite = {                  \
                    .test_cases = __TEST_CASES_ARRAY,                \
                    .desc = ""_desc,                             \
            };                                                   \
    ARRAY_SPREAD_ADD(all_tests, &test_suite)

Он определяет массив переменной длины для отдельных тестов. С данным массивом вышла неувязка — имя у массива должно быть уникальным, ведь оно не может быть статическим, хоть мы и подумывали о добавлении возможности статического объявления массива. В своём проекте мы используем собственную систему сборки, и для каждого модуля генерим уникальный идентификатор с его полным именем. С ходу проблему не удалось решить и поэтому для объявления набора тестов необходимо задать уникальное имя его массива с тестами.
#define __TEST_CASES_ARRAY test_case_array_1
TEST_SUITE("first test suite");

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

Немного поменяем маскрос для объявления тест-кейса:
#define TEST_CASE(desc) \
    __TEST_CASE_NM("" desc, MACRO_GUARD(__test_case_struct), \
                    MACRO_GUARD(__test_case))

#define __TEST_CASE_NM(_desc, test_struct, test_func)  \
    static int test_func(void);                        \
    static struct test_case_desc test_struct = {       \
            .routine = test_func,                      \
            .desc = _desc,                             \
    };                                                 \
    ARRAY_SPREAD_ADD(__TEST_CASES_ARRAY, &test_struct); \
    static int test_func(void)

По сути, меняется только массив, в который заносится наш тест.

Осталось заменить вызов тестов:
    array_spread_foreach(test_suite, all_tests) {
        printf("%s", test_suite->desc);
        array_spread_nullterm_foreach(test_case, test_suite->test_cases) {
            if (test_case->routine()) {
                printf("error in test 0x%s\n", test_case->desc);
                return 0;
            }
            printf(".");
        }
        printf("OK\n");
    }


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

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

P.S. Автором оригинальной идеи является abusalimov.
Tags:
Hubs:
+25
Comments 3
Comments Comments 3

Articles

Information

Website
emboxing.ru
Registered
Founded
Employees
2–10 employees
Location
Россия