Eine andere Möglichkeit, Komponententests in C automatisch aufzurufen

    Habré bereits hat einige Artikel darüber , wie die Entwicklung von Unit - Tests in C - Sprache. Ich werde die beschriebenen Ansätze nicht kritisieren, sondern nur einen anderen vorschlagen - den, den wir im Embox- Projekt verwenden . Einige Male haben wir bereits auf Habré darauf hingewiesen .

    Wen kümmert es, bitte packen! Aber ich warne Sie: Es gibt viele Fußtücher aus Makros und "Linker" -Magie.

    Statische Arrays mit variabler Länge


    Wir werden uns ein wenig mit dem Thema befassen. Der Grund für die Komplexität der Entwicklung von Komponententests in C ist das Fehlen statischer Konstruktoren in der Syntax. Dies bedeutet, dass Sie die Aufrufe aller Funktionen mit den Tests, die Sie ausführen möchten, explizit beschreiben müssen, und dies ist, Sie werden zustimmen, äußerst unpraktisch.
    Wenn es andererseits darum geht, eine große Anzahl von Funktionen aufzurufen, denke ich sofort über eine Reihe von Zeigern nach. Das heißt, um alle erforderlichen Funktionen aufzurufen, müssen Sie ein Array von Zeigern auf diese Funktionen nehmen, auf jedes seiner Elemente verweisen und die entsprechende Funktion aufrufen. Es gibt also eine Konstruktion wie diese:
    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;
    }
    

    Was sofort auffällt, ist, dass das Array manuell initialisiert wird, was jedoch nicht praktisch ist. Wenn wir darüber nachdenken, wie wir dies vermeiden können, können wir einen solchen Wunsch formulieren:
    Wenn Sie eine bestimmte Variable definieren, sollten Sie angeben können, dass sie sich auf ein bestimmtes Array bezieht.

    Es gibt keinen solchen Mechanismus in der C-Sprache, aber lasst uns über die Syntax phantasieren. Es könnte so aussehen:
     arr[];
     a(array(arr));
     b(array(arr));
    

    Oder wenn Sie den Erweiterungsmechanismus in gcc verwenden, der mit __attribute__ ausgedrückt wird.
     arr[];
     a __attribute__(array_member(arr)));
     b __attribute__(array_member(arr)));
    

    Es bleibt zu erinnern, dass ein Array in C ein konstanter Zeiger auf das erste Element in diesem Array ist und die Elemente nacheinander angeordnet sind und dieselbe Größe haben. Wenn wir also dem Compiler mitteilen können, dass bestimmte Variablen nacheinander gespeichert werden müssen, können wir unser eigenes Array organisieren. Zumindest können wir mit diesen Variablen genauso umgehen wie mit Elementen eines realen Arrays.

    Der Linker ist nicht für die Platzierung der Variablen verantwortlich, aber der Linker und die Linkerskripte geben an, wie dies erfolgen soll. Aus der Syntax dieser Skripte geht hervor, dass der Linker die Daten in Abschnitte gruppiert. Wenn es in einem bestimmten Abschnitt nur einen Variablentyp gibt, handelt es sich im Wesentlichen um ein Array, und es bleibt nur zu bestimmen, wie das Array beschriftet ist.

    Wenn wir ein Array definieren, geben wir den Typ seiner Elemente an. Sie können also das erste Element definieren und den Link dazu als Array verwenden. Geben Sie besser noch ein leeres Array des angegebenen Typs ein, da Sie es für die korrekte Syntax noch benötigen.
    Sie erhalten so etwas:
     arr[] __attribute__((section(“array_spread.arr”))) = {};
    

    Damit die Beschriftung den Beginn des Abschnitts anzeigt, können Sie Skripte aus dem Linker verwenden. Standardmäßig platziert der Linker die Daten in zufälliger Reihenfolge. Wenn Sie jedoch die SORT-Funktion („Abschnittsname“) verwenden, sortiert der Linker die Zeichen im Abschnitt in lexikografischer Reihenfolge. Damit das Array-Symbol auf den Beginn des Abschnitts verweist, muss der Name des Unterabschnitts lexikografisch vor dem Rest des Arrays stehen. Zu diesem Zweck weisen Sie dem Anfang des Arrays einfach "0_head" und für alle Variablen "1_body" zu. Natürlich würden nur "0" und "1" ausreichen, aber dann würde der Text des Programms weniger lesbar werden.

    Die Array-Deklaration sieht also so aus:
     arr[] __attribute__((section(“array_spread.arr_0_head.rodata”))) = {};
    

    Das Linker-Skript selbst sieht wie folgt aus:
    SECTIONS {
        .rodata.array_spread : {
            *(SORT(.array_spread.*.rodata))
        }
    }
    INSERT AFTER .rodata;
    

    Sie können es mit dem Schalter gcc -T verbinden.

    Um anzuzeigen, dass eine Variable in einem bestimmten Abschnitt platziert werden muss, müssen Sie das entsprechende Attribut hinzufügen:
     a __attribute__((section(“array_spread.arr_1_body.rodata”)));
    

    Wir werden also ein Array bilden, aber es bleibt noch ein Problem: Wie erhält man die Größe dieses Arrays? Wenn wir bei normalen Arrays nur die Größe in Bytes genommen und durch die Größe des ersten Elements dividiert haben, weiß der Compiler in dieser Situation nichts über die Größe des Abschnitts. Um dieses Problem zu lösen, fügen wir am Ende dasselbe Label wie am Anfang des Arrays hinzu, wobei wir uns wieder an die alphabetische Sortierung erinnern.
    Wir erhalten also Folgendes:
     arr_tail[] __attribute__((section(“array_spread.arr_9_tail.rodata”))) = {};
    

    Nachdem wir nun alle erforderlichen Informationen zum Erstellen eines Arrays erhalten haben, versuchen wir, das vorherige Beispiel neu zu schreiben:
    #include 
    #include 
    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;
    }
    

    Wenn Sie dieses Programm unter Angabe des obigen Linkerskripts ausführen, erhalten Sie dasselbe Ergebnis wie bei normalen Arrays. Gleichzeitig können Sie jedoch nicht nur ein statisches Array variabler Länge in einer Datei erstellen, sondern auch ein Array, das auf verschiedene Dateien verteilt ist, da der Linker in der letzten Phase der Assembly alle Objektdateien in einer sammelt. Dies ist manchmal sehr nützlich.

    Natürlich überprüft der Linker nicht den Typ und die Größe der Objekte, die Sie in den Abschnitt eingefügt haben, und wenn Sie Objekte unterschiedlicher Typen in den gleichen Abschnitt einfügen, werden Sie selbst böser Pinocchio. Wenn Sie jedoch alles sorgfältig ausführen, erhalten Sie einen interessanten Mechanismus zum Erstellen von statischen Arrays variabler Länge in der C-Sprache.

    Natürlich ist dieser Ansatz in Bezug auf die Syntax nicht sehr praktisch, daher lohnt es sich, die ganze Magie in Makros zu verstecken.

    Vereinfachen Sie zunächst unser Leben und führen Sie einige Hilfsmakros ein, die die Namen von Arrays, Abschnitten und Variablen eingeben.
    Das erste vereinfacht den Namen der Abschnitte:
    #define __ARRAY_SPREAD_SECTION(array_nm, order_tag) \
        ".array_spread." #array_nm order_tag ".rodata,\"a\",%progbits;#"
    

    Die zweite definiert eine interne Variable (das oben beschriebene Label für das Ende des Arrays).
    #define __ARRAY_SPREAD_PRIVATE(array_nm, private_nm) \
        __array_spread__##array_nm##__##private_nm
    

    Definieren Sie nun das Makro, das das Array auflöst.
    #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. */ }
    

    Tatsächlich ist dies der zuvor verwendete Code, der in ein Makro eingebunden ist.
    Zuerst geben wir das Label am Anfang des Arrays ein - ein leeres Array - und fügen es in den Abschnitt „0_head“ ein. Dann stellen wir ein weiteres leeres Array vor und platzieren es im Abschnitt „9_tail“. Dies ist das Ende des Arrays. Es lohnt sich, für das Ende des Arrays einen kniffligen, nicht verwendeten Namen zu erfinden, für den bereits das Makro __ARRAY_SPREAD_PRIVATE eingegeben wurde. Eigentlich ist es das! Jetzt können wir die Elemente in den richtigen Abschnitt einfügen und sie als Elemente eines Arrays bezeichnen.

    Lassen Sie uns ein Makro für diese Zwecke einführen:
    #define ARRAY_SPREAD_ADD(name, item)                                       \
        static typeof(name[0]) name ## __element[]  \
            __attribute__ ((used,                                               \
                    section(__ARRAY_SPREAD_SECTION(name, "1_body")))) = { item }
    

    Wie bei Labels deklarieren wir ein Array und platzieren es in einem Abschnitt. Der Unterschied ist der Name des Unterabschnitts "1_body" und die Tatsache, dass es sich nicht um ein leeres Array handelt, sondern um ein Array mit einem einzelnen Element, das als Argument übergeben wird. Übrigens können Sie mit Hilfe der einfachen Modifikation eine beliebige Anzahl von Elementen zum Array hinzufügen, aber um den Artikel nicht zu laden, werde ich ihn hier nicht angeben. Eine erweiterte Version finden Sie in unserem Repository .

    Dieses Makro hat ein kleines Problem: Wenn Sie es verwenden, um dem Array in einer Datei zwei Elemente hinzuzufügen, liegt ein Problem mit der Schnittmenge von Zeichen vor. Natürlich können Sie das oben beschriebene Makro verwenden und alle Elemente in der Datei gleichzeitig hinzufügen, aber Sie sehen, das ist nicht sehr praktisch. Verwenden Sie daher einfach das Makro __LINE__ und rufen Sie eindeutige Zeichen für die Variablen ab.

    Lassen Sie uns ein paar Hilfsmakros vorstellen.
    Ein Makro verkettet zwei Zeilen:
    #define MACRO_CONCAT(m1, m2) __MACRO_CONCAT(m1, m2)
    #define __MACRO_CONCAT(m1, m2) m1 ## m2
    

    Makro zum _at_line_-Zeichen und zur Zeilennummer hinzufügen:
    #define MACRO_GUARD(symbol) __MACRO_GUARD(symbol)
    #define __MACRO_GUARD(symbol) MACRO_CONCAT(symbol ## _at_line_, __LINE__)
    

    Und schließlich ein Makro, das uns einen eindeutigen Namen für eine bestimmte Datei hinzufügt, oder besser gesagt, nicht eindeutig, aber sooo selten :)
    #define __ARRAY_SPREAD_GUARD(array_nm) \
        MACRO_GUARD(__ARRAY_SPREAD_PRIVATE(array_nm, element))
    

    Schreiben Sie das Makro neu, um das Element hinzuzufügen:
    #define ARRAY_SPREAD_ADD(name, item)                                       \
        static typeof(name[0]) __ARRAY_SPREAD_GUARD(name)[]  \
            __attribute__ ((used,                                               \
                    section(__ARRAY_SPREAD_SECTION(name, "1_body")))) = { item }
    

    Um die Größe des Arrays zu ermitteln, müssen Sie die Adressmarkierung des letzten Elements und die Markierung des Arrayanfangs davon abziehen. Die Größe der Elemente kann nicht berücksichtigt werden, da die Operation in Adressarithmetik ausgeführt wird, da die Bezeichnung als Array dieses Typs definiert ist.
    #define ARRAY_SPREAD_SIZE(array_name) \
    ((size_t) (__ARRAY_SPREAD_PRIVATE(array_name, tail) - array_name))
    

    Fügen Sie für Schönheit syntaktischen Zucker als foreach Makro hinzu
    #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


    Kehren wir zu den Unit-Tests zurück. In unserem Projekt ist die Referenzsyntax für Komponententests die Googletest- Syntax . Was ist daran wichtig:
    • Ankündigung der aktuellen Testsuite
    • Es gibt Ankündigungen von Einzeltests
    • Es gibt Pre- und Post-Call-Funktionen für einzelne Tests und Testsuiten
    • Es gibt alle Arten von Prüfungen auf Richtigkeit der Prüfungen

    Versuchen wir, die Syntax in C unter Berücksichtigung der im vorherigen Abschnitt beschriebenen Arrays variabler Länge zu formulieren. Eine Testsuite-Deklaration ist eine Array-Deklaration.
    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);
    

    Dementsprechend kann der Testaufruf folgendermaßen geschrieben werden:
    array_spread_foreach(test, all_tests) {
            if (test()) {
                printf("error in test 0x%zu\n", (uintptr_t)test);
                return 0;
            }
            printf(".");
        }
        printf("OK\n");
    

    Das Beispiel ist natürlich stark vereinfacht, aber es ist bereits klar, dass im Falle eines Fehlers im Test die Funktionsadresse angezeigt wird, was nicht sehr informativ ist. Sie können natürlich dumm mit der Symboltabelle sein, da wir den Linker auf harte Weise verwenden, aber es wird noch angenehmer sein, wenn die Syntax der Testdeklaration so aussieht:
    TEST_CASE(“test1 description”) {
    };
    

    Es ist einfacher, einen ausführlichen Kommentar zu lesen als den Namen einer Funktion. Um dies zu unterstützen, führen wir eine Testbeschreibungsstruktur ein. Zusätzlich zur Aufruffunktion muss ein Beschreibungsfeld enthalten sein:
    struct test_case_desc {
        test_routine_t routine;
        char desc[];
    };
    

    Dann sieht der Aufruf aller Tests so aus:
        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");
    

    Und um einen separaten Test einzuführen, verwenden wir wieder das Makro __LINE__.
    Dann deklariert der in dieser Zeile deklarierte Test die Testfunktion als test _ ## __LINE__, und das gesamte Makro kann folgendermaßen geschrieben werden:
    #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)
    

    Es fällt ziemlich gut aus. Ein internes Makro wurde eingeführt, um die Lesbarkeit des Codes zu verbessern.

    Versuchen wir nun, das Konzept einer Testsuite - TEST_SUITE - vorzustellen.

    Gehen wir einen bewährten Weg. Für jeden Satz von Tests deklarieren wir ein Array variabler Länge, in dem Strukturen mit einer Beschreibung der Tests gespeichert werden.

    Jetzt werden wir keinen separaten Test ausführen, sondern eine Reihe von Tests, die wiederum separate Tests aufrufen. Hier haben wir ein weiteres Problem: Es ist notwendig, alle Arrays kompilierter Tests zu deklarieren, da wir die Länge jedes Arrays kennen müssen. Die Länge des Arrays kann gefunden werden, ohne es zu deklarieren, wenn Sie beispielsweise die Markierung am Ende des Arrays verwenden, wie dies für Zeichenfolgen der Fall ist.

    Statische Arrays variabler Länge mit Abschlusselement


    Kehren wir zu Arrays variabler Länge zurück. Was muss hinzugefügt werden, damit wir eine Variante mit einem Abschlusselement erhalten? Machen wir das, was wir schon mehrmals gemacht haben, und fügen einem speziellen Unterabschnitt ein Abschlusselement hinzu, das wir nach den Elementen des Arrays, aber vor dem Marker am Ende des Arrays - „8_term“ - einfügen.
    Das heißt, wir schreiben unser vorheriges Array-Deklarationsmakro ein wenig um:
    #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. */ }
    

    Fügen Sie für nullterminierte Arrays das Makro foreach () hinzu
    #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) 
    

    Testsuiten


    Jetzt können Sie zu den Testsuiten zurückkehren.
    Sie sind auch sehr einfach. Lassen Sie uns eine Struktur für eine Testsuite einführen:
    struct test_suite_desc {
        const struct test_case_desc *volatile const *test_cases;
        char desc[];
    };
    

    Tatsächlich benötigen wir nur einen Textdeskriptor und einen Zeiger auf ein Array von Tests.
    Lassen Sie uns ein Makro einführen, um eine Testsuite zu deklarieren.
    #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)
    

    Es definiert ein Array variabler Länge für einzelne Tests. Bei diesem Array ist ein Problem aufgetreten. Der Name des Arrays muss eindeutig sein, da er nicht statisch sein kann, obwohl wir darüber nachgedacht haben, die Möglichkeit der statischen Deklaration des Arrays hinzuzufügen. In unserem Projekt verwenden wir unser eigenes Build-System und generieren für jedes Modul einen eindeutigen Bezeichner mit seinem vollständigen Namen. Das Problem konnte nicht sofort behoben werden. Um eine Testsuite zu deklarieren, müssen Sie einen eindeutigen Namen für das Array der Tests angeben.
    #define __TEST_CASES_ARRAY test_case_array_1
    TEST_SUITE("first test suite");
    

    Ansonsten sieht die Tippanzeige anständig aus.
    Zusätzlich zum Array wird die Struktur dieses Sets bestimmt und initialisiert, und ein Zeiger auf diese Struktur wird im globalen Array von Test-Sets platziert.

    Lassen Sie uns die Maskros ein wenig ändern, um einen Testfall zu deklarieren:
    #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)
    

    Tatsächlich ändert sich nur das Array, in das unser Test eingegeben wird.

    Es bleibt der Testanruf zu ersetzen:
        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");
        }
    


    Wir haben noch viele ungeprüfte Aspekte, aber ich möchte den Artikel an dieser Stelle vervollständigen, da die Hauptidee berücksichtigt wurde. Wenn es für die Leser interessant sein wird, werde ich im nächsten Artikel fortfahren.
    Abschließend gebe ich einen Screenshot von dem, was passiert ist:

    Der Code in dem Artikel befindet sich in unserem separaten Repository . Wir fanden, dass die Lösung interessant war und dass dies nicht nur in unserem Projekt als separater Rahmen gefragt sein könnte. Deshalb haben wir begonnen, sie zu entwickeln. Nun, zur gleichen Zeit schrieb ich einen Artikel, ich hoffe, ein interessanter.

    PS Der Autor der ursprünglichen Idee ist Abusalimov .

    Jetzt auch beliebt: