2012年1月31日火曜日

ASSERT_EQ(NULL, ptr) がコンパイルエラーにならない理由

社内テストフレームワーク用に、ASSERT_EQ を実装した時のお話です。

これまでのテストは、 ASSERT のみで記述されていました。
しかし、それではログから必要な情報を十分に得ることができなかったため、
比較マクロを追加することにしました。

まず、書いたのが以下のようなコード
#define COMPARE_HELPER(op_name, op) \
template<typename T1, typename T2>  \
static AssertionResult CmpHelper##op_name( \
            const char* expr1, const char* expr2  \
            , const T1& val1, const T2& val2) { \
    if( val1 op val2 ) return AssertionResult::Success();   \
    else {  \
        return AssertionResult::Failure() \
            << "error: Expected: " << expr1 << " " #op " " << expr2   \
            << "\n  Actual: " << val1 << " vs " << val2; \
    }   \
}

COMPARE_HELPER(EQ, ==)
COMPARE_HELPER(NE, !=)
COMPARE_HELPER(LE, <=)
COMPARE_HELPER(LT, < )
COMPARE_HELPER(GE, >=)
COMPARE_HELPER(GT, > )
比較テストマクロの部分は省いてありますが、
ASSERT_EQ(expected, actual) を書くと、CmpHelperEQ 関数が呼ばれるようになってます。

簡単簡単と意気揚々とテストを書くわけですが、これだけではダメです。

TEST(Hoge, foge)
{
    int* p = NULL;
    // error C2040: '==' : 'const int' は 'int *const ' と間接操作のレベルが異なります。
    ASSERT_EQ(NULL, p);
}
ASSERT_EQ(NULL, p) は、CmpHelperEQ<int, int*> と解釈されるため、エラーになってしまいます。
※gtest ではエラーになりません。

では、なぜ gtest ではエラーにならないのか、調べてみましょう。
(※アバウトなことしか書いていないです。ご了承ください。)

NULL リテラルか判断する
gtest の ASSERT_EQ マクロの定義を見てみると以下のようになっています。
#if !GTEST_DONT_DEFINE_ASSERT_EQ
# define ASSERT_EQ(val1, val2) GTEST_ASSERT_EQ(val1, val2)
#endif

#define GTEST_ASSERT_EQ(expected, actual) \
  ASSERT_PRED_FORMAT2(::testing::internal:: \
                      EqHelper<GTEST_IS_NULL_LITERAL_(expected)>::Compare, \
                      expected, actual)
他の ASSERT_* マクロとは、少し違っています。
まず、 EqHelper と GTEST_IS_NULL_LITERAL_ が目に入ります。
GTEST_IS_NULL_LITERAL_
見るからに NULL と関係がありそうです。

class Secret;
char IsNullLiteralHelper(Secret* p);  // A
char (&IsNullLiteralHelper(...))[2];  // B

# define GTEST_IS_NULL_LITERAL_(x) \
    (sizeof(::testing::internal::IsNullLiteralHelper(x)) == 1)
コメントなど省いてありますが、 GTEST_IS_NULL_LITERAL_ の実装はこうです。
Secret* に変換できるならば A
それ以外は、B として解釈されます。

すると、
GTEST_IS_NULL_LITERAL_(NULL) は、true です。

スバラシイ!!でも、まだです。

以下は、EqHelper の GTEST_IS_NULL_LITERAL_ が true の場合の実装です。
template <>
class EqHelper<true> {
 public:
  template <typename T1, typename T2>
  static AssertionResult Compare(
      const char* expected_expression,
      const char* actual_expression,
      const T1& expected,
      const T2& actual,
      typename EnableIf<!is_pointer<T2>::value>::type* = 0) {
         // 第二引数がポインター型でない
         // 略
      }
  template <typename T>
  static AssertionResult Compare(
      const char* expected_expression,
      const char* actual_expression,
      Secret* /* expected (NULL) */,
      T* actual) {
    return CmpHelperEQ(expected_expression, actual_expression,
                       static_cast<T*>(NULL), actual);
  }
};
ASSERT_EQ の第二引数がポインター型であることをチェックしています。
ポインターであれば、2番目の関数が呼ばれます。
2番目の関数では、比較対象のポインター型にキャストしてますので、エラーにならないという寸法です。


いやー、非常に勉強になりますね。

2012年1月28日土曜日

Google Test を使ってみる - その3(テストケース)

