redboltzの日記 このページをアンテナに追加 RSSフィード

2014-06-06

[] cmakeでshared libraryとstatic libraryの名前(本体)を同じにする方法

FAQに解説があった。

http://www.cmake.org/Wiki/CMake_FAQ#How_do_I_make_my_shared_and_static_libraries_have_the_same_root_name.2C_but_different_suffixes.3F

しかし、これはかなりバッドノウハウ臭い。

まず、cmakeでは以下のように同名でADD_LIBRARYすることができない。

ADD_LIBRARY(msgpack SHARED ${msgpack_sources})
ADD_LIBRARY(msgpack STATIC ${msgpack_sources})

そこで、shared libraryをmsgpack, static libraryをmsgpack-staticとする。

ADD_LIBRARY(msgpack SHARED ${msgpack_sources})
ADD_LIBRARY(msgpack-static STATIC ${msgpack_sources})

そして、msgpack-static の PROPERTY である OUTPUT_NAME を msgpack に変更する。

SET_TARGET_PROPERTIES(msgpack-static PROPERTIES OUTPUT_NAME "msgpack")

あくまでも PROPERTYの変更であり、TARGET名が変わったわけではない。

これでUNIX系の場合、

libmsgpack.a
libmsgpack.so

というファイル名で出力を得る形でビルドできるようになる。

しかし、Windowsでは、

msgpack.lib (static ライブラリ)
msgpack.lib (インポートライブラリ)
msgpack.dll (DLL)

となり、static ライブラリと、インポートライブラリの名前が衝突してしまう。

これを避ける(バッドノウハウ臭い)方法として

SET_TARGET_PROPERTIES(msgpack-static PROPERTIES PREFIX "lib")

と設定するというのだ。

すると、Windows版のstaticライブラリが libmsgpack.libとなり、衝突は回避される。

一方、UNIX系では、staticライブラリは、libmsgpack.a のままとなる。

なぜならば、PREFIXとして、

Windowsは "" => "lib"

Linuxは "lib" => "lib"

という設定がなされるためだ。

つまりこのノウハウは、UNIX系のライブラリprefixがlibであることを利用したアプローチで、Windows版のstaticライブラリの前に、libをつけるという限定された方法で問題を回避するということを意味する。

sharedライブラリWindowsのように複数ファイルで構成される場合、それらの要素毎に名前を変えたりする方法があれば良いが、なさそうだ(見つけられてないだけかも)。

2014/06/06 23:05追記
SET_TARGET_PROPERTIES (msgpack PROPERTIES IMPORT_SUFFIX "_import.lib")
といった形でimportライブラリの名前のSUFFIXを設定することができることが分かった。
出力は msgpack_import.lib となる。
http://www.cmake.org/cmake/help/v2.8.8/cmake.html#prop_tgt:IMPORT_SUFFIX
これで、目的は達成できた。

libを決め打ちで付ける以外の解決策としては、staticライブラリとsharedライブラリビルドする際のファイルの出力先ディレクトリを分ける方法が考えられる。

つまり、期待する出力は、

static/msgpack.lib
shared/msgpack.lib
shared/msgpack.dll

な感じ。

出力先のディレクトリを個別に指定できればできそうだ。まだ方法が見つけられていないが、きっとありそうな気がする。

2014-02-15

[]仮想マシンにWANからアクセス

Vagrantで作った仮想マシンにWANからアクセスするのにとても苦労したので、まとめておく。

ネットワーク構成は以下の通り。

GW:192.168.0.1 (ATerm WR8700N)

HOST:192.168.0.10 (Arch Linux)

GUEST:192.168.0.11 (Debian 7.2)

サブネットマスクは 255.255.255.0

最小限のVagrantfileの設定でvagrant upしてみる。仮想マシンVirtualBoxを用いた。

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  ##### Proxy ######
  config.vm.define "proxy" do |proxy|
    proxy.vm.box = "Debian7"
    proxy.vm.box_url = "http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_debian-7.2.0_chef-provisionerless.box"
  end
end

vagrant ssh でゲストマシンに入り、

/sbin/route を実行すると、

vagrant@debian-7:~$ /sbin/route 
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         10.0.2.2        0.0.0.0         UG    0      0        0 eth0
10.0.2.0        *               255.255.255.0   U     0      0        0 eth0

となっており、NICとしてeth0が設定されていることがわかる。

vagrant sshsshアクセスできていることからも分かるように、このネットワーク設定はホスト-ゲスト間接続に使用されている。ゲストからホストは、IPアドレス 10.0.2.2 として見えており、これがGatewayとなって、外部へのアクセスも可能である。このおかげで、ゲストからchef-soloなどを使ってインターネットから各種ソフトウェアインストールすることができる。

今回、ゲストのマシンに固定IPアドレス 192.168.0.11 を割り当ててサーバを立ち上げ、インターネット(WAN)側からアクセスできるようにしたい。さらに、192.168.0.1 のルータから直接この仮想マシンにforwardしたい。

