「Calendar for ソフトウェアテストの小ネタ | Advent Calendar 2022 - Qiita」の記事です。
C++のメジャーなテスティングフレームワークGoogleTestは、gccやclangに組み込まれたSanitizerと連動することで、不正なメモリ操作や不適切なスレッド間データ共有、リスクある未規定処理の実行などの異常を、テスト上で検出できるようになります。今回はそのGoogleTestとSanitizerの連携で、コードレベルの動的解析の環境を構築する例を解説します。
Sanitizerとの連携
GoogleTestのテストコード上で以下の関数を定義することで、Sanitizerがエラーを検出したときの処理をHookできるようになります。
- address sanitizer :void __asan_on_error()
- 不正なメモリ操作を検出
- behavior sanitizer : void __ubsan_on_report()
- 致命的なエラーや例外を発生させる不正な動作を検出
- thread sanitizer : void __tsan_on_report()
- スレッド間の不適切なデータ共有を検出
例えばGoogleTestのテスト実行中、Address Sanitizerが異常なメモリ操作を検出した際に、特定の文字列を出力するとともにテストをFailさせたいならば、以下をテストコード上で定義します。
extern "C" { void __asan_on_error() { FAIL() << "address sanitizer error!!!!"; } } // extern "C"
注意点として、Sanitizerが異常を検出したタイミングでテストコードは強制終了されます。その際、--gtest_outputによる結果ファイル出力は動作しません。そのため、CIなどで自動実行する際は、XMLなどの出力ファイルでなく、コンソールログ解析でテスト結果を判定する必要があります。
実装例
以下のテスト対象をテストするとします。範囲外への不正なメモリアクセスを行っています。
int target() { int a[5]; return a[5]; }
通常のユニットテストでは、上記を実行した時のふるまいは不定です。ただ特に何もなくテストが終了する場合もあります。
ここで上記のような不正なメモリアクセスをSanitizerで見逃しなく検出させる場合、テストコードを次のように記述します。
//test_hoge.cpp #include <gtest/gtest.h> ... extern "C" { void __asan_on_error() { FAIL() << "Encountered an address sanitizer error"; } } // extern "C" TEST(TestCase, dummy) { EXPECT_EQ(0, target());//サンプル例示のための適当な確認 }
このテストコードをビルドする際に、Address Sanitizerを有効化します。例えば次のようなオプションで記述します。
g++ test_hoge.cpp -lgtest_main -lgtest -lpthread -fsanitize=address -o test_hoge
これを実行すると、a[5]にアクセスしたタイミングで、「Encountered an address sanitizer error」が出力され、テストがFailします。具体的には以下のようなメッセージが出力されます。
[==========] Running 1 tests from 1 test suite. [----------] Global test environment set-up. [----------] 1 tests from TestCase [ RUN ] TestCase.dummy ================================================================= test_hoge.cpp:8: Failure Failed Encountered an address sanitizer error ==468==ERROR: AddressSanitizer: stack-buffer-overflow on address ... 以下、不正メモリアクセスを検出した旨のSanitizerの出力メッセージ ...
上記のようなアプローチで、Sanitizerの機能を使った動的解析を、GoogleTestのテストコードとして記述できるようになります。