Google Test シリーズ第3弾。

今回は、様々なテストの書き方を見てみたいと思います。

※ Google Test 関係の記事一覧はこちら
最も簡単なテスト
最も簡単なテストの作り方は以下になります。
TEST(CaseName, TestName)
{
    // ここにテストコード
}
テストフィクスチャ
テストフィクスチャを使うことで、テストリソースの共有が可能です。
class TestFix : public ::testing::Test
{
protected:
    std::vector data;

    virtual void SetUp()
    {
        printf("TestFix::SetUp\n");
        data.push_back(0);
        data.push_back(1);
        data.push_back(2);
    }
    virtual void TearDown()
    {
        printf("TestFix::TearDown\n");
    }
}:
TEST_F(TestFix, Test1)
{
    data.push_back(3);  // TestFix のメンバーにアクセス可能
    TEST_ASSERT_EQ(3, data.size());
}
TEST_F(TestFix, Test2)
{
    TEST_ASSERT_EQ(3, data.size());
}
TEST_F マクロの第一引数に、::testing::Test クラスを継承した
テストフィクスチャのクラス名を指定します。
このテストフィクスチャは、テスト毎に作られテスト開始前に SetUp 関数をコールします。
(テスト終了後には、TearDown をコール)

実行結果
[----------] 2 tests from TestFix
[ RUN      ] TestFix.Test1
TestFix::SetUp
main.cpp(21): error: Value of: data.size()
  Actual: 4
Expected: 3
TestFix::TearDown
[  FAILED  ] TestFix.Test1 (0 ms)
[ RUN      ] TestFix.Test2
TestFix::SetUp
TestFix::TearDown
[       OK ] TestFix.Test2 (0 ms)
[----------] 2 tests from TestFix (1 ms total)

さらに、テストケース名を分けたい場合は以下のようにも書けます。
class TestFix : public ::testing::Test

// 略 

TEST_F(TestFix, Test1)
{
    TEST_ASSERT_EQ(3, data.size());
}

typedef TestFix TestFix2; // 別名を定義する
TEST_F(TestFix2, Test1)
{
    TEST_ASSERT_EQ(3, data.size());
}
値をパラメータ化したテスト
値をパラメータ化することで、I/Oテストなどが書きやすくなります。
値のパラメータ化テストを使用する場合、まずは、::testing::TestWithParam クラスを継承したクラスを用意します。(このクラスは、::testing::Test も継承しているのでテストフィクスチャと同じように使用できます。)

次に、TEST_P マクロを使ってテストを作成します。
// template 引数にはパラメータの型を渡す
class TestP : public ::testing::TestWithParam {};

TEST_P(TestP, TestA)
{
   int param = GetParam(); // パラメータは GetParam 関数で取得できる。
   printf("%d\n", param);
}
これだけでは、まだテストは実行しません。
最後に、パラメータを与えてテストをインスタンス化します。
INSTANTIATE_TEST_CASE_P(InstantiationName, TestP
    , ::testing::Values(1, 3, 0)); // ここがパラメータ
パラメータの作成には、::testing::Values 以外に
Range, ValuesIn, Bool, Combine があります。

実行結果
[----------] 6 tests from InstantiationName/TestP
[ RUN      ] InstantiationName/TestP.TestA/0
1
[       OK ] InstantiationName/TestP.TestA/0 (0 ms)
[ RUN      ] InstantiationName/TestP.TestA/1
3
[       OK ] InstantiationName/TestP.TestA/1 (0 ms)
[ RUN      ] InstantiationName/TestP.TestA/2
0
[       OK ] InstantiationName/TestP.TestA/2 (0 ms)

型付けテスト
template クラスや関数のテストをしたいときに、便利なのが型付けテストです。
// 型を受け取る(T は型リストから渡される)
template<class T>
class TypedTest : public testing::Test {
};
// 型リスト
typedef ::testing::Types<int, float, double> TestTypes;
// 型リストとテストを関連づける
TYPED_TEST_CASE(TypedTest , TestTypes);

// テストを記述する
TYPED_TEST(TypedTest, Test1)
{
    TypeParam p = 0; // TypeParam で型を取れる
    // ここにテストを書く
}
型付けテストを使用するのに必要な準備は3つ。
  • フィクスチャクラステンプレート(TypedTest)
  • 型リスト(TestTypes)
  • 型リストとテストクラスの関連付け(TYPED_TEST_CASE)