これを実現しようとすると、いろいろな問題があることが調査の結果わかった。

https://github.com/mitchellh/vagrant/issues/921

http://cloverrose.hateblo.jp/entry/2013/09/13/212120

https://github.com/mitchellh/vagrant/issues/743

上記のサイトを参考にしながら、辿り着いた解決策は以下のとおりだ。

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  ##### Proxy ######
  config.vm.define "proxy" do |proxy|
    proxy.vm.box = "Debian7"
    proxy.vm.box_url = "http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_debian-7.2.0_chef-provisionerless.box"

    proxy.vm.network :public_network, :auto_config => false
    proxy.vm.provision :shell, :path => "bootstrap.sh"
  end
end

この設定で、VirtualBox上に外部からもアクセス可能なbridged設定の2枚目のNICが生成される。ただし、Vagrantの設定に任せるとうまく行かないため、 :auto_config => false を設定している。この設定により、VirtualBox上のNIC生成され、その後のコンフィギュレーションは何も行われない状態となる。

実際のコンフィギュレーションは、bootstrap.shの中で行う。

ファイル名は、以下の行で指定するため、任意の名前を付けることができる。

proxy.vm.provision :shell, :path => "bootstrap.sh"

bootstrap.sh の内容は以下の通り。

sudo ifconfig eth1 192.168.0.11 netmask 255.255.255.0 up
sudo route del default gw 10.0.2.2 eth0
sudo route add default gw 192.168.0.1 eth1

上記参考サイトには、以下の行が含まれておらず、これでかなり悩んでしまった。

sudo route del default gw 10.0.2.2 eth0

ここまで設定して、vagrant reloadするなり、新規作成するなりすれば、外部からのアクセスが可能となっているはずだ。なお、ルータ192.168.0.1においては 仮想マシンIPアドレス 192.168.0.11 に必要なポートをforwardする設定を行っている。

vagrant@debian-7:~$ /sbin/route 
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         192.168.0.1     0.0.0.0         UG    0      0        0 eth1
10.0.2.0        *               255.255.255.0   U     0      0        0 eth0
192.168.0.0     *               255.255.255.0   U     0      0        0 eth1

試行錯誤している段階では、以下の行をbootstrap.shに書いていなかったため、

sudo route del default gw 10.0.2.2 eth0

routeの設定が、

vagrant@debian-7:~$ /sbin/route 
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         192.168.0.1     0.0.0.0         UG    0      0        0 eth1
default         10.0.2.2        0.0.0.0         UG    0      0        0 eth0
10.0.2.0        *               255.255.255.0   U     0      0        0 eth0
192.168.0.0     *               255.255.255.0   U     0      0        0 eth1

となっていた。Gatewayが複数設定されていたため、正しく外部とのコネクションが確立されなかったと思われる。

Proxy仮想マシン名です。あと、最小限といいつつ、複数VM対応の書き方になってました。が、本質は変わらないということでご容赦を。

2013-04-15

[]C++11でのis_callable

前回のエントリ http://d.hatena.ne.jp/redboltz/20130402/1364877581 で、is_callableって今どうなってるんだろう?みたいな話を書いたが、やっぱり無いみたいなので、各種サイトを参考に書いてみた。

メタ関数としては以下の実装で行けるんじゃないかと思う。

#include <type_traits>

template <typename Func, typename... Args>
struct is_callable
{
private:
    template <typename CheckType>
    static std::true_type check(decltype(std::declval<CheckType>()(std::declval<Args>()...)) *);
    template <typename CheckType>
    static std::false_type check(...);
public:
    typedef decltype(check<Func>(nullptr)) type;
    static constexpr bool value = type::value;
};

constexprな関数インターフェースとしては、なんか ::typeを書かなくていいだけという効果しかなくて微妙だけど、以下のような感じか?引数推論をうまく使う方法があるのかな?でもどうせ期待する引数の型を書くんだし、これでいいかな?などと悩みつつ。

template <typename Func, typename... Args>
constexpr bool is_callable_func() {
    return is_callable<Func, Args...>::value;
}

テストコードは以下のような感じ。

is_callableのテンプレート第1引数としてdecltype(チェックしたいオブジェクト)

テンプレート第2引数以降は、呼び出しに必要な引数を列挙という感じ。

#include <iostream>
#include <string>
#include <vector>

extern void* enabler;

template <typename F, typename std::enable_if<is_callable<F, std::string>::value>::type*& = enabler>
void check(F const&) {
    std::cout << "callable" << std::endl;
}

template <typename F, typename std::enable_if<!is_callable<F, std::string>::value>::type*& = enabler>
void check(F const&) {
    std::cout << "not callable" << std::endl;
}

void free_func1();
void free_func2(int);
void free_func3(int, std::string);

int main() {
    // Meta Function I/F
    static_assert( is_callable<decltype(free_func1)>::value, "");
    static_assert(!is_callable<decltype(free_func1), int>::value, "");
    static_assert(!is_callable<decltype(free_func2)>::value, "");
    static_assert( is_callable<decltype(free_func2), int>::value, "");
    static_assert( is_callable<decltype(free_func3), int, std::string>::value, "");

    auto lambda_ex1 = [](){};
    auto lambda_ex2 = [](int){};
    auto lambda_ex3 = [](int, std::string){};

    static_assert( is_callable<decltype(lambda_ex1)>::value, "");
    static_assert(!is_callable<decltype(lambda_ex1), int>::value, "");
    static_assert(!is_callable<decltype(lambda_ex2)>::value, "");
    static_assert( is_callable<decltype(lambda_ex2), int>::value, "");
    static_assert( is_callable<decltype(lambda_ex3), int, std::string>::value, "");

    // Function I/F
    static_assert( is_callable_func<decltype(lambda_ex1)>(), "");
    static_assert(!is_callable_func<decltype(lambda_ex1), int>(), "");
    static_assert(!is_callable_func<decltype(lambda_ex2)>(), "");
    static_assert( is_callable_func<decltype(lambda_ex2), int>(), "");
    static_assert( is_callable_func<decltype(lambda_ex3), int, std::string>(), "");

    // SFINAE
    check([](){});
    check([](int) {});
    check([](std::string){});
}

ただ、チェック対象のオブジェクトをdecltypeしているため、対象がオーバーロードされた関数群である場合、ill-formedとなりコンパイルエラーとなってしまう。この辺、うまく対応するテクニックはあるのだろうか?

ソース

http://liveworkspace.org/code/47vxTs$1

追記(2013/04/16)

id:gintenlabo さんの指摘により、修正したコードを以下に追加。

http://liveworkspace.org/code/47vxTs$10

#include <type_traits>

template <typename Func, typename... Args>
struct is_callable
{
private:
    template <typename CheckType>
    static std::true_type check(decltype(std::declval<CheckType>()(std::declval<Args>()...), (void)0)*);
    template <typename CheckType>
    static std::false_type check(...);
public:
    typedef decltype(check<Func>(nullptr)) type;
    static constexpr bool value = type::value;
};

2013-04-02

[][]期待値の様々な表現方法

期待値の表現

turtleでは

MOCK_EXPECT(Mock化された関数).with(期待値);

とwithを利用することで、期待値を設定することができる。

上記の例では、期待値と実際にMock化された関数に渡された値を operator==で比較し一致した場合にテストが成功となる。

Mock化された関数が複数の引数を取る場合、

MOCK_EXPECT(Mock化された関数).with(期待値1, 期待値2, 期待値3);

という形で記述できる。

もし期待値1と期待値3だけに興味があり、期待値2はどうでもいい場合、

MOCK_EXPECT(Mock化された関数).with(期待値1, mock::any, 期待値3);

と記述することで第2引数をテスト対象から除外することができる。

様々な期待値(制約)の記述法

完全一致以外の比較を行いたいこともあるだろう。

turtleでは以下のような様々な期待値の表現方法がサポートされている。

http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.expectation.constraints

期待値としてファンクションオブジェクトを記述することができるため、一覧で提供される比較では不十分な場合にも対応が可能だ。

作為的な例だが、以下のような関数を考える。client_2_1は、引数を3倍した値をtarget_functionに渡す。

int client_2_1(int a) {
    return target_function(a*3);
}

そしてテスト側では、引数が3で割り切れるかをチェックするといったことができる。

BOOST_AUTO_TEST_CASE( test_2_1 ) 
{
    MOCK_EXPECT(target_function).once().with(std::function<bool(int)>([](int a){ return a % 3 == 0; }));
    client_2_1(5);
    mock::reset();
}

現在のturtleでは、C++11のlambda expressionを直接渡すことができないので(要望はあげている)、std::functionでwrapした。

注意点

さて、鋭い人は既に気付いているかも知れないが、ファンクションオブジェクトそのものが期待値の場合どうすればよいのだろう?

実は

with(オブジェクト)

という記述は、

  • オブジェクトのクラスがoperator()を持っているならばmock::call(expected)とみなされる。つまり、ファンクションオブジェクトとして制約を記述したことになる。
  • それ以外の場合、mock::equal(expected)とみなされる。つまり operator==での比較となる。

余談

C++11 の lambda expressionをそのまま渡せないのはなぜだろう?

http://sourceforge.net/p/turtle/code/613/tree/trunk/turtle/matcher.hpp#l84

    template< typename Actual, typename Functor >
    class matcher< Actual, Functor,
        BOOST_DEDUCED_TYPENAME boost::enable_if<
            detail::is_functor< Functor>
        >::type
    > : public detail::matcher_base< Actual >
    {
    public:
        explicit matcher( const Functor& f )
            : c_( f )
        {}
        virtual bool operator()( Actual actual )
        {
            return c_( actual );
        }
    private:
        virtual void serialize( std::ostream& s ) const
        {
            s << mock::format( c_ );
        }
    private:
        Functor c_;
    };

で、SFINAEを用いてファンクションオブジェクトに部分特殊化されたmatcherクラステンプレートが定義されており、これを使って欲しいところだが、残念ながらここにディスパッチされていないようだ。

            detail::is_functor< Functor>

が期待通り働いていないようなので、実装を見ると、

http://sourceforge.net/p/turtle/code/613/tree/trunk/turtle/detail/is_functor.hpp#l25

namespace mock
{
namespace detail
{
    BOOST_MPL_HAS_XXX_TRAIT_DEF( result_type )
    BOOST_MPL_HAS_XXX_TEMPLATE_DEF( sig )
    BOOST_MPL_HAS_XXX_TEMPLATE_DEF( result )

    template< typename T >
    struct is_functor
        : boost::mpl::or_<
            boost::function_types::is_callable_builtin< T >,
            has_result_type< T >,
            has_result< T >,
            has_sig< T >
        >
    {};
}
} // mock

こんな感じになっている。

boost::function_types::is_callable_builtin< T >,

C++11のlambda expressionに対してtrueを返さないようだ。

そういえば、C++11でcallableをチェックする方法ってあるんだっけ?

http://stackoverflow.com/questions/5100015/c-metafunction-to-determine-whether-a-type-is-callable

に、関連する議論があるようだ。

この質問は、引数の数や種類に関わらずcallableか知る術を求めており、decltypeさんの回答はなかなかトリッキーだ。

よりエレガントな実装はあるのだろうか?興味深いが、本題に戻ろう。

Mikael Perssonさんの回答は、質問の答えにはなっていないが、引数リストを指定すれば、その引数で呼び出せる場合にtrueを返すmeta functionを実装している。こっちが今回の用途には使えそうな気がする。

ところで、そのものずばりのcallable判定utilityって標準にはないよね?

Boostではis_callable_builtinも提供されているFunction Types あたりが近い気がするが、

http://www.boost.org/libs/function_types/doc/html/index.html

現在の状況ってどうなっているのだろう?

Boost.TTIあたりで実現されている?(ドキュメントのビルドに失敗し、未確認)

[][]関数のMock化

以下のようなディレクトリ/ファイル構成があるとする。

--+--target-+-inc/target_function.hpp         
            +-target_function.cpp
            +-client_1.cpp

clientはtargetを使用するという関係だとする。

inc/target_function.hpp ではtarget関数が宣言される。

#ifndef TARGET_FUNCTION_HPP
#define TARGET_FUNCTION_HPP

int target_function(int a);

namespace target {

int function(int a);

}// target

#endif // TARGET_FUNCTION_HPP

target_function.cpp では、target関数が定義される。

#include "target_function.hpp"

int target_function(int a) {
    return a + a;
}

namespace target {

int function(int a) {
    return a + a;
}

}// target

client_1.cpp では、target_function.hpp をインクルードし、target関数を呼び出す。

inc/をインクルードパスに追加することが想定されている。

#include "target_function.hpp"

int client_1_1(int a) {
    return target_function(a + a);
}

namespace client {

int client_1_2(int a) {
    return target::function(a) + target::function(a + a);
}

}// client

ここで、clientをテストするため、targetをMock化する。

target_functionやtarget::functionは、関数であるため、以下のようなディレクトリを追加し、include先を変える形でMock化する。

--+--target-+-inc/target_function.hpp         
  |         +-target_function.cpp
  |         +-client_1.cpp
  |
  +--test---+-mock/target_function.hpp
            +-test_1.cpp

mock/target_function.hpp では、targetをMock化する。

#ifndef TARGET_FUNCTION_HPP
#define TARGET_FUNCTION_HPP

MOCK_FUNCTION(target_function, 1, int(int))

namespace target {

MOCK_FUNCTION(function, 1, int(int))

}// target
#endif // TARGET_FUNCTION_HPP

MOCK_FUNCTION(Mock化対象関数名, 引数の数, 関数シグネチャ)

でMock化された関数を準備できる。

名前空間の中にある関数をMock化する場合は、namespace内で、MOCK_FUNCTIONを記述すればよい。

Mockを用いたテストコードは以下のようになる。

ここで、inc/の代わりに、mock/がインクルードパスとして指定されるようにビルドを構成する。

test_1.cpp

#include <boost/test/auto_unit_test.hpp>
#include <turtle/mock.hpp>

namespace {
#include "../target/client_1.cpp" // テスト対象
}

BOOST_AUTO_TEST_CASE( test_1_1 ) 
{
    MOCK_EXPECT(target_function).once().with(3+3).returns(5);
    BOOST_CHECK_EQUAL(client_1_1(3), 5);
    mock::reset();
}

BOOST_AUTO_TEST_CASE( test_1_2 ) 
{
    MOCK_EXPECT(target::function).once().with(3).returns(4);
    MOCK_EXPECT(target::function).once().with(3+3).returns(5);
    BOOST_CHECK_EQUAL(client::client_1_2(3), 4+5);
    mock::reset();
}

test_1_1では、

    MOCK_EXPECT(target_function).once().with(3+3).returns(5);

target_functionが1度だけ、3+3を引数に呼び出すことを期待する旨が記述されている。そして、結果として5を返すことが記述されている。

    BOOST_CHECK_EQUAL(client_1_1(3), 5);

client_1_1は引数3で呼び出され、

int client_1_1(int a) {
    return target_function(a + a);
}

が正しく実装されているならば、target_functionは引数3+3で呼び出されるべきだからである。

そして、client_1_1はtarget_functionの戻り値をそのまま返す仕様となっている。

※仕様については事前に言及していないが、まあそうなっていることに○○○

Mockが呼び出されていることを確認するため、敢えて3+3の6ではなく5を返すように設定した。

結果、client_1_1の戻り値が5であることを BOOST_CHECK_EQUAL で確認している。

test_2_2では、

namespace client {

int client_1_2(int a) {
    return target::function(a) + target::function(a + a);
}

}// client

をテスト対象としている。

BOOST_AUTO_TEST_CASE( test_1_2 ) 
{
    MOCK_EXPECT(target::function).once().with(3).returns(4);   // 順不同
    MOCK_EXPECT(target::function).once().with(3+3).returns(5); // 順不同
    BOOST_CHECK_EQUAL(client::client_1_2(3), 4+5);
    mock::reset();
}

withで期待する値が異なる場合、once()をそれぞれの期待値において記述することができる。

もし、テスト対象が以下のように同じ引数でtarget::functionを2回呼び出すならば、

namespace client {

int client_1_2(int a) {
    return target::function(a) + target::function(a);
}

}// client

テストは以下のように書ける。

BOOST_AUTO_TEST_CASE( test_1_2 ) 
{
    MOCK_EXPECT(target::function).exactly(2).with(3).returns(4);   // 2回呼び出されることを期待
    BOOST_CHECK_EQUAL(client::client_1_2(3), 4+4);
    mock::reset();
}

もし、returnを変えたい場合は、今まで通りonceを2回書けばよい。

2013-03-31

[][]turtleとBoost.Testを使ってmake_sharedのテストを書いてみた

C++で使えるMockライブラリ

turtle http://turtle.sourceforge.net/ は、C++で使えるMockライブラリだ。同様のライブラリとしてGoogleMock https://code.google.com/p/googlemock/ がある。

当初、GoogleMockでは仮想関数のオーバーライドによるMockクラスへの処理のディスパッチができるのみで、関数テンプレートがサポートされていないのかと思っていた。しかし、 https://code.google.com/p/googlemock/wiki/CookBook#Mocking_Nonvirtual_Methods によると、非仮想関数のMockもサポートされているようだ。しかし、関数テンプレートについては明言されておらず、触りながら調べる必要がありそうだった。

一方、turtle では、 http://turtle.sourceforge.net/turtle/limitations.html#turtle.limitations.template_methods_cannot_be_mocked に、Template methods cannot be mocked と言うタイトルに反して、明示的にシグニチャを指定すれば関数テンプレートがサポートされると言うことが書いてあった。日本語ドキュメントもほとんどないみたいなので、触りつつ、ここにまとめてみたい。

make_sharedをテストしてみる

関数テンプレートをターゲットにどこまでMock機能が使えるのか試してみたかったので、テストターゲットをboost::make_sharedとした。といっても、make_sharedそのものではなく、make_shared内部から呼び出すクラスや関数をMockにするので、関数テンプレートサポートを確認するためのターゲットとして適切かどうかは怪しい。しかし、sp_enable_shared_from_this という関数テンプレートを内部で呼び出していたので、結果的に意味のあるテストになったと思う。

以下がその全コードである。

#define BOOST_AUTO_TEST_MAIN
#include <boost/test/auto_unit_test.hpp>
#include <turtle/mock.hpp>


#if 0 // Test target function
// Variadic templates, rvalue reference
template< class T, class Arg1, class... Args > typename boost::detail::sp_if_not_array< T >::type make_shared( Arg1 && arg1, Args && ... args )
{
    boost::shared_ptr< T > pt( static_cast< T* >( 0 ), BOOST_SP_MSD( T ) );

    boost::detail::sp_ms_deleter< T > * pd = static_cast<boost::detail::sp_ms_deleter< T > *>( pt._internal_get_untyped_deleter() );

    void * pv = pd->address();

    ::new( pv ) T( boost::detail::sp_forward<Arg1>( arg1 ), boost::detail::sp_forward<Args>( args )... );
    pd->set_initialized();

    T * pt2 = static_cast< T* >( pv );

    boost::detail::sp_enable_shared_from_this( &pt, pt2, pt2 );
    return boost::shared_ptr< T >( pt, pt2 );
}
#endif // Test target function


