今年のD言語フォーラムでは特にメモリ管理が多く話題に上がりました。
メモリ管理の話題の多くはD言語のGCに対する批判や、参照カウント方式の採用など、GCを使わずにD言語を使いたいというものでした。
「GCなしでもD言語は使えるのだから、GCが嫌って理由でD言語使わないとか言うのは甘え」みたいに言う人もいますが、実際のところそんなに簡単にGCを切り離すことはできません。
GCを簡単に切ることができない主要な要因は標準ライブラリ(Phobos)が使用できなくなることが大きいです。代替となるGCなしで動作する強力なライブラリがあれば、D言語はGCが…みたいな意見も少なくなるかもしれません。
そこで今回は、BetterCで動作するライブラリを書くための基本の一つ、BetterCでもunittestを動作させる方法について紹介したいと思います。
概要
- 普通に書くとbetterCでテストできない
- 単体テストを実行するテスト関数を書く
- unittestの定義に、betterCかそうじゃないかで実行有無を切り替えるよう手を加える
-
dub.json
で専用のconfigurationを追加する -
$ dub run -c=unittest-bc -b=unittest
でBetterCのテストを実行
普通にunittestできないのか?
BetterCを使用すると、D言語のランタイムが利用できなくなるため、unittestを実行することができません。D言語のunittestはdruntimeでmain関数を呼び出す前に実行されているのです。
逆に、druntimeでやっているようなことを代替する方法があれば、BetterCでもunittestを動作させることができます。
BetterCでのテスト記載方法
ここでは、hoge
というライブラリを作ることを想定し、それをテストすることを考えます。
以下のコードを、例えば hoge.ut
などのモジュールに組み込みます。
module hoge.ut;
version (D_BetterC)
{
/// static ifで使えるBetterC判定
enum bool isBetterC = true;
}
else
{
/// static ifで使えるBetterC判定
enum bool isBetterC = false;
}
/// モジュール一覧を列挙する
private static immutable allModules = [
"hoge.foo",
"hoge.bar",
"hoge.buz"
];
/// インポート
private template from(string modname)
{
mixin("import from = ", modname, ";");
}
/***************************************************************
* 単体テスト実行関数
*
* unittestを実行するコードは、
* `-betterC`かつ`-unittest`かつ`-version=HogeTest`
* を指定した場合のみ実行
*/
version(D_BetterC) version(unittest) version(HogeTest)
extern (C) int main(int argc, const char** argv)
{
import core.stdc.stdio;
static foreach (mod; allModules)
{
printf("Testing ...%s\n", mod.ptr);
static foreach (test; __traits(getUnitTests, from!mod))
{
printf("ut ...... %s\n", test.stringof.ptr);
test();
}
}
printf("Test completed!\n");
return 0;
}
次に、 hoge.foo
などでの単体テストの書き方です。
module hoge.foo;
import hoge.ut: isBetterC;
// BetterCでもBetterCじゃなくても実行されるテスト
@nogc nothrow unittest
{
// 単体テストを書く
import core.stdc.stdio;
printf("Both betterC or not.\n");
}
// BetterCの時にしか実行されないテスト
static if (isBetterC) @nogc nothrow unittest
{
// 単体テストを書く
import core.stdc.stdio;
printf("Only betterC.\n");
}
// BetterCの時には実行されないテスト
// (通常のDランタイム・Phobosを使うテスト)
static if (!isBetterC) unittest
{
// 単体テストを書く
import std.stdio;
writeln("Not betterC.");
}
そして、ビルドするためのdub.json
は以下のようになります。
{
"authors": ["SHOO"],
"copyright": "Copyright © 2021, SHOO",
"description": "Hoge library",
"license": "public domain",
"name": "hoge",
"configurations": [
{
"name": "library",
"targetType": "library"
},
{
"name": "unittest",
"targetType": "library"
},
{
"name": "unittest-bc",
"targetName": "hoge-test-unittest-bc",
"versions": ["HogeTest"],
"buildOptions": ["betterC"],
"targetType": "executable"
}
]
}
テストの実行
通常テストはこう
$ dub test
Generating test runner configuration 'hoge-test-unittest' for 'unittest' (library).
Performing "unittest" build using P:\app\dmd\bin64\dmd.exe for x86_64.
hoge ~master: building configuration "hoge-test-unittest"...
Linking...
Running hoge-test-unittest.exe
Both betterC or not.
Not betterC.
1 modules passed unittests
BetterCでテストするときはこう
$ dub run -c=unittest-bc -b=unittest
Performing "unittest" build using P:\app\dmd\bin64\dmd.exe for x86_64.
hoge ~master: building configuration "unittest-bc"...
Linking...
Running hoge-test-unittest-bc.exe
Testing ...hoge.foo
ut ...... __unittest_L6_C24()
Both betterC or not.
ut ...... __unittest_L13_C46()
Only betterC.
Testing ...hoge.bar
Testing ...hoge.buz
Test completed!
仕組み
isBetterC
unittestを書く際に、BetterCの時にテストしたいのか、そうじゃないとき(通常時)にテストしたいのかを使い分ける際、static if文で使用できるようにenum値を定義します。
enumで定義すると何がうれしいのかというと、 static if (!isBetterC)
とかけるのがうれしいです。これを定義しないと、 version (D_BetterC) {} else
と書かなければいけないので煩わしいです。
unittestの書き方
通常、BetterCであればPhobosがあってもなくても動くので、BetterCで動くことを確認すれば十分です。しかし、テストコードくらいはPhobosありで楽に書きたい…ということがあります。そのような場合は、static if (!isBetterC)
を用います。
dub.jsonの中身
各コンフィグレーションを記載しているのがポイントです。
-
"name": "library"
:
今回の例ではライブラリとして構成しているので、通常はこのコンフィグレーションが使われます。-unittest
や-betterC
は、このライブラリを使う側が制御します。 -
"name": "unittest"
:
dub test
したときにデフォルトで使われるコンフィグレーションです。つまり、このコンフィグレーションの時には、-unittest
はつくけど-betterC
はつきません。 -
"name": "unittest-bc"
:
BetterCでテストする場合に、dub run -c=unittest-bc -b=unittest
という感じに明示的に使うためのコンフィグレーションです。つまり、このコンフィグレーションの時には、-unittest
も-betterC
もつきます。この時にBetterCのテスト(テスト関数の呼び出し)を行うので、"versions": ["HogeTest"]
でテストを行うための条件付けを行っています。
ちなみに、dub test -c=unittest-bc -b=unittest
だとダメなのか?という疑問がわくかもしれませんが、dub test
コマンドが勝手に生成する*_dub_test_root.d
というテスト関数を含むモジュールがBetterCではないため、コンパイルに失敗してしまいます。dubのmainSourceFile
や--main-file
の指定などを駆使しても回避することはできません。
テスト関数
モジュール一覧の列挙が面倒くさいです。
以前一瞬だけ __traits(allMembers, moduleName)
とすることで public import
されたモジュールも検索することができたことがありましたが、今はできないので、残念ながら列挙が必要です。ここに新規作成したモジュールを追加するのを忘れがちなので注意が必要です。 core.reflection
が導入されたら状況が変わるかな?
テスト関数は、今回はライブラリなので、単純にC言語のmain関数を定義します。
アプリケーションの場合は、main関数の中の先頭でテスト時にのみ単体テストする関数を呼び出すとよいでしょう。
特に version(D_BetterC) version(unittest) version(HogeTest)
で定義されるパターンを絞っているのがポイントです。
今回はライブラリですので、基本的にメイン関数が定義されてしまうのはマズいです。
ですので、本当に必要な時に限って定義するよう調整しています。
まあ、実のところ dub run -c=unittest-bc -b=unittest
の時にだけ定義できれば良いので、 version (HogeTest)
だけが最低条件です。
テスト関数の中身では…
static foreach (mod; allModules)
ですべてのモジュールを巡回して
from!mod
でそのモジュールをimportし、
static foreach (test; __traits(getUnitTests, from!mod))
で各モジュールの単体テストを抜き出して各テストをtestという識別子にaliasしています。
test()
とすることで、unittestを実行しています。
それ以外のprintf
は、適当なお飾りです。
おわりに
今回はBetterCでの単体テスト方法について紹介しました。
実はこの方法、Phobosでも一部取り入れられているようです。(というか今回の記事のベースはPhobosの方法を参照しました)
以下のコードがそれです。ご参照ください。