この3つを用意したら、あとは TYPED_TEST マクロを使ってテストを書くだけです。

実行結果
[----------] 1 tests from TypedTest/0, where TypeParam = int
[ RUN      ] TypedTest/0.Test1
[       OK ] TypedTest/0.Test1 (0 ms)
[----------] 1 tests from TypedTest/0 (0 ms total)

[----------] 1 tests from TypedTest/1, where TypeParam = float
[ RUN      ] TypedTest/1.Test1
[       OK ] TypedTest/1.Test1 (0 ms)
[----------] 1 tests from TypedTest/1 (0 ms total)

[----------] 1 tests from TypedTest/2, where TypeParam = double
[ RUN      ] TypedTest/2.Test1
[       OK ] TypedTest/2.Test1 (0 ms)
[----------] 1 tests from TypedTest/2 (0 ms total)
型をパラメータ化したテスト
型付けテストと違い、テスト自体は型リストのことを知らなくて良いのが、大きな違いです。
あらかじめテストを書いておき、あとからインスタンス化できます。
さらに、インスタンス化は異なる型リストで複数つくることが可能です。
//
// test.h

template<class T>
class TypedTestP : public testing::Test {
};
// テストケースを設定
TYPED_TEST_CASE_P(TypedTestP);

// テストを記述する
TYPED_TEST_P(TypedTestP , Test1)
{
    TypeParam p = 0; // TypeParam で型を取れる
    // ここにテストを書く
}
TYPED_TEST_P(TypedTestP , Test2)
{
}

// テストケースにテストを登録する
REGISTER_TYPED_TEST_CASE_P(TypedTestP  // テストケース名
    , Test1, Test2, Test3);  // テスト名の列挙
これで、テストをする部分が完成です。
値をパラメータ化したテストと同様に、まだテストが実行されない状態です。)

あとは、好きなところで好きな型リストを与えてインスタンス化します。
// test.cpp
#include "test.h"

typedef ::testing::Types<int, char> TestTypes;
// インスタンス化
INSTANTIATE_TYPED_TEST_CASE_P(InstantiateName // このインスタンスの名前
    , TypedTestP // テストケース名
    , TestTypes); // 型リスト

typedef ::testing::Types<float, double> TestTypes2;
// 別の型リストでインスタンス化
INSTANTIATE_TYPED_TEST_CASE_P(Floating // このインスタンスの名前(同名はダメ)
    , TypedTestP // テストケース名
    , TestTypes2); // 型リスト


実行結果
[----------] 2 tests from InstantiateName/TypedTestP/0, where TypeParam = int
[ RUN      ] InstantiateName/TypedTestP/0.Test1
[       OK ] InstantiateName/TypedTestP/0.Test1 (0 ms)
[ RUN      ] InstantiateName/TypedTestP/0.Test2
[       OK ] InstantiateName/TypedTestP/0.Test2 (0 ms)
[----------] 2 tests from InstantiateName/TypedTestP/0 (0 ms total)

[----------] 2 tests from InstantiateName/TypedTestP/1, where TypeParam = char
[ RUN      ] InstantiateName/TypedTestP/1.Test1
[       OK ] InstantiateName/TypedTestP/1.Test1 (0 ms)
[ RUN      ] InstantiateName/TypedTestP/1.Test2
[       OK ] InstantiateName/TypedTestP/1.Test2 (0 ms)
[----------] 2 tests from InstantiateName/TypedTestP/1 (0 ms total)

[----------] 2 tests from Floating/TypedTestP/0, where TypeParam = float
[ RUN      ] Floating/TypedTestP/0.Test1
[       OK ] Floating/TypedTestP/0.Test1 (0 ms)
[ RUN      ] Floating/TypedTestP/0.Test2
[       OK ] Floating/TypedTestP/0.Test2 (0 ms)
[----------] 2 tests from Floating/TypedTestP/0 (0 ms total)

[----------] 2 tests from Floating/TypedTestP/1, where TypeParam = double
[ RUN      ] Floating/TypedTestP/1.Test1
[       OK ] Floating/TypedTestP/1.Test1 (0 ms)
[ RUN      ] Floating/TypedTestP/1.Test2
[       OK ] Floating/TypedTestP/1.Test2 (0 ms)
[----------] 2 tests from Floating/TypedTestP/1 (0 ms total)
最後に
ここで紹介した方法は gtest のほんの一部です。
より詳しいことは、マニュアルにわかりやすく書かれているので、そちらをご確認下さい。
日本語訳されたマニュアルはこちらです。

