Rump kernelを使ってATFテストを書く

この記事は[NetBSD Advent Calendar 2015](http://qiita.com/advent-calendar/2015/netbsd)の20日目として書かれました。

#はじめに

NetBSDにはATFというテストフレームワークが用意されており、ユーザランドプログラムやカーネルのための[テストが定期的に実行され](http://releng.netbsd.org/test-results.html)され、コード変更によってリグレッションを起こしていないかチェックされています。

またNetBSDには[rump kernel](https://wiki.netbsd.org/rumpkernel/)という仕組みがあり、ユーザプロセスとしてNetBSDカーネルを実行できます。ATFテストにはrump kernelを使ったテストがたくさんあります。

本記事では、特にNetBSDカーネルのネットワーク機能のATFテストをrump kernelを使って書く方法について説明します。

#ATFテストの書き方

基本的なATFテストの書き方は[Creating atf-based tests for NetBSD src](https://wiki.netbsd.org/tutorials/atf/)に書いてあります。ATFテストはC言語とシェルスクリプトで書く方法がありますが、rump kernelを使ったテストは今ではシェルスクリプトで書くことが推奨されているので[^atf-rump]、そちらの書き方の説明をします。

[^atf-rump]: ...とrump kernelの作者の人が言ってました。

##シェルスクリプトでATFテストを書く
簡単なテストを書いてみます。

```shell-session
atf_test_case test1 cleanup
atf_test_case test2 cleanup

test1_head()
{
	atf_set "descr" "Tests for something of test1"
	atf_set "require.progs" "touch" "test"
}

test2_head()
{
	atf_set "descr" "Tests for something of test2"
}

test1_body()
{
	atf_check -s exit:0 touch testfile
	atf_check -s exit:0 test -f testfile
}

test2_body()
{
	// do some tests...
}

test1_cleanup()
{
	rm -f testfile
}

test2_cleanup()
{
	// cleanup
}

atf_init_test_cases()
{
	atf_add_test_case test1
	atf_add_test_case test2
}
```

ここではtest1とtest2という2つのテストを書きました。

まず`atf_test_case`と`atf_init_test_cases`と`atf_add_test_case`を使って、ATFにtest1とtest2の2つのテストを追加することを宣言します[^atf_test_case]。`atf_test_case`の第二引数のcleanupはこのテストにcleanup部があることを宣言しています[^cleanup]。

[^atf_test_case]: 重複しているように見えますが、`atf_test_case`と`atf_init_test_cases`(`atf_add_test_case`)は両方とも必要です。

[^cleanup]: cleanupを書き忘れるとcleanup部が実行されないので忘れずに書きましょう。

テスト自体はheadとbodyとcleanupの3つで構成されています。headはテストのメタデータを書く場所で、テストの概要やテストに必要なコマンドを列挙します[^progs]。bodyはテスト本体です。テスト環境をセットアップしたり、`atf_check`等のassertionコマンドを用いてテストを記述します。cleanupはテストで作成したファイルを消したり、プロセスをkillしたり、デバッグ用の出力を行ったりする場所です。bodyの後に実行されます。上の例ではbodyで作ったファイル`testfile`を消しています。

[^progs]: 例のためtouchとtestコマンドを`require.progs`に書いていますが、baseに常に入っているコマンドは書かなくても構いません。

##`atf_check`
bodyで使われている`atf_check`コマンドについて説明します。`atf_check`はコマンドラインを引数に取り、コマンドを実行して期待する結果になっているかをチェックします。`-s exit:0`は指定したコマンドが成功する(exist statusが0)ことをassertionしています。逆に失敗することを期待する場合は、`-s not-exit:0`のように書きます。

`atf_check`は他にも標準出力や標準エラー出力のチェックもできます。`-s exit:0 -o match:"hoge"`でコマンドが成功して、かつ標準出力にhogeという文字列が含まれているかをチェックします。`-s not-exit:0 -e match:"fuga"`でコマンドが失敗して、かつ標準エラー出力にfugaという文字列が含まれているかをチェックします。出力を無視したい時は`-o ignore`や`-e ignore`と書きます。

コマンドラインでパイプ等を使いたい場合は、`atf_check ... -x "ls |grep hoge"`のように書きます。

ほとんどの場合は`atf_check`で事足りますが、単に文字列の一致をチェックをする`atf_equal`などを使う場合もあります。

#Rump kernelを使ったATFテストの書き方

rump kernelを使って、NetBSDの通信機能が正しく動作するかpingを使ってチェックする、簡単なテスト(`ping_test`)を書いてみます。上記説明と重複する部分は省略しています。

##Rump kernelを使ったATFテスト例

```
inetserver="rump_server -lrumpnet -lrumpnet_net -lrumpnet_netinet"
inetserver="$inetserver -lrumpnet_netinet6 -lrumpnet_shmif"

SOCK1=unix://commsock1
SOCK2=unix://commsock2
BUS=bus

IP1=10.0.0.1
IP2=10.0.0.2
TIMEOUT=1

setup_rump_server
{
	local sock=$1
	local ip=$2
	local bus=$3

	atf_check -s exit:0 ${inetserver} $sock

	export RUMP_SERVER=$sock

	atf_check -s exit:0 rump.ifconfig shmif0 create
	atf_check -s exit:0 rump.ifconfig shmif0 linkstr $bus
	atf_check -s exit:0 rump.ifconfig shmif0 $ip
	atf_check -s exit:0 rump.ifconfig shmif0 up
	atf_check -s exit:0 rump.ifconfig -w 10
}

ping_test_body
{
	setup_rump_server $SOCK1 $IP1 $BUS
	setup_rump_server $SOCK2 $IP2 $BUS

	export RUMP_SERVER=$SOCK1
	atf_check -s exit:0 -o ignore rump.ping -n -c 1 -w $TIMEOUT $IP2

	export RUMP_SERVER=$SOCK2
	atf_check -s exit:0 -o ignore rump.ping -n -c 1 -w $TIMEOUT $IP1
}

ping_test_cleanup
{
	env RUMP_SERVER=$SOCK1 rump.halt
	env RUMP_SERVER=$SOCK2 rump.halt
}
```

このテストでやっていることは、2つのrump kernelプロセスを立ち上げネットワークインタフェースとIPを設定(`setup_rump_server`)、双方からpingが成功するかチェック、最後にcleanupでrump kernelプロセスを停止しています。

##`rump_server`

Rump kernelプロセスは`rump_server`コマンドで立ち上げます。Rump kernelはモジュール化(共有ライブラリ化)されており、必要な機能だけをロードして使います。この例では、ネットワーク機能とrump kernel用ネットワークインタフェース(`shmif`)をロードしています。

`rump_server`の第二引数には、`rump_server`プロセスと他のユーザプロセスが通信するためのソケットを指定します。ATFテストではローカルで閉じているのでUnixドメインソケットを使っています。以降、当該`rump_server`に対して何かコマンドを実行したい場合は、`RUMP_SERVER`環境変数にソケットを指定して、`rump.XXX`というコマンドを使って実行します。この例では`rump.ifconfig`や`rump.ping`コマンドを使っています[^hijack]。

[^hijack]: Rump kernelにはrump化されたコマンドを使う方法の他に、librumphijack.soというシステムコールをハイジャックする共有ライブラリを使って、rump化されていないユーザプログラムを使う方法もあります。

##`shmif`

`shmif`はrump kernel用のネットワークインタフェースです。ファイルを介してEthernetフレームをrump kernelプロセス間でやりとりします。`rump.ifconfig ... linkstr $bus`でファイル名を指定しています。

Rump kernel間で送受信されたEthernetフレームは、すべてファイルに保存されているので中身を見ることが簡単にできます。`shmif_dumpbus -p - $bus 2>/dev/null| tcpdump -n -e -r -`とやると`tcpdump`でフレームを表示させることができます。

#おわりに

簡単ですが、rump kernelを使ってATFテストを書く方法を説明してみました。

カーネルはコード規模が大きいので、既存の動作を壊さずコードを変更することが難しいです。Rump kernelでネットワーク機能をはじめとするカーネル機能のテストを書いておくことで、開発のコストが大幅に少なくなります。

カーネルのコードを改良することだけがカーネル開発ではありません。カーネルのためのATFテストを追加することもカーネル開発においてはとしても重要です。この記事がそのための一助になれば幸いです。