Cで単体テストを自動的に呼び出す別の方法

Habrには、C言語で単体テストを開発する方法に関する記事が既にいくつかあります。 説明したアプローチを批判するつもりはありませんが、 Emboxプロジェクトで使用している別のアプローチを提案するだけです。 数回、すでにHabréでそれを参照しました。



誰も気にしないで、取り組んでください! しかし、私はあなたに警告します:マクロと「リンカー」マジックから多くのフットクロスがあります。



静的可変長配列



この問題について少し掘り下げます。 Cで単体テストを開発するのが複雑な理由は、構文に静的コンストラクターがないことです。 これは、実行するテストですべての関数の呼び出しを明示的に記述する必要があることを意味し、これは非常に不便です。

一方、多数の関数を呼び出すことになると、すぐにポインターの配列について考えます。 つまり、必要なすべての関数を呼び出すには、これらの関数へのポインターの配列を取得し、各要素を参照して、対応する関数を呼び出す必要があります。 したがって、次のような構造があります。

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; }
      
      





すぐに目を引くのは、配列が手動で初期化されることですが、これは便利ではありません。 これを回避する方法を考えて、このような願いを定式化できます。

特定の変数を定義するとき、特定の配列を参照していることを示すことができるはずです。


C言語にはそのようなメカニズムはありませんが、構文を空想しましょう。 次のようになります。

 <type> arr[]; <type> a(array(arr)); <type> b(array(arr));
      
      





または、__ attribute__を使用して表されるgccの拡張メカニズムを使用する場合

 <type> arr[]; <type> a __attribute__(array_member(arr))); <type> b __attribute__(array_member(arr)));
      
      





Cの配列はこの配列の最初の要素への定数ポインターであり、要素は順番に配置され、同じサイズを持っていることを思い出してください。 そのため、特定の変数をメモリに順番に保存する必要があることをコンパイラーに伝えることができれば、独自の配列を編成できます。 少なくとも、実際の配列の要素と同じ方法でこれらの変数を処理できます。



リンカは変数を配置する責任を負いませんが、リンカとリンカスクリプトはこれを行う方法を示します。 これらのスクリプトの構文から、リンカがデータをセクションにグループ化することは明らかであり、特定のセクションに変数のタイプが1つしかない場合、これは本質的に配列になり、配列のラベルを何らかの方法で決定するためにのみ残ります。



配列を定義するとき、その要素のタイプを示します。 したがって、最初の要素を定義し、その要素へのリンクを配列として使用できます。 さらに適切なのは、正しい構文に必要なため、指定したタイプの空の配列を入力することです。

次のようなものが得られます。

 <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”)));
      
      





したがって、配列を作成しますが、もう1つの問題が残っています。この配列のサイズを取得する方法は? 通常の配列では、バイト単位のサイズを最初の要素のサイズで割っただけの場合、この状況ではコンパイラーはセクションのサイズについて何も知りません。 この問題を解決するために、配列の先頭と同じラベルを最後に追加し、アルファベット順のソートを思い出してみましょう。

したがって、次のようになります。

 <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; }
      
      





上記のリンカースクリプトを指定してこのプログラムを実行すると、通常の配列と同じ結果が得られます。 ただし、同時に、1つのファイルに可変長の静的配列を作成できるだけでなく、異なるファイルに分散した配列を作成することもできます。リンカーはアセンブリの最後の段階で機能し、すべてのオブジェクトファイルを1つにまとめるためです。



もちろん、リンカはセクションに配置したオブジェクトのタイプとサイズをチェックしません。同じセクションに異なるタイプのオブジェクトを配置すると、自分にとって邪悪なピノキオになります。 しかし、すべてを注意深く行うと、C言語で可変長の静的配列を作成するためのかなり興味深いメカニズムが得られます。



もちろん、このアプローチは構文の点ではあまり便利ではないため、マクロのすべての魔法を隠す価値があります。



まず、生活を簡素化し、配列、セクション、変数の名前を入力するいくつかの補助マクロを導入します。

最初のものはセクションの名前を単純化します:

 #define __ARRAY_SPREAD_SECTION(array_nm, order_tag) \ ".array_spread." #array_nm order_tag ".rodata,\"a\",%progbits;#"
      
      





2番目は、内部変数(上記の配列の終わりのラベル)を定義します

 #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」の名前と、空の配列ではなく、引数として渡された単一の要素を持つ配列であるという事実です。 ちなみに、簡単な変更の助けを借りて、任意の数の要素を配列に追加できますが、記事を読み込まないために、ここでは説明しません。 拡張バージョンは、リポジトリにあります



このマクロには小さな問題があります。1つのファイルの配列に2つの要素を追加するために使用すると、文字の交差に問題が発生します。 もちろん、上記のマクロを使用して、ファイル内のすべての要素を同時に追加できますが、これはあまり便利ではありません。 したがって、__ LINE__マクロを使用して、変数の一意の文字を取得するだけです。



それでは、いくつかのヘルパーマクロを紹介しましょう。

マクロは2行を連結します。

 #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)
      
      







単体テストの構文



ユニットテストに戻りましょう。 このプロジェクトでは、単体テストの参照構文はgoogletest構文です。 その中で重要なこと:



前のセクションで説明した可変長配列を考慮して、Cで構文を定式化してみましょう。 テストスイートの宣言は、配列宣言です。

 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を紹介してみましょう。



実証済みのパスに行きましょう。 テストの各セットに対して、テストの説明を含む構造が格納される可変長の配列を宣言します。



ここで、個別のテストではなく、一連のテストを実行し、順番に個別のテストを呼び出します。 ここで、別の問題に直面しています。各配列の長さを知る必要があるため、コンパイル済みテストのすべての配列を宣言する必要があります。 配列の長さは宣言せずに見つけることができます。たとえば、文字列の場合と同様に、配列の最後にマーカーを使用する場合です。



終端要素を持つ静的可変長配列



可変長の配列に戻りましょう。 終端要素を持つバリアントを取得するには、何を追加する必要がありますか? 2回以上行ったことを行い、終端要素を特別なサブセクションに追加します。これを配列の要素の後、配列の最後のマーカーが「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");
      
      





そうでなければ、タイピング広告はまともです。

配列に加えて、このセットの構造が決定および初期化され、この構造へのポインターがテストセットのグローバル配列に配置されます。



テストケースを宣言するためにmaskrosを少し変更しましょう。

 #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"); }
      
      







まだ多くの未検討の側面がありますが、主要なアイデアが検討されているので、この時点で記事を完成させたいと思います。 読者にとって興味深い場合は、次の記事に進みます。

結論として、私は何が起こったかのスクリーンショットを提供します:



この記事に記載されているコードは、 別のリポジトリにあります 。 私たちは、このソリューションが面白いことが判明し、これがプロジェクトだけでなく、別のフレームワークとして需要があると考えたので、それを作り始めました。 まあ、同時に記事を書いた、私は興味深いものを願っています。



PS元のアイデアの著者はabusalimovです。



All Articles