それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

MySQL 探索記 ~自作プラグイン開発でユニットテストを気合いでサポートするための色々~

はじめに

どうも、どうにかこうにか MySQL の自作プラグイン開発で mysql-server 本体に手を加えずに gtest 等*1 を使ったユニットテストを実行したいと思い気がつけば 3 連休を溶かしていたけんつです。

MySQLユニットテストは unittests/ 以下に実装が存在しており、各 Storage Engine やその他プラグインの実装ディレクトリ下には基本的にユニットテストは存在しない。*2 そのため、ユニットテストを書きたい場合は mysql-server 下の実装に手を加える必要がある。しかし、大抵の場合 git repository で自作プラグインを管理した時にどうにか mysql-server の実装にパッチを当てるなりしてビルドする必要が出てきてしまう。それはあまり嬉しいことではない。
というわけで、ここではどうにか mysql-server に手を加えずにユニットテストをサポートする方法をまとめる。

長々と書いたが、ここでやりたいことは unittest/ 以外で gtest を使ったテストを実装することである。

最初に書いておくが、ADD_EXECUTABLE, TARGET_LINK_LIBRARY で頑張る方法は大変苦行なのでおすすめしない。

前提

libmysqlclient-dev のみでは全然やりたいことなんか出来ねぇぜ*3 という大変ロックな人、または自作ストレージエンジンをやりたがる奇特な人向けとなっている。
つまり何かというと、mysql-server のソースコードを引っ張ってきて、自作プラグインソースコードを配置し、MySQL ごとビルドする必要に迫られる上にそれをどうにか git repository で管理したい人向けである。

この記事ではみんな大好き EXAMPLE ストレージエンジンに少しだけ実装を追加し、その実装を gtest を使ってテストを書き、それを実行することを目的とする。
想定しているディレクトリ構成は以下の通り。

./storage/example/
|-- CMakeLists.txt
|-- ha_example.cc
|-- ha_example.h
|-- sample.cc
|-- sample.h
`-- tests
    |-- CMakeLists.txt
    `-- sample_test.cc

1 directory, 7 files

この状態にした上で MySQL をビルドし tests ディレクトリ以下に存在するテストを実行する実行可能ファイルを吐き出させたいというところに目的がある。

気合で gtest を使えるようにする

テスト対象の実装を追加

以下の2つのファイルを storage/example 以下に追加する。

// sample.cc
#include <mysql/psi/mysql_file.h>
#include "sample.h"


bool SampleUtil::returnTrue() {
  return true;
}

void SampleUtil::seek() {
  mysql_file_seek(0,0,0,0);
}
// sample.h
#ifndef EXAMPLE_SAMPLE_H
#define EXAMPLE_SAMPLE_H

class SampleUtil {
 public:
  static bool returnTrue();
  static void seek();
};

#endif 

EXAMPLE ストレージエンジンのビルド対象に追加する

上記のファイルを EXAMPLE ストレージエンジンのビルド対象に含める。

diff --git a/storage/example/CMakeLists.txt b/storage/example/CMakeLists.txt
index 99e79270399..5f368c667bf 100644
--- a/storage/example/CMakeLists.txt
+++ b/storage/example/CMakeLists.txt
@@ -24,7 +24,7 @@ DISABLE_MISSING_PROFILE_WARNING()
 ADD_DEFINITIONS(-DMYSQL_SERVER)
 
 IF(WITH_EXAMPLE_STORAGE_ENGINE AND NOT WITHOUT_EXAMPLE_STORAGE_ENGINE)
