2011年12月10日

BOOST_TEST_DONT_PRINT_LOG_VALUE()が効くBOOST_CHECK_EQUAL_COLLECTIONS()を作る

この記事は……

Boost Advent Calendar 2011の一環として書かれた。ボレロ村上さんから引き継いだ、10日目である。

Advent Calendarとは何か? については、例えば師走を楽しもう。技術系アドベントカレンダーの魅力とは - @ITを参照の事。

参加者を見回すと、STAR WARSに出てくるシスの暗黒卿ばりに黒のローブを引きずったC++闇の軍団ばっかりで非常に恐ろしい。しかし、話を振られてしまったら、参加するしかないじゃない! あなたも、私も……!

なお、当記事中で言及するBoostのバージョンは、記事執筆時点での最新バージョンであるVersion 1.48.0を前提としている。

BOOST_CHECK_EQUAL_COLLECTIONS()マクロ

Boost.Testにおいて、コンテナに格納されている値を検証するにはBOOST_CHECK_EQUAL_COLLECTIONS()マクロを利用する。他のマクロと同様、BOOST_WARN_EQUAL_COLLECTIONS(), BOOST_REQUIRE_EQUAL_COLLECTIONS()という、異なるlevelのマクロも存在する。この記事では便宜上、これら3つをひっくるめてBOOST_CHECK_EQUAL_COLLECTIONSと呼ぶ事にする。

詳細な使用法は、The UTF testing tools referenceBOOST_<level>_EQUAL_COLLECTION項を参照。

BOOST_TEST_DONT_PRINT_LOG_VALUE()マクロ

BOOST_TEST_DONT_PRINT_LOG_VALUE()マクロは、BOOST_CHECK_EQUAL()等で検証した値をログに出力しない事を指定する物だ。BOOST_CHECK_EQUAL()等で検証するオブジェクトは出力ストリームへのoperator<<をサポートしている必要が有る。が、全ての型がそれをサポートしているという事はあり得ない。例えばstd::pairがそれに当たる。そういった型をこのマクロで指定すると、テスト結果のログに、その型のオブジェクトの値は出力されなくなる。

詳細はTest log outputを参照。

BOOST_TEST_DONT_PRINT_LOG_VALUE()対応BOOST_CHECK_EQUAL_COLLECTIONS

さて、BOOST_TEST_DONT_PRINT_LOG_VALUE()マクロだが、困った事にBOOST_CHECK_EQUAL_COLLECTIONS()マクロでのログ出力においては適応されない。これは困る。

という訳で、BOOST_TEST_DONT_PRINT_LOG_VALUE()に対応したBOOST_CHECK_EQUAL_COLLECTIONS()を作ってみる。

BOOST_CHECK_EQUAL_COLLECTIONS()の実装を覗く

まず、BOOST_CHECK_EQUAL_COLLECTIONS()がどのように値を出力しているのかを調べる。boost/test/test_tools.hppの272行目にBOOST_CHECK_EQUAL_COLLECTIONS()の定義が在る。BOOST_EQUAL_COLLECTIONS_IMPL()を呼び出すだけの簡単な物だ。

#define BOOST_CHECK_EQUAL_COLLECTIONS( L_begin, L_end, R_begin, R_end )		 \
	BOOST_EQUAL_COLLECTIONS_IMPL( L_begin, L_end, R_begin, R_end, CHECK )

次に、同ファイル262行目。BOOST_EQUAL_COLLECTIONS_IMPL()の定義。BOOST_CHECK_EQUAL_COLLECTIONS()の引数として渡した各iterator、L_begin, L_end, R_begin, R_endの4つは、boost::test_tools::tt_detail::equal_coll_impl()に引数として渡されているのが分かる。関数の引数として使われているだけなので、これ以降の部分には注意しなくて良い。

#define BOOST_EQUAL_COLLECTIONS_IMPL( L_begin, L_end, R_begin, R_end, TL )	  \
	BOOST_TEST_TOOL_IMPL( check_impl, ::boost::test_tools::tt_detail::equal_coll_impl( \
		(L_begin), (L_end), (R_begin), (R_end) ), "", TL, CHECK_EQUAL_COLL ),   \
	4,																		  \
	BOOST_STRINGIZE( L_begin ), BOOST_STRINGIZE( L_end ),					   \
	BOOST_STRINGIZE( R_begin ), BOOST_STRINGIZE( R_end ) )					  \
/**/

そして、equal_coll_impl()。631行。値の出力は、単純にiteratorをdereferenceしているだけだと言う事が分かる。

template <typename Left, typename Right>
inline predicate_result
equal_coll_impl( Left left_begin, Left left_end, Right right_begin, Right right_end )
{
	predicate_result	res( true );
	std::size_t		 pos = 0;

	for( ; left_begin != left_end && right_begin != right_end; ++left_begin, ++right_begin, ++pos ) {
		if( *left_begin != *right_begin ) {
			res = false;
			res.message() << "\nMismatch in a position " << pos << ": "  << *left_begin << " != " << *right_begin;
		}
	}
	// ((後は関係ないので略))
}

成る程、これじゃBOOST_TEST_DONT_PRINT_LOG_VALUE()が何をやろうが意味がない。

BOOST_TEST_DONT_PRINT_LOG_VALUE()の実装を覗く

では、BOOST_TEST_DONT_PRINT_LOG_VALUE()はどうなっているのだろうか? このマクロが定義されているboost/test/test_tools.hppの340行目を見てみる。

#define BOOST_TEST_DONT_PRINT_LOG_VALUE( the_type )		 \
namespace boost { namespace test_tools {					\
template<>												  \
struct print_log_value<the_type > {						 \
	void operator()( std::ostream&, the_type const& ) {}	\
};														  \
}}														  \
/**/

boost::test_tools::print_log_valueテンプレートクラスを、BOOST_TEST_DONT_PRINT_LOG_VALUE()の引数で指定された型で特殊化しているだけだ。この特殊化するprint_log_valueは、std::ostreamの参照と、特殊化に用いた型のconst参照を取る、何もしないoperator()をメンバに持つ。BOOST_TEST_DONT_PRINT_LOG_VALUE()の提供する機能から考えると、BOOST_CHECK_EQUAL()等、同マクロの影響を受ける物は、内部でprint_log_valueを使って値を出力しているであろう事が容易に想像出来る。

BOOST_TEST_DONT_PRINT_LOG_VALUE()の影響を受けるBOOST_CHECK_EQUAL_COLLECTIONSを実装する

という事は、boost::test_tools::tt_detail::equal_coll_impl()内部での値の出力に、なんとかしてboost::test_tools::print_log_value::operator()を適用出来ればいい、という事になる。Boost.Testの実装を元に、それを実現させた物が以下のコードとなる。

Boostのそれと衝突しないよう、aslib名前空間、及び"ASLIB_"というprefixを採用している。

#include <boost/test/test_tools.hpp>

#define ASLIB_BOOST_WARN_EQUAL_COLLECTIONS( L_begin, L_end, R_begin, R_end )          \
    ASLIB_BOOST_EQUAL_COLLECTIONS_IMPL( L_begin, L_end, R_begin, R_end, WARN )
#define ASLIB_BOOST_CHECK_EQUAL_COLLECTIONS( L_begin, L_end, R_begin, R_end )         \
    ASLIB_BOOST_EQUAL_COLLECTIONS_IMPL( L_begin, L_end, R_begin, R_end, CHECK )
#define ASLIB_BOOST_REQUIRE_EQUAL_COLLECTIONS( L_begin, L_end, R_begin, R_end )       \
    ASLIB_BOOST_EQUAL_COLLECTIONS_IMPL( L_begin, L_end, R_begin, R_end, REQUIRE )
#define ASLIB_BOOST_EQUAL_COLLECTIONS_IMPL( L_begin, L_end, R_begin, R_end, TL )      \
    BOOST_TEST_TOOL_IMPL( check_impl, aslib::boost::test_tools::tt_detail::equal_coll_impl( \
        (L_begin), (L_end), (R_begin), (R_end) ), "", TL, CHECK_EQUAL_COLL ),   \
    4,                                                                          \
    BOOST_STRINGIZE( L_begin ), BOOST_STRINGIZE( L_end ),                       \
    BOOST_STRINGIZE( R_begin ), BOOST_STRINGIZE( R_end ) )                      \