#include <boost/make_shared.hpp> // for the test target make_shared

// -------------------------------------------------------------------------------

MOCK_CLASS( TestClass ) {
    TestClass() {}
    MOCK_CONSTRUCTOR(TestClass, 3, (int, int, int), Ctor)
};

namespace boost {

namespace detail {

template <>
MOCK_CLASS( sp_ms_deleter< TestClass > ) {
    sp_ms_deleter<TestClass>() {}
    MOCK_METHOD(address, 0, void* ())
    MOCK_METHOD(set_initialized, 0, void ())
};

template <>
MOCK_CLASS( sp_inplace_tag<sp_ms_deleter< TestClass >> ) {
    MOCK_CONSTRUCTOR(sp_inplace_tag, 0, (), DefCtor)
};

MOCK_FUNCTION(sp_enable_shared_from_this, 3, void(shared_ptr<TestClass> const*, TestClass const*, TestClass const*))

} // detail

template <>
MOCK_CLASS( shared_ptr<TestClass> ) {
    shared_ptr<TestClass>(TestClass* p, boost::detail::sp_inplace_tag<boost::detail::sp_ms_deleter<TestClass> >) {
        DefCtor(*this, p);
    }
    MOCK_STATIC_METHOD(DefCtor, 2, void(shared_ptr<TestClass>&, TestClass*)) // constructed_object, params...
    MOCK_CONSTRUCTOR_TPL(shared_ptr, 2, (shared_ptr<TestClass> const&, TestClass*), CopyAliasCtor)
    MOCK_CONST_METHOD(_internal_get_untyped_deleter, 0, void* ())
    MOCK_CONST_METHOD(get, 0, TestClass* ())

};

} // boost

BOOST_AUTO_TEST_CASE( test1 ) 
{
    TestClass t;
    boost::detail::sp_ms_deleter<TestClass> pd;
    MOCK_EXPECT(boost::detail::sp_inplace_tag<boost::detail::sp_ms_deleter<TestClass>>::DefCtor).once();
    MOCK_EXPECT(boost::shared_ptr<TestClass>::DefCtor ).once().with(mock::any, nullptr).calls(
        [&t, &pd](boost::shared_ptr<TestClass>& s, TestClass*) -> void {  
            MOCK_EXPECT(s._internal_get_untyped_deleter).once().returns(&pd);
            MOCK_EXPECT(boost::detail::sp_enable_shared_from_this).once().with(&s, &t, &t);
        });
    MOCK_EXPECT(pd.address).once().returns(&t);
    MOCK_EXPECT(TestClass::Ctor).once().with(1, 2, 3);
    MOCK_EXPECT(pd.set_initialized).once();
    MOCK_EXPECT(boost::shared_ptr<TestClass>::CopyAliasCtor ).once();
    boost::make_shared<TestClass>(1, 2, 3);
    mock::reset();
}

準備

順に説明をしていくと、

#define BOOST_AUTO_TEST_MAIN
#include <boost/test/auto_unit_test.hpp>
#include <turtle/mock.hpp>

最初の2行はBoost.Testのテストケースとして、BOOST_AUTO_TEST_CASE( test1 ) を実行するためのコード。

Boost.Testを実行するために、boost_test_exec_monitorをリンクする必要がある。

※ヘッダオンリーの方法もある

3行目はturtleのヘッダをインクルードしている。

テスト対象のコード

#if 0で囲っているのでメモ程度の意味しかないが、make_sharedのコードを引っ張ってきた。今回、これを見ながら、いろいろMock化にトライした。

#if 0 // Test target function
// Variadic templates, rvalue reference
template< class T, class Arg1, class... Args > typename boost::detail::sp_if_not_array< T >::type make_shared( Arg1 && arg1, Args && ... args )
{
    boost::shared_ptr< T > pt( static_cast< T* >( 0 ), BOOST_SP_MSD( T ) );

    boost::detail::sp_ms_deleter< T > * pd = static_cast<boost::detail::sp_ms_deleter< T > *>( pt._internal_get_untyped_deleter() );

    void * pv = pd->address();

    ::new( pv ) T( boost::detail::sp_forward<Arg1>( arg1 ), boost::detail::sp_forward<Args>( args )... );
    pd->set_initialized();

    T * pt2 = static_cast< T* >( pv );

    boost::detail::sp_enable_shared_from_this( &pt, pt2, pt2 );
    return boost::shared_ptr< T >( pt, pt2 );
}
#endif // Test target function

コンストラクタの呼び出し BOOST_SP_MSD( T )

定義

ターゲットコードの冒頭、

    boost::shared_ptr< T > pt( static_cast< T* >( 0 ), BOOST_SP_MSD( T ) );

で、最初に呼ばれるのがBOOST_SP_MSD( T )だ。

コードを追っかけると

https://github.com/ryppl/boost-svn/blob/master/boost/smart_ptr/make_shared_object.hpp#L125

