
On Habré already
there are several
articles on how to develop
unit tests in the C language. I am not going to criticize the described approaches, but only offer another one - the one that we use in the
Embox project. A couple of times we already referred to it on
Habré .
Who cares, please roll! But I warn you: there are a lot of spoilers from macros and linker magic.
Variable Length Static Arrays
A little dive into the issue. The reason for the complexity of developing unit tests in the C language is the lack of static constructors in the syntax. This means that you need to explicitly describe the calls of all functions with tests that you want to perform, and this, you see, is extremely inconvenient.
On the other hand, when it comes to calling a large number of functions, I immediately have thoughts about an array of pointers. That is, to call all the required functions, it is necessary to take an array of pointers to these functions, refer to each of its elements and call the corresponding function. Thus, there is a design like this:
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; }
It immediately catches the eye that the array is initialized manually, and this is not convenient. Reflecting on how to avoid this, we can formulate this Wishlist:
When defining a variable, we should be able to indicate that it belongs to an array.
Such a mechanism in the C language does not exist, but let's fantasize about the syntax. It might look like this:
<type> arr[]; <type> a(array(arr)); <type> b(array(arr));
Or, if you use the extension mechanism in gcc, which is expressed using __attribute__
<type> arr[]; <type> a __attribute__(array_member(arr))); <type> b __attribute__(array_member(arr)));
It now remains to recall that the array in the C language is a constant pointer to the first element in this array, and the elements are arranged in series and have the same size. So, if we can tell the compiler to put certain variables in memory in sequence, we will be able to organize our own array. At the very least, we can handle these variables in the same way as we can with the elements of a real array.
It is not the compiler that is responsible for the placement of the variables, but the linker, and in the linker scripts it is specified exactly how it should do this. From the syntax of these scripts, it is clear that the linker groups data into sections, and if there are only one type of variables in a certain section, this will be an array in essence, and it will only be necessary to somehow determine the array label.
')
When we define an array, we specify the type of its elements. So, you can define the first element and you can use the reference to it as an array. And even better to enter an empty array of the specified type, since it is still needed for the correct syntax.
It turns out something like this:
<type> arr[] __attribute__((section(“array_spread.arr”))) = {};
In order for the label to point to the beginning of the section, you can use scripts from the linker. By default, the linker places data in an arbitrary order, but if you use the SORT (“section_name”) function, the linker will sort the characters in the section in lexicographical order. Thus, in order for the array symbol to point to the beginning of a section, we need the name of the subsection to lexicographically go before the rest of the array. To do this, it is enough to assign “0_head” to the beginning of the array, and for all variables - “1_body”. Of course, it would be enough just “0” and “1”, but then the program text would become less readable.
Thus, the array declaration will look like this:
<type> arr[] __attribute__((section(“array_spread.arr_0_head.rodata”))) = {};
The linker script itself looks like this:
SECTIONS { .rodata.array_spread : { *(SORT(.array_spread.*.rodata)) } } INSERT AFTER .rodata;
You can connect it using the gcc -T key
In order to indicate that some variable should be placed in one section or another, you need to add the corresponding attribute:
<type> a __attribute__((section(“array_spread.arr_1_body.rodata”)))
Thus, we will form an array, but one more problem remains: how to get the size of this array? If with regular arrays, we simply took its size in bytes and divided it by the size of the first element, then in this situation the compiler knows nothing about the size of the section. To solve this problem, let us, as with the beginning of the array, add the same label to the end, again remembering the alphabetic sorting.
So, we get the following:
<type> arr_tail[] __attribute__((section(“array_spread.arr_9_tail.rodata”))) = {};
Now that we have all the necessary information on how to create an array, let's try to rewrite the previous example:
#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; }
If you run this program by specifying the above linker script, we get the same result as with regular arrays. But you can not only create a static array of variable length in one file, but also an array distributed across different files, since the linker works at the last stage of the assembly, collecting all the object files into one, and this is sometimes very useful.
Of course, the linker does not check the type and size of the objects that you placed in the section, and if you place objects of different types in the same section, you will be angry with Buratino yourself. But if you do everything carefully, you will get a rather interesting mechanism for creating static arrays of variable length in the C language.
Of course, this approach is not very convenient in terms of syntax, so it is worth hiding all the magic in macros.
To begin with, simplify your life and introduce a couple of auxiliary macros that introduce the names of arrays, sections and variables.
The first simplifies the section name:
#define __ARRAY_SPREAD_SECTION(array_nm, order_tag) \ #array_nm order_tag a\
The second defines an internal variable (the end-of-array label described above)
#define __ARRAY_SPREAD_PRIVATE(array_nm, private_nm) \ __array_spread__##array_nm##__##private_nm
Now we define a macro that gets the array.
#define ARRAY_SPREAD_DEF(element_type, name) \ element_type volatile const name[] __attribute__ ((used, \ \ section(__ARRAY_SPREAD_SECTION(name, "0_head")))) = \ { }; \ element_type volatile const __ARRAY_SPREAD_PRIVATE(name,tail)[] \ __attribute__ ((used, \ \ section(__ARRAY_SPREAD_SECTION(name, "9_tail")))) = \ { }
Actually, this is the previously used code wrapped in a macro.
First we enter the label of the beginning of the array - an empty array, and put it in the “0_head” section. Then we enter another empty array and put it in the “9_tail” section, this is the end of the array. For the end of the array label, it is worthwhile to invent some tricky unused name, for which the macro __ARRAY_SPREAD_PRIVATE has already been entered. Actually, everything! Now we can put elements in the right section and access them as elements of an array.
Let's introduce a macro for this purpose:
#define ARRAY_SPREAD_ADD(name, item) \ static typeof(name[0]) name ## __element[] \ __attribute__ ((used, \ section(__ARRAY_SPREAD_SECTION(name, "1_body")))) = { item }
In the same way as with labels, we declare an array and place it into a section. The difference is the name of the subsection “1_body” and the fact that it is not an empty array, but an array with a single element passed as an argument. By the way, with the help of a light modification, you can add an arbitrary number of elements to the array, but in order not to load the article, I will not give it here. An updated version can be found in
our repository .
This macro has a small problem: if with its help we add two elements to the array in one file, there will be a problem with the intersection of characters. Of course, you can use the macro described above and add all the elements in the file at the same time, but, you see, this is not very convenient. Therefore, we simply use the macro __LINE__ and get unique symbols for the variables.
So, we introduce a couple of auxiliary macros.
The macro concatenates two lines:
#define MACRO_CONCAT(m1, m2) __MACRO_CONCAT(m1, m2) #define __MACRO_CONCAT(m1, m2) m1 ## m2
The macro that adds to the _at_line_ character and the line number:
#define MACRO_GUARD(symbol) __MACRO_GUARD(symbol) #define __MACRO_GUARD(symbol) MACRO_CONCAT(symbol ## _at_line_, __LINE__)
And finally, the macro that adds us a unique name for this file, or rather, is not unique, but it is sooooo rare :)
#define __ARRAY_SPREAD_GUARD(array_nm) \ MACRO_GUARD(__ARRAY_SPREAD_PRIVATE(array_nm, element))
Rewrite the macro to add an item:
#define ARRAY_SPREAD_ADD(name, item) \ static typeof(name[0]) __ARRAY_SPREAD_GUARD(name)[] \ __attribute__ ((used, \ section(__ARRAY_SPREAD_SECTION(name, "1_body")))) = { item }
To get the size of the array, you need to take the address marker of the last element and subtract from it the marker of the beginning of the array;
#define ARRAY_SPREAD_SIZE(array_name) \ ((size_t) (__ARRAY_SPREAD_PRIVATE(array_name, tail) - array_name))
For beauty, add syntactic sugar in the form of a macro 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 test syntax
Let's return to unit tests. In our project, the standard syntax for unit tests is the
googletest syntax. What is important in it:
- Present test suite ad
- Present advertisements for individual tests.
- There are pre- and post-call functions for both individual tests and test suites.
- There are all sorts of checks to ensure that the test passes.
Let's try to formulate the syntax in C, taking into account the variable-length arrays described in the previous section. A test suite declaration is an array declaration.
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);
Accordingly, the test call can be written as:
array_spread_foreach(test, all_tests) { if (test()) { printf("error in test 0x%zu\n", (uintptr_t)test); return 0; } printf("."); } printf("OK\n");
Naturally, the example is greatly simplified, but it is already clear that if an error occurs in the test, the address of the function is displayed, which is not very informative. You can, of course, ponder with the symbol table, since we use a linker in a hard way, but it will be even more pleasant if the syntax of the test declaration is:
TEST_CASE(“test1 description”) { };
It is easier to read a detailed comment than the name of the function. To support this, we introduce a test description structure. In addition to the call function, it must contain a description field:
struct test_case_desc { test_routine_t routine; char desc[]; };
Then the call of all tests will look as follows:
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");
And in order to introduce a separate test, we will use the __LINE__ macro again.
Then the test declared on this line will declare the test function as test _ ## __LINE__, and the whole macro can be written like this:
#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)
It turns out quite beautiful. The internal macro is introduced solely to improve the readability of the code.
Now we will try to introduce the concept of a test suite - TEST_SUITE.
Let's go on a proven path. For each set of tests, we will declare a variable length array in which the structures with the description of the tests will be stored.
Now we will run not a separate test, but a test suite, which, in turn, will trigger individual tests. Here we are faced with another problem: it is necessary to declare all arrays of compiled tests, since we need to know the length of each array. You can find the length of an array without its declaration, for example, if you use the end of an array marker, as it does for strings.
Variable-length static arrays with terminating element
Let's return to arrays of variable length. What should be added in order for us to have a version with a terminating element? Let's do what we have done more than once, and add a termination element to a special subsection that we place after the array elements, but before the end of the array marker - “8_term”.
That is, let's rewrite our previous array declaration macro:
#define ARRAY_SPREAD_DEF(element_type, name) \ ARRAY_SPREAD_TERM_DEF(element_type, name, ) #define ARRAY_SPREAD_TERM_DEF(element_type, name, _term) \ element_type volatile const name[] __attribute__ ((used, \ \ section(__ARRAY_SPREAD_SECTION(name, "0_head")))) = \ { }; \ element_type volatile const __ARRAY_SPREAD_PRIVATE(name,term)[] \ __attribute__ ((used, \ \ section(__ARRAY_SPREAD_SECTION(name, "8_term")))) = \ { _term }; \ element_type volatile const __ARRAY_SPREAD_PRIVATE(name,tail)[] \ __attribute__ ((used, \ \ section(__ARRAY_SPREAD_SECTION(name, "9_tail")))) = \ { }
Add a foreach () macro for zero-terminated arrays
#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)
Test suites
Now you can go back to the test sets.
They are also very simple. Let's enter the structure for the test suite:
struct test_suite_desc { const struct test_case_desc *volatile const *test_cases; char desc[]; };
As a matter of fact, we need only a text descriptor and a pointer to an array of tests.
Let's enter a macro to declare a test suite.
#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)
It defines a variable length array for individual tests. There was a problem with this array - the name of the array should be unique, because it cannot be static, even though we were thinking about adding the possibility of a static array declaration. In our project we use
our own build system , and for each module we generate a unique identifier with its full name. The problem could not be solved right away, and therefore, to declare a test suite, you must specify the unique name of its test array.
#define __TEST_CASES_ARRAY test_case_array_1 TEST_SUITE("first test suite");
As for the rest, the typing ad looks decent.
In addition to an array, the structure of this set is determined and initialized, and a pointer to this structure is placed in the global array of test sets.
Let's slightly change the mask for the announcement of the test case:
#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)
In fact, only the array in which our test is entered changes.
It remains to replace the test call:
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"); }
We have a lot of unexamined aspects left, but I want to complete the article at this point, since the main idea has been considered. If readers will be interested continue in the next article.
In conclusion, I will give a screenshot of what happened:
The code given in the article lies in our
separate repository . We thought that the decision turned out to be interesting and it can be in demand as a separate framework not only in
our project , and therefore we began to endure it. Well, at the same time wrote an article, I hope interesting.
PS The author of the original idea is
abusalimov .