libift

Branch Coverage 100% とかって戸愚呂弟めいた条件に対し,mallocとかfopenとかそういうやつの失敗ルートをどう確認する?みたいな話が出ることがあると思う.define置換する方法とかあるみたいだけど,余計な場所まで根刮ぎ失敗させちゃったり,別のmallocって識別子があったらそっちを阻害しちゃったりとかあまり良いこと無い.

そこで,ホワイトボックスでmalloc等の失敗発生箇所を少しずつズラしながらエラーケースを通してみるためのライブラリlibift.関数毎に"関数名"_failable__(か,失敗要因が複数ある関数の場合は,"関数名"_failable_by__(errno))という,"その位置からの初回呼び出しのみ指定された原因で失敗するスコープ"が使えるようになる.1回失敗させた箇所は記憶され,2回目以降の呼び出しには影響を与えない.…なんか説明ヘタクソだな.

例えば,テスト対象のソース(foo.c)が,

static int foo()
{
    void* p = NULL;
    void* q = NULL;
    p = malloc(10);
    if (NULL == p) return 1;
    q = malloc(10);
    if (NULL == q)
    {
        free(p);
        return 2;
    }
    free(p);
    free(q);
    return 0;
}

こんな関数だとして,単体テストフレームワーク上(以下はgoogletestの場合)で,テスト(unittest.cpp)

#include "foo.c"

TEST(SampleTest,TestFoo)
{
    ASSERT_EQ(0, foo()); // 普通に成功する
    malloc_failable__
    {   // mallocが失敗するスコープ
        ASSERT_EQ(1, foo()); // pのほうが失敗する
        ASSERT_EQ(2, foo()); // pのほうはさっき失敗したので成功し,今度はqのほうが失敗する
        ASSERT_EQ(0, foo()); // pもqも成功する
    }
    ASSERT_EQ(0, foo()); // 普通に成功する
    malloc_failable__
    {   // mallocが失敗するスコープ
        ASSERT_EQ(0, foo()); // pもqも成功する.以下同じ
        ASSERT_EQ(0, foo());
        ASSERT_EQ(0, foo());
    }
    ASSERT_EQ(0, test_target());
}

int main(int argc, char** argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

のように書いて,-liftをリンクしてコンパイルした上でruniftを通して実行,

$ runift unittest

グリーンになるはず.

利点は以下のような感じ,

  • テスト対象(foo.c)に手を加える必要が無い
  • 失敗と成功の発生がある程度制御できるので
    • マクロで失敗したことにするやりかたのように,根刮ぎ失敗させてしまわない
    • ランダムで失敗させるやりかたのように,テストが書きにくくなったり,再現させにくくなったりしない
  • 失敗スコープの外には影響が無い
  • もちろん複数の失敗スコープを以下のように組み合わせて開くこともできる
malloc_failable__ fopen_failable_by__(ENOENT)
{   // mallocが失敗,fopenがENOENTで失敗するスコープ
    // テストコード
    realloc_failable__ 
    {   // 加えてreallocも失敗するスコープ
        // テストコード
    }
}

欠点は,

  • 思わぬものが失敗対象を使っていて謎の失敗を起こしたりするかもしれない
    • 例えば(といってもこれは当然ぽい例だけど)malloc_failable__中だと大抵のコンテナがbad_allocするとか
  • 思わぬものが実際には使われていなくて失敗が起きないかもしれない
    • 例えば最適化によって起き換えられてしまうようなもの
    • printfは引数と最適化オプションによっては実態がputsになってしまっていたり
  • それでも通せないルートは作れる
    • けど,そんな分岐があるならコード見直すのがオススメ

標準関数で必要っぽいものについては大体OKだと思うけど,fcntl系とかsocket系とかまだ無い.