-  MYSQL_ADD_PLUGIN(example ha_example.cc
+  MYSQL_ADD_PLUGIN(example ha_example.cc sample.cc
     STORAGE_ENGINE
     DEFAULT
     LINK_LIBRARIES ext::zlib
@@ -36,3 +36,6 @@ ELSEIF(NOT WITHOUT_EXAMPLE_STORAGE_ENGINE)
     LINK_LIBRARIES ext::zlib
     )
 ENDIF()
+
+ADD_SUBDIRECTORY(tests/)

テストファイルを追加する

tests ディレクトリを掘ってからテストファイルと CMakeLists.txt を追加する。

// tests/sample_test.cc
#include <gtest/gtest.h>

#include "sample.h"

TEST(sample_returnTrue, success) {
  bool res = SampleUtil::returnTrue();
  ASSERT_EQ(res, true);
}

TEST(sample_seek, success) {
  SampleUtil::seek();
}
# tests/CMakeLists.txt
INCLUDE_DIRECTORIES(
        ${CMAKE_SOURCE_DIR}/storage/example
)

SET(TESTS
        sample_test.cc
        )

SET(ALL_EXAMPLE_TESTS)
FOREACH (test ${TESTS})
    LIST(APPEND ALL_EXAMPLE_TESTS ${test})
ENDFOREACH ()

MYSQL_ADD_EXECUTABLE(example_tests ${ALL_EXAMPLE_TESTS}
        ENABLE_EXPORTS
        ADD_TEST example_tests
        LINK_LIBRARIES gunit_large server_unittest_library
        )

ビルドする

今回は EXAMPLE ストレージエンジンもビルドしたいので以下のようにビルドする。

$ mkdir build && cd $_
$ cmake ../ -DCMAKE_BUILD_TYPE=Debug -DWITH_BOOST=./boost -DDOWNLOAD_BOOST=1 -DWITH_EXAMPLE_STORAGE_ENGINE=1
$ make -j12

テストを実行する

$ cd build/runtime_output_directory
$ ./example_tests 
[==========] Running 2 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 1 test from sample_returnTrue
[ RUN      ] sample_returnTrue.success
[       OK ] sample_returnTrue.success (0 ms)
[----------] 1 test from sample_returnTrue (0 ms total)

[----------] 1 test from sample_seek
[ RUN      ] sample_seek.success
[       OK ] sample_seek.success (0 ms)
[----------] 1 test from sample_seek (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 2 test suites ran. (0 ms total)
[  PASSED  ] 2 tests.

ヨシ!

ここまでに何が行われたのか

この様に書いてみれば大変楽な作業に思えるが、ここに至るまでに 3 日程費やしただけあり、どうやってここまでたどり着いたかが割と重要だった。

既存実装の調査と紆余曲折

storage/perfschema の unittest

storage/**/ 下に unittest が存在する場合というのがまず一番嬉しい(簡単であろう)ケースである。なので、まずはそこから調査する。ここでは perfschema が unittest を利用していたのでそこから。
そして恐らく重要なのはこのあたりである。これは unittest ディレクトリ以下のテストケースに対して MYSQL_ADD_EXECUTABLE*4 を実行し、テストファイルごとに必要なライブラリをリンクし、実行可能ファイルを生成してくれる。

# storage/perfschema/unittest/CMakeLists.txt
...
MACRO (PFS_ADD_TEST name)
  MYSQL_ADD_EXECUTABLE(${name}-t ${name}-t.cc ADD_TEST ${name})
  TARGET_LINK_LIBRARIES(${name}-t
    mytap perfschema mysys pfs_server_stubs strings ext::icu)
ENDMACRO()

SET(tests
 pfs_instr_class
 pfs_instr_class-oom
 pfs_instr
 pfs_instr-oom
 pfs_account-oom
 pfs_host-oom
 pfs_user-oom
 pfs_noop
 pfs
 pfs_misc
 pfs_mem
)
FOREACH(testname ${tests})
  PFS_ADD_TEST(${testname})
ENDFOREACH()
...

mysql-server/storage/perfschema/unittest/CMakeLists.txt at trunk · mysql/mysql-server · GitHub

ここにあるテストコードは mytap*5 を利用したものだが、これの boost test library 版や gtest 版を実装してみたところ、pfs や dd 等のテスト実行時に呼ばれる依存関係を解決できずにビルドに失敗してしまった。*6
また TARGET_LINK_LIBRARIES に必要となるライブラリをひたすら指定するという、これは mysql-server の実装を隅から隅まで知らないといけないのレベルで面倒だったので断念。

脳筋戦法

ここで心が折れそうになり、だったら ha_example をテストの実行可能ファイルにそのままリンクしてやればいいのではという脳筋戦法にたどり着いた。しかし、その場合も perfschema と同様の問題にぶち当たる上に MYSQL_ADD_PLUGIN に指定されるビルドタイプの影響をモロに受ける*7 という別問題が出てきたので早々に断念した。

InnoDB の unittest

やはり InnoDB である。やはり InnoDB、お前は最高である。
というわけで gtest を利用しているコードを検索しまくっていると unittest/gunit/innodb/ 以下にごっそり存在していることが分かったので、それを参考にする。

# unittet/gunit/innodb/CMakeLists.txt

...

INCLUDE_DIRECTORIES(
  ${CMAKE_SOURCE_DIR}/sql
  ${CMAKE_SOURCE_DIR}/storage/innobase/include
)

...

SET(TESTS
  #example
  fil_path
  ha_innodb
  log0log
  mem0mem
  os0file
  os0thread-create
  srv0conc
  sync0rw
  ut0crc32
  ut0lock_free_hash
  ut0math
  ut0mem
  ut0new
  ut0rnd
)

...

SET(ALL_INNODB_TESTS)
FOREACH(test ${TESTS})
  LIST(APPEND ALL_INNODB_TESTS ${test}-t.cc)
ENDFOREACH()

...

MYSQL_ADD_EXECUTABLE(merge_innodb_tests-t ${ALL_INNODB_TESTS}
  ENABLE_EXPORTS
  ADD_TEST merge_innodb_tests-t
  LINK_LIBRARIES gunit_large server_unittest_library
  )

...

https://github.com/mysql/mysql-server/blob/trunk/unittest/gunit/innodb/CMakeLists.txt

やっていることの本質としてはこの前に出てきた perfschema と同じではある。ストレージエンジンの実装に存在する必要な header ファイルの場所と、テストで利用しているであろう boost, gmock の header を教えてやってから、複数のテストをまとめた実行可能ファイルを生成している。そしてここで一番大きいのは MYSQL_ADD_EXECUTABLE に出てくる gunit_large, server_unittest_library の存在が大変それっぽい。

ここで最初に紹介した方法に戻り、実装を少し修正してからビルドすると gunit_large, server_unittest_library をリンクし直していることがわかる。

$ make -j12
...
[ 96%] Linking CXX shared library library_output_directory/libserver_unittest_library.so
[ 96%] Linking CXX executable ../runtime_output_directory/mysqld
[ 96%] Built target server_unittest_library
Consolidate compiler generated dependencies of target example_tests
[ 96%] Linking CXX executable ../../../runtime_output_directory/avid_tests
[ 96%] Linking CXX executable ../../../runtime_output_directory/pfs_connect_attr-t
[ 96%] Linking CXX executable ../../../runtime_output_directory/group_replication_member_version-t
[ 96%] Linking CXX executable ../../../../plugin_output_directory/minimal_chassis_test_driver-t
[ 96%] Linking CXX executable ../../../runtime_output_directory/group_replication_member_info-t
[ 96%] Linking CXX executable ../../../../plugin_output_directory/reference_cache-t
[ 96%] Linking CXX executable ../../../runtime_output_directory/group_replication_compatibility_module-t
[ 96%] Linking CXX executable ../../../runtime_output_directory/example_tests
[ 96%] Linking CXX executable ../../../runtime_output_directory/merge_keyring_file_tests-t
[ 96%] Linking CXX executable ../../../runtime_output_directory/merge_innodb_tests-t
[ 96%] Linking CXX executable ../../runtime_output_directory/merge_large_tests-t
[ 96%] Built target mysqld
[ 96%] Linking CXX executable ../../../runtime_output_directory/group_replication_mysql_version_gcs_protocol_map-t
[ 96%] Built target minimal_chassis_test_driver-t
[ 96%] Linking CXX executable ../../../runtime_output_directory/group_replication_gcs_mysql_network_provider-t
[ 96%] Built target reference_cache-t
[ 96%] Built target avid_tests
[ 96%] Linking CXX executable ../../../runtime_output_directory/merge_temptable_tests-t
...

というわけで unittest/gunit/CMakeLists.txt を見てみる。

# unittest/gunit/CMakeLists.txt
...
# gunit_large
ADD_STATIC_LIBRARY(gunit_large
  benchmark.cc
  gunit_test_main_server.cc
  test_utils.cc
  thread_utils.cc
  LINK_LIBRARIES ext::icu ext::zlib
)
...

https://github.com/mysql/mysql-server/blob/trunk/unittest/gunit/CMakeLists.txt


gunit_large に関しては明らかにそれらしい実装たちを link して static library として吐いてくれる。むしろこれが無いと mysql-server の絡む gtest が実行できない*8 のではというところまである。
mysql-server/unittest/gunit/gunit_test_main_server.cc at trunk · mysql/mysql-server · GitHub

次に server_unittest_library について。こいつは project root 直下の CMakeLists.txt に記述が存在する。

# ./CMakeLists.txt
...
MERGE_LIBRARIES_SHARED(server_unittest_library SKIP_INSTALL LINK_PUBLIC
      sql_main
      ${MYSQLD_STATIC_PLUGIN_LIBS}
      minchassis
      ext::icu
...
    ADD_LIBRARY(server_unittest_library STATIC ${DUMMY_SOURCE_FILE})
    TARGET_LINK_LIBRARIES(server_unittest_library perfschema)
    TARGET_LINK_LIBRARIES(server_unittest_library sql_main)
    TARGET_LINK_LIBRARIES(server_unittest_library minchassis)
    TARGET_LINK_LIBRARIES(server_unittest_library ext::icu)
...

https://github.com/mysql/mysql-server/blob/trunk/CMakeLists.txt


ここでは実装を見るにビルドオプションによって必要なライブラリをリンクするか、ダミーファイルを持つ static library を生成してくれそう。*9

おわりに

というわけで、3 日間に渡り格闘した気の狂ったプラグイン自作におけるユニットテストのサポートは完了した。*10
これで少しだけ mysql-server のユニットテストについて詳しくなれたと思うが、ちゃんとテストコードを実装してみて「こうは言ったものの、やっぱりだめだった」となったら気合でどうにかする。

*1:gtest 以外にも Boost Test Library や mytap なるものを使ったテストも行けそうではあるが試してはいない。恐らく gtest > mytap > Boost Test Library の順番で簡単にサポートできる。

*2: 一部、perfschema などはその通りではない

*3: 例: InnoDB のバッファープールのポインタを直接参照する等

*4:実行可能ファイルを吐いてくれる MySQL 独自の便利な cmake マクロ

*5:MySQL: Unit Testing Using TAP

*6:この記事の先頭で mysql_file_seek などが含まれているのはこのケースに当てはまる場合があるため。それらを利用しない実装であれば呼び出しても undefined references で死ぬことはなかった。

*7:EXAMPLE Storage Engine の CMakeLists.txt で言うところの DEFAULT のこと。これが MODULE_ONLY だとそもそもリンク出来ない等の仕様が存在することが分かったが、その観点で戦い始めた話はまた別の機会に。

*8:正確にはできないことも無いが途方もない労力がかかる

*9:こいつ自体がリンクされていないとどうなるかは分かったが何故そうなったかはわかっていない。sql_main が含まれていることから server_unittest_library が無いと恐らく server layer に依存した処理がビルドで死ぬと思われる。

*10:サポートしたは良いものの、まだテストコード自体はちゃんと書いていないのでもしかしたらだめかもしれない