# define BOOST_SP_MSD( T ) boost::detail::sp_inplace_tag< boost::detail::sp_ms_deleter< T > >()

と定義されている。

よって、これに対応するMockクラスを用意する。

以下を、boost::detail 名前空間に定義する。これは、sp_inplace_tagの完全特殊化バージョンである。TestClassは今回のテスト用に用意する自前のクラスである。翻訳単位上に、クラステンプレート sp_inplace_tag は存在しているが、 完全特殊化バージョンが存在するためこちらに処理がディスパッチされる。Mock化したい対象が、クラステンプレート関数テンプレートの場合、このようなアプローチが可能となる。通常のクラスや関数の場合、Mockではないオリジナルのクラスを何らかの形で切り離さないと2重定義になってしまうので注意が必要である。 http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.creation

※多分ヘッダを分けて、テストの際、インクルードパスを別の所にするなどのアプローチを採るんじゃないかと思う。

template <>
MOCK_CLASS( sp_inplace_tag<sp_ms_deleter< TestClass >> ) {
    MOCK_CONSTRUCTOR(sp_inplace_tag, 0, (), DefCtor)
};

Mockクラスの定義は、http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.creation.class を、コンストラクタのMockは、http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.creation.constructor を参照。sp_inplace_tagはクラステンプレートだが、コンストラクタシグニチャテンプレートパラメタが含まれないため、MOCK_CONSTRUCTOR_TPLではなくMOCK_CONSTRUCTORを利用している。

なお、Mockの定義では、

MOCK_???( name, arity, signature[, identifier] ) 

というのがおきまりのパターンで、第4引数はオプショナルで、一般にオーバーロードする際に指定する(後述のMOCK_EXPECTで区別するため)。デフォルトはnameと同じ値が設定される。ただし、コンストラクタのMockではidentifierは必須となっている。

チェック

Boost.Testのテストケースとして、以下を記述する。

    MOCK_EXPECT(boost::detail::sp_inplace_tag<boost::detail::sp_ms_deleter<TestClass>>::DefCtor).once();

MOCK_EXPECT http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.expectation として期待値を記述する。最初の()の中には、名前空間とクラス名修飾されたidentifierを指定している。その後の、once()は一度だけ呼び出されることを期待することを意味する。once()以外に指定可能な期待値は、 http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.expectation.invocation にまとめて記述されている。なお、ここでは1回だけ呼び出されることを期待として設定しているだけであり、呼び出しの順序までは規定していない。よって、2回以上呼び出されたり、一度も呼び出されないままテストが終了した場合に、テスト失敗として検出される。

コンストラクタの呼び出し boost::shared_ptr< T > からのメンバ関数呼び出し

定義
    boost::shared_ptr< T > pt( static_cast< T* >( 0 ), BOOST_SP_MSD( T ) );

    boost::detail::sp_ms_deleter< T > * pd = static_cast<boost::detail::sp_ms_deleter< T > *>( pt._internal_get_untyped_deleter() );

次に、ターゲットコードは、shared_ptrのコンストラクタを呼び出して、ptを構築し、ptに対してメンバ関数_internal_get_untyped_deleter()を呼び出す流れとなる。

これをMock化するために以下を定義する。

template <>
MOCK_CLASS( shared_ptr<TestClass> ) {
    shared_ptr<TestClass>(TestClass* p, boost::detail::sp_inplace_tag<boost::detail::sp_ms_deleter<TestClass> >) {
        DefCtor(*this, p);
    }
    MOCK_STATIC_METHOD(DefCtor, 2, void(shared_ptr<TestClass>&, TestClass*)) // constructed_object, params...
    MOCK_CONST_METHOD(_internal_get_untyped_deleter, 0, void* ())
    /* ... */
};

コンストラクタをMock化しているのに、MOCK_CONSTRUCTORやMOCK_CONSTRUCTOR_TPLを用いていないのがポイントだ。これは、単にコンストラクタの呼び出しをチェックしたいだけではなく、コンストラクタ呼び出し後、それによって構築されたオブジェクトメンバ関数 _internal_get_untyped_deleter()の呼び出もチェックしたいからだ。当初、これをどのようにテストするのか分からず、作者に問い合わせたところ、 http://sourceforge.net/p/turtle/tickets/26/ このアプローチで対応可能であることが分かった。

まず、コンストラクタは、MOCKではなく通常のものを定義する。MOCK_CLASS 内部には、通常のコンストラクタメンバ関数も定義することができる。そしてコンストラクタの中で、DefCtor(*this, p); を呼び出している。DefCtorは、

    MOCK_STATIC_METHOD(DefCtor, 2, void(shared_ptr<TestClass>&, TestClass*)) // constructed_object, params...

と、staticメンバ関数として定義する。そして第1引数コンストラクタによって構築したオブジェクトを受けている。

チェック

このような仕組みを用意することで、以下のようなチェックが可能となる。

    MOCK_EXPECT(boost::shared_ptr<TestClass>::DefCtor ).once().with(mock::any, nullptr).calls(
        [&t, &pd](boost::shared_ptr<TestClass>& s, TestClass*) -> void {  
            MOCK_EXPECT(s._internal_get_untyped_deleter).once().returns(&pd);
            MOCK_EXPECT(boost::detail::sp_enable_shared_from_this).once().with(&s, &t, &t);
        });

まずは、MOCK_EXPECTでDefCtorの呼び出しチェックを行う。コンストラクタの第1引数は、DefCtorの第2引数となる。そして、with()において、引数を期待値として比較している。

http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.expectation.constraints

turtleでは . 演算子で各種比較をつないで記述することができる。withの第1引数は、構築したオブジェクトの参照なので、mock::anyを設定することでノーチェックに設定し、第2引数はnullptrと比較している。ポイントは、calls()で、引数にファンクションオブジェクトラムダ式を渡すことで、それが、Mock化した関数引数を受ける形で呼び出される。

http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.expectation.actions

今回は、構築されたオブジェクトの参照である第1引数 s に着目し、sに関連するEXPECTを記述する。

            MOCK_EXPECT(s._internal_get_untyped_deleter).once().returns(&pd);
            MOCK_EXPECT(boost::detail::sp_enable_shared_from_this).once().with(&s, &t, &t);

このように、ある程度まで処理が進まないと確定しないオブジェクトに関わるテストの期待値を、随時追加していくことが可能である。

            MOCK_EXPECT(s._internal_get_untyped_deleter).once().returns(&pd);

では、returns()というアクションを呼び出している。

http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.expectation.actions

これは、引数戻り値として戻すためのアクションである。

テストケースのコードの冒頭で、pdをローカル変数としてデフォルトコンストラクトしており、このアドレスをreturnsで返している。

    boost::detail::sp_ms_deleter<TestClass> pd;

すると、テスト対象コードの、pt._internal_get_untyped_deleter() が、pdのアドレスを返すことになる。

    boost::detail::sp_ms_deleter< T > * pd = static_cast<boost::detail::sp_ms_deleter< T > *>( pt._internal_get_untyped_deleter() );

よって、テスト対象コードの以下のような呼び出しの期待値を、

    pd->set_initialized();

以下のように記述することが可能となる。

    MOCK_EXPECT(pd.set_initialized).once();

テスト対象コードのpdの指すオブジェクトが、テストケースのコードの冒頭で定義されたpdになるため、このテストは成功する。

ちなみに、以下のように、オブジェクトを別物に変えた場合、期待していないオブジェクト(pd)からset_initialized()が呼び出されたとして、テストケースは失敗する。つまり、thisポインタが期待通りかが正しくチェックされているということだ。

    boost::detail::sp_ms_deleter<TestClass> pd, pd2;
    /* ... */
    MOCK_EXPECT(pd2.set_initialized).once();

テストコードの実行

テストコードは、テストケースの最後でキックされる。

BOOST_AUTO_TEST_CASE( test1 ) {

/* ... */

boost::make_shared<TestClass>(1, 2, 3);

mock::reset();

}

テスト後は、Mockの期待値などをクリアする為に、mock::reset()を呼び出している。http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.reset

全体構成

以下が、turtleを用いてテストコードを書く場合の全体構成となる。

#define BOOST_AUTO_TEST_MAIN
#include <boost/test/auto_unit_test.hpp>
#include <turtle/mock.hpp>

テスト対象クラスや関数本体(テンプレートの場合)

MOCK_CLASSやMOCK_FUNCTION (完全特殊化やオーバーロードを利用)

BOOST_AUTO_TEST_CASE( テストケース1 ) {
    MOCK_EXPECT() を列挙
    テスト対象をキック
    mock::reset();
}
BOOST_AUTO_TEST_CASE( テストケース2 ) {
    MOCK_EXPECT() を列挙
    テスト対象をキック
    mock::reset();
}

順序に関して

今回は、呼び出し順序についてはチェックしなかったが、以下の方法で期待値の設定が可能である。

http://turtle.sourceforge.net/turtle/reference.html#turtle.reference.expectation.sequence

しかし、多くの場合、Aの結果を利用してBとCを行う場合、BとCの順序は関係ないことが多い。そして、BとCがAよりも後に実行されるべき理由は、多くの場合、BとCがAの結果を使っているからだろう。そのような場合、withでのチェックや、callsによるMOCK_EXPECTの追加(一種の遅延評価)、などを行えば、順序に頼らなくても目的のチェックができるように思う。

テストは、Howの部分に立ち入らず、Whatに関して行うべきであろうから、この方向性は間違っていないと思う。

なお、make_sharedを例とした今回のMockの実装は、テスト論ではなく、turtleでどこまでできるのかを試したいということを主眼としている。よって、実装の詳細かな?という部分も気にせず、利用している関数やクラスを片っ端からMock化してみた。