/**/

namespace aslib { namespace boost { namespace test_tools { namespace tt_detail {
template <typename Left, typename Right>
	inline ::boost::test_tools::predicate_result
	equal_coll_impl( Left left_begin, Left left_end, Right right_begin, Right right_end )
	{
		using namespace ::boost::test_tools;

		predicate_result    res( true );
		std::size_t         pos = 0;

		print_log_value<std::remove_cv<std::remove_reference<decltype( *left_begin)>::type>::type> lPrintLogValue;
		print_log_value<std::remove_cv<std::remove_reference<decltype(*right_begin)>::type>::type> rPrintLogValue;

		for( ; left_begin != left_end && right_begin != right_end; ++left_begin, ++right_begin, ++pos ) {
			if( *left_begin != *right_begin ) {
				std::ostringstream buf;
				res = false;
				res.message() << "\nMismatch in a position " << pos << ": ";
				lPrintLogValue(buf, *left_begin);
				res.message() << buf << " != ";
				rPrintLogValue(buf, *right_begin);
				res.message() << buf;
			}
		}

		if( left_begin != left_end ) {
			std::size_t r_size = pos;
			while( left_begin != left_end ) {
				++pos;
				++left_begin;
			}

			res = false;
			res.message() << "\nCollections size mismatch: " << pos << " != " << r_size;
		}

		if( right_begin != right_end ) {
			std::size_t l_size = pos;
			while( right_begin != right_end ) {
				++pos;
				++right_begin;
			}

			res = false;
			res.message() << "\nCollections size mismatch: " << l_size << " != " << pos;
		}

		return res;
	}
}}}}

以下に、上で作成したASLIB_BOOST_CHECK_EQUAL_COLLECTIONS()、及びlevel違いの2つのマクロの動作を検証するBoost.Testコードを示す。
INSERT_BOOST_ORIGINAL_MACRO_CHECKER#defineする事で、オリジナルのBOOST_CHECK_EQUAL_COLLECTIONS()を用いたテストケースを追加出来る。その場合、std::pair<int, int>を右辺値に取るoperator<<が無い事に起因するコンパイルエラーが発生する事を確認出来る筈だ。

typedef std::vector<std::pair<int, int>> PVec;
namespace {
	struct TestFixture {
		TestFixture() {
			for(int i = 1; i <= 5; ++i) expected.push_back(std::make_pair(i, i * 10));
			actual = expected;
			actual[1] = std::make_pair(actual[1].first, actual[1].second * 11);
		}
		PVec expected, actual;
	};
}
BOOST_TEST_DONT_PRINT_LOG_VALUE(PVec::value_type);

BOOST_FIXTURE_TEST_SUITE(Test, TestFixture)
	BOOST_AUTO_TEST_CASE(testWarn) {
		boost::unit_test::unit_test_log.set_threshold_level(boost::unit_test::log_warnings);

		ASLIB_BOOST_WARN_EQUAL_COLLECTIONS(std::begin(actual), std::end(actual), std::begin(expected), std::end(expected));
	}
	BOOST_AUTO_TEST_CASE(testCheck) {
		ASLIB_BOOST_CHECK_EQUAL_COLLECTIONS(std::begin(actual), std::end(actual), std::begin(expected), std::end(expected));
	}
	BOOST_AUTO_TEST_CASE(testReq) {
		ASLIB_BOOST_REQUIRE_EQUAL_COLLECTIONS(std::begin(actual), std::end(actual), std::begin(expected), std::end(expected));
	}

#ifdef INSERT_BOOST_ORIGINAL_MACRO_CHECKER
	BOOST_AUTO_TEST_CASE(testOrigin) {
		BOOST_CHECK_EQUAL_COLLECTIONS(std::begin(actual), std::end(actual), std::begin(expected), std::end(expected));
	}
#endif
BOOST_AUTO_TEST_SUITE_END()

実装には、見ての通りC++11のdecltypeを利用している。恐らくこれが、BOOST_CHECK_EQUAL_COLLECTIONSにBOOST_TEST_DONT_PRINT_LOG_VALUE()の影響を与える事が出来ない理由だろう。decltypeを使わないと、iteratorをdereferenceした結果の型が分からず、boost::test_tools::print_log_valueに必要な、適切な型が決定出来ない。std::iterator_trais::referenceを使えば、C++03規格上でもiteratorをderefereceした結果の型を求める事が出来た。詳細は当記事のコメント欄を参照。iorateさん有り難う御座います。