2012年1月21日土曜日

ゲームプログラマのためのC++を読んだ

あまり技術書って買わないのですが、Twitter をやるようになってから
注目されてると気になってしまうようになりました。

そんな訳で、 ゲームプログラマのためのC++ を買いました。



約3週間かけて、ようやく読み終わりました。(読むのが遅いだけです。)
とりあえず、入門書ではないもののそんなに難しい内容ではないと思います。
事例もゲームっぽいものが多いので理解しやすくてよかった。
個人的には、これまであまりやってこなかった、STL やシリアライズの章がためになった。
社内勉強にちょうど良い内容じゃないかなと思いました。

2012年1月15日日曜日

Visual Studio 2008 で nullptr を使う

nullptr について、詳しいことはここでは説明しません。

VisualStudio では、2010 から nullptr が使えるようになりましたが、
これを VisualStudio 2008 で使えるようにします。

これは非常に簡単です。
More C++ Idioms/nullptr
こちらのイディオムをそのまま使いましょう。
以上、めでたしめでたし。


では、終わりません。


NoStepInto を活用する
nullptr を使えるようになりましたが、1つ問題があります。
ステップイン実行したときに、nullptr_t の operator にステップインしてしまうのです。
これは些細な問題かもしれませんが、 nullptr はプログラムの要所要所に出てきます。
nullptr がある度に、ステップインされるのはうざったいです。
int* F(int* p, int* q, int* r);

int* G()
{
  // F 関数にステップインする際に、 nullptr に3回ステップインしてから
    // ようやく目的の F にステップインする
    return F(nullptr, nullptr, nullptr);
}
そこで、NoSteInto を利用します。
NoStepInto の設定の仕方は、Visual Studio のステップインで特定の関数だけステップインしない方法 を参照してください。

今回は、nullptr_t のメンバにステップインしたくないので、
nullptr_t\:\:.*=NoStepInto
のように設定します。

これで、デバッグ時のうざったさが解消されました。
めでたしめでたし。

2012年1月10日火曜日

bugspots を使ってみた

bugspots は、グーグルのバグ予測アルゴリズムを実装したツールです。

こちらの紹介記事
グーグルのバグ予測アルゴリズムを実装したツール「bugspots」、オープンソースで公開
を参考に、Windows + cygwin + svn で bugspots を使ってみました。

cygwin の準備
bugspots を使用するには、git, ruby が必要になるのでインストールしておきます。
また、bugspots のインストールに gem が必要になります。
このままでは、gem を使えないので以下から rubygems-*.*.*.tgz をダウンロードします。
http://rubyforge.org/frs/?group_id=126
適当な場所に解凍し、setup.rb を実行します。
$ tar xvzf rubygems-1.8.15.tgz 
$ cd rubygems-1.8.15
$ ruby setup.rb

今回は、svn のリポジトリに対して bugspots を使うので git-svn もインストールしておきます。

bugspots のインストール
bugspots のダウンロードページから tar.gz ファイルをダウンロードします。
解凍して、gem install を実行します。
$ tar xvzf igrigorik-bugspots-v0.1.0-3-ga9debf9.tar.gz
$ cd igrigorik-bugspots-a9debf9
$ gem install bugspots
2012/03/06 : ダウンロードは不要でした。

gem install を実行します。
$ gem install bugspots

これで、bugspots を使う準備が整いました。

bugspots を実行する
git を使っていれば、あとは
$ git bugspots /path/to/repo 
とすればいいだけなのですが、svn に対して bugspots を使用する場合は、git svn clone をします。
あとは、clone したパスで bugspots を実行するだけです。
$ git svn clone /path/to/repo git-repo
$ cd git-repo
$ git bugspots ./

実際に使ってみて
まず最初に、個人的なプログラム群に使って見ました。
Scanning ./ repo
        Found 0 bugfix commits, with 0 hotspots:

        Fixes:

        Hotspots:
何も出ません。
bugspots は fixes や closed がついたコメントがなければ、なにも教えてくれません。
ちゃんと、コミットメッセージを書け。ということです。

ついで、会社のプロジェクトに対して実行してみたところ、
ちゃんとレポートが出力されました。

しかし、この情報をどのように活用したら良いのかイマイチ…
とりあえず、コードレビューをすべきファイルの指標にはなるのかなぁーと思いました。



bugspots-svn
"bugspots" でグーグル検索すると上の方に出てくるので、これを読んでいる過程で気づいた方もいるかもしれません。
今回、git-svn で clone を作成して bugspots を適用しましたが、
bugspots-svn を使えば svn のリポジトリに対して直接適用できそうです。

spawn.h
最近、家でインストールしたときに spawn.h がなくてインストールに失敗しました。
こちらハングアップの日々 - 2012/06/06を参考に posix-spawn をインストールしたところ、bugspots も正常にインストールできました。
2012/10/2 追記

2012年1月9日月曜日

ExpandEnvironmentStrings の問題

自分のための備忘録です。

Windows7 と XP で ExpandEnvironmentStrings の挙動が違うもよう。
基本的に XP での動作が怪しい感じ。

ExpandEnvironmentStrings についてはこんな感じ。
DWORD ExpandEnvironmentStrings(
LPCTSTR lpSrc, // 環境変数を表す文字列へのポインタ
LPTSTR lpDst, // 展開後の環境変数を表す文字列へのポインタ
DWORD nSize // 展開後の文字列の最大文字数
);

戻り値
関数が成功すると、展開後の文字列を受け取るバッファに格納された文字数が返ります。
指定したバッファのサイズより展開後の文字列数の方が大きいときは、展開後の文字列を
保持するために必要なバッファのサイズが返ります。


日本語を含む文字列の展開
char* hoge()
{
    DWORD len = ExpandEnvironmentStringsA( "%USERPROFILE%\\デスクトップ", NULL, 0 );
    char* str = new char [len];
    ExpandEnvironmentStringsA( "%USERPROFILE%\\デスクトップ", str, len );
    return str;
}
1回目の ExpandEnvironmentStringsA で必要なサイズ(文字数)を取得して、
2回目の ExpandEnvironmentStringsA 動的確保したバッファに読み込みます。

使い方に関して、一見問題なさそうですが、
Windows XP では、len に想定外の値が返ってきます。
実際に必要な文字数より大きい値であれば、必要な文字列が取得できるのですが(余分なメモリは確保されてしまうが)小さい値になる場合もあるようで、この場合文字列が切り捨てられてしまいます。

対策としては、余分にバッファを確保するか、ExpandEnvironmentStringsW を使うといったところでしょうか…

非常に長い文字列の展開
日本語を含む文字列の展開については、ExpandEnvironmentStringsW を使うことにして対処しました。
しかし、ExpandEnvironmentStringsW にも問題がありました。
TEST(EnvironmentStrings, LongLongString)
{
    wchar_t str[0x10000];
    // 適当な文字列を作る
    for( int i=0; i < 0x10000; ++i )
    {
        str[i] = L'0' + i%10;
    }
    str[0x10000-1] = L'\0';
    DWORD len = ExpandEnvironmentStringsW( str, NULL, 0 );
    ASSERT_EQ( 0x10000, len );
}
上記コードは、Windows7 では成功します。 しかし、Windows XP では len に 0x7FFF が返ってきて、失敗してしまいます。 どうやら、XP では 0x8000 以上の文字列は展開できないようです… そんなに長い文字列を展開することがあるのか、という気もしますが、 最近あったからこれを書いているわけで… 対策としては、事前に分解して長い文字列の展開をしないように工夫するしかないですね…
追記
  • 2012/01/22
    TEST(EnvironmentStrings, LongLongString) のコードに間違いがあったので修正しました。

2012年1月7日土曜日

新年の抱負

新年あけましておめでとうございます。
本年もよろしくお願いします。

さて、新年ということで皆さんも
今年一年○○でありたい、○○をする・やりたいと一年の目標を立ていると思います。

私の今年一年のテーマは、 不撓不屈 です。
※強い意志をもって、どんな苦労や困難にもくじけないさま。

でも、毎年目標なりテーマを掲げるのですが、
毎年中途半端になりがち…

しかし、今年こそは。
と、書き初めをすることにしました。
小中と書道教室に通った経験が活かされるときが来ましたよ。


道具は近所の大型スーパーの文具売り場で買い揃えました。
筆・硯・文鎮・下敷き・墨汁・筆巻・半紙
全部で 1500円 程度で買えました。

8年ぶりの書道、下手になっててちょっとショックです。
でも楽しく書けたし、来年もまたやります。