今後、各ベンダのC++コンパイラのC++11準拠度が上がっていけば、オリジナルのBOOST_CHECK_EQUAL_COLLECTIONSもBOOST_TEST_DONT_PRINT_LOG_VALUE()の影響を受けるよう修正されるのではないだろうか。

そんな訳で

Boost Advent Calendar 2011の10日目であるこの記事ではBOOST_TEST_DONT_PRINT_LOG_VALUE()を適用出来るBOOST_CHECK_EQUAL_COLLECTIONSの実装を示した。

では、ここら辺で筆を置き、11日目担当であるiorateさんにバトンタッチしたい。

posted by 天井冴太 at 21:52 | Comment(7) | TrackBack(0) | Other | このブログの読者になる | 更新情報をチェックする
投票お願いします 人気blogランキング - 投票する
この記事へのコメント
Iterator を dereference した結果の型なら std::iterator_traits<Iterator>::reference で取れるので,C++03 でも何とかなると思います.
Posted by iorate at 2011年12月11日 23:18
iorateさんコメント有り難う御座います。

コード中の
> print_log_value<std::remove_cv<std::remove_reference<decltype( *left_begin)>::type>::type> lPrintLogValue;
> print_log_value<std::remove_cv<std::remove_reference<decltype(*right_begin)>::type>::type> rPrintLogValue;
を、
print_log_value<std::remove_reference<std::iterator_traits<Left>::reference>::type> lPrintLogValue;
print_log_value<std::remove_reference<std::iterator_traits<Right>::reference>::type> rPrintLogValue;
と変更して試してみると、確かに通りますね……iterator_traisの存在を完璧に忘れていた……orz
しかもremove_cvが要らなくなって若干見通しが良くなったという……orz

しかしそうなると、なんでBOOST_CHECK_EQUAL_COLLECTIONSはBOOST_TEST_DONT_PRINT_LOG_VALUE()の影響を受けない作りになってるんでしょうね……?
Posted by 天井冴太 (AmaiSaeta) at 2011年12月13日 03:07
Boost.Test のことはよく分からないので何とも言えませんが,設計or実装上のミスであれば Boost の ML か Trac に投げてみるといいんじゃないでしょうか.

>remove_cvが要らなくなって
いることはないと思います
Posted by iorate at 2011年12月14日 05:08
>>remove_cvが要らなくなって
>いることはないと思います
あれ、ホントだ。
VC++2010でエラーが出たから付けた記憶が有るけど気の所為だったのかな……?
Posted by 天井冴太 (AmaiSaeta) at 2011年12月14日 10:35
コード中で、テンプレートパラメータがなくなっているようです。

template
inline predicate_result
equal_coll_impl( Left left_begin, Left left_end, Right right_begin, Right right_end )



template <typename Left, typename Right>
inline predicate_result
equal_coll_impl( Left left_begin, Left left_end, Right right_begin, Right right_end )


「Boostのそれと衝突しないよう、aslib名前空間、及び"ASLIB_"というprefixを採用している。」のところのコードで「#include」と書いてあってインクルードするファイル名がありませんが、これは意図したものでしょうか。

vectorのテンプレート引数がなくなっているようです。

typedef std::vector> PVec;



typedef std::vector<std::pair<int, int> > PVec;
Posted by 高橋 晶 at 2012年02月06日 11:41
高橋さん、ご指摘有り難う御座います。
このblogをホスティングしてるSeesaa blogが広告挿入の為に使用しているJavaScriptの不具合が原因でした。
問題となる広告挿入機能をオフにしたので、現在はマトモなコードとして見られる筈……
Posted by 天井冴太 at 2012年02月06日 17:39
ありがとうございます!無事正常なコードが見れました!
Posted by 高橋 晶 at 2012年02月06日 17:41
コメントを書く
お名前: [必須入力]

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

コメント欄右下をドラッグ&ドロップすると、コメント欄の拡大縮小が出来ます。
認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック