カーネルのテストプログラムを作ってみる

TOPPERSの各カーネルの配布パッケージには，testというディレクトリがあり，文字通り，カーネルのテストプログラムが入れてあります。ここに入れてあるテストプログラムの多くは，作成・保守を容易にするために，テストコード生成ツール（utils/gentest.rb）を用いて作成してあります。

この記事では，テストコード生成ツールの必要性と，それを用いたテストプログラムの作成方法について，TOPPERS/ASP3カーネル（Release 3.6.0を使いました）のサービスコールをテストする例を用いて説明したいと思います。

## この例でテストする仕様

ここでは，カーネルの機能テストの例として，wup_tskサービスコールの基本機能のテストを取り上げます。具体的には，[TOPPERS第3世代カーネル（ITRON系）統合仕様書](https://www.toppers.jp/documents.html#tgki_spec)のwup_tskの【機能】の項に記載されている以下の仕様をテストするプログラムを作成してみます。

> 対象タスクが起床待ち状態である場合には，対象タスクが待ち解除される【NGKI1271】．待ち解除されたタスクには，待ち状態となったサービスコールからE_OKが返る【NGKI1272】．

## テストプログラムを作ってみる

NGKI1271をテストするには，まず，タスクを2つ用意します（以下，TASK1とTASK2と呼びます）。TASK1を起床待ち状態にしておいて，TASK2からTASK1をwup_tskした結果，TASK1が待ち解除されることを確認すればOKです。ここで，2つのタスクの優先関係により，TASK1が待ち解除された時に，タスク切換えが起こるか起こらないかが変わります。具体的には，TASK1の方が優先度が高い場合にはタスク切換えが起こり，そうでない場合には，タスク切換えが起こりません。ここでは，この両方のケースをテストしたいものとします。

まず最初に，TASK1の方が優先度が高い場合をテストしましょう。テストには，ASP3カーネルに同梱されているテストプログラム用サービスを用いることにします（テストプログラム用サービスの詳細については，TOPPERS/ASP3カーネル ユーザーズマニュアルの10.1節を参照してください）。

まず，タスク2つを生成するシステムコンフィギュレーションファイルを作成します。

```test_wuptsk.cfg
INCLUDE("tecsgen.cfg");

#include "test_wuptsk.h"

CRE_TSK(TASK1, { TA_ACT, 1, task1, MID_PRIORITY, STACK_SIZE, NULL });
CRE_TSK(TASK2, { TA_ACT, 2, task2, LOW_PRIORITY, STACK_SIZE, NULL });
```

テストプログラム用サービスは，TECS（TOPPERS組込みコンポーネントシステム）を使って作成されていますので，tecsgen.cfgを```INCLUDE```する必要があります。上のファイルで```#include```しているtest_wuptsk.hは，以下の内容とします。

```test_wuptsk.h
#include <kernel.h>

#define MID_PRIORITY 9
#define LOW_PRIORITY 10

#define STACK_SIZE 4096

extern void task1(EXINF exinf);
extern void task2(EXINF exinf);
```

いよいよテストプログラムの本体ですが，TASK1が起床待ちになっている状態で，TASK2からTASK1をwup_tskすればよいわけですから，2つのタスクのコードの骨格は次のようになります。

```test_wuptsk.c
#include <kernel.h>
#include "syssvc/test_svc.h"
#include "kernel_cfg.h"
#include "test_wuptsk.h"

void
task1(EXINF exinf)
{
	slp_tsk();
}

void
task2(EXINF exinf)
{
	wup_tsk(TASK1);
}
```

ただし，これでは，テストが正しく実行されたかどうかがわかりません。そこで，プログラムがどういう順序で実行されたかを表示するために，チェックポイントを入れます。

```test_wuptsk.c
#include <kernel.h>
#include "syssvc/test_svc.h"
#include "kernel_cfg.h"
#include "test_wuptsk.h"

void
task1(EXINF exinf)
{
	check_point(1);
	slp_tsk();
	check_finish(3);
}

void
task2(EXINF exinf)
{
	check_point(2);
	wup_tsk(TASK1);
	check_assert(false);
}
```

3つのチェックポイントを入れましたが，これが数字の順で実行されることを確認すれば，TASK2が発行した```wup_tsk(TASK1)```により，TASK1に切り換わったことが確認できます。TASK1に切り換わったことで，TASK1が待ち解除されたことは間接的に確認できますが，念を入れるなら，TASK1の状態をref_tskで確認してもいいでしょう。3つめのチェックポイントが，```check_finish(3)```となっていますが，これは，これが最後のチェックポイントであることを示しており，これを実行するとテストプログラムは終了します。task2の最後の```check_assert(false)```は，ここが実行されないことを確認するために入れてあります。

以上のテストプログラムを，ARMプロセッサ向けにビルドして実行してみます。

まず，ASP3カーネルの配布ファイルを置いたディレクトリの下に，テストプログラム用のディレクトリを作り，そのディレクトリ中に上記の3つのファイルを置き，以下のビルド手順を実行します。

```
% ../configure.rb -T ct11mpcore_gcc -O "-DTOPPERS_USE_QEMU" -A test_wuptsk -C ../test/test_pf.cdl
Generating Makefile from ../sample/Makefile.
% make
…ここで長いビルドプロセスが走ります（省略）… 
```

ビルドに成功したら，できたプログラムを実行してみます。ここでは，ARMのバイナリコードを実行するために，[QEMU](https://ja.wikipedia.org/wiki/QEMU)を使用します。

```
% qemu-system-arm -M realview-eb-mpcore -semihosting -m 128M -nographic -kernel asp
TOPPERS/ASP3 Kernel Release 3.6.0 for ARM CT11MPCore (Dec 22 2020, 16:25:15)
Copyright (C) 2000-2003 by Embedded and Real-Time Systems Laboratory
                            Toyohashi Univ. of Technology, JAPAN
Copyright (C) 2004-2020 by Embedded and Real-Time Systems Laboratory
            Graduate School of Information Science, Nagoya Univ., JAPAN

no time event is processed in hrt interrupt.
Check point 1 passed.
Check point 2 passed.
Check point 3 passed.
All check points passed.
```

3つのチェックポイントを通過し，テストプログラムが正しく実行されたことがわかります。

## テストプログラムを拡張する

次に，TASK1の方が優先度が低い場合をテストするコードを，このプログラムに追加してみましょう。そのために，TASK1より優先度が高いTASK3を用意し，TASK1を起床待ち状態にしておいて，TASK3からTASK1をwup_tskした結果，TASK1が待ち解除されることを確認します。

まず，TASK3の生成する記述を追加します。

```test_wuptsk.cfgに追加
CRE_TSK(TASK3, { TA_NULL, 3, task3, HIGH_PRIORITY, STACK_SIZE, NULL });
```

```test_wuptsk.hに追加
#define HIGH_PRIORITY 8

extern void task3(EXINF exinf);
```

テストプログラムの本体ですが，こちらはさきほどのように単純にはいきません。TASK3からTASK1をwup_tskするには，TASK3を起動しなければなりませんが，TASK3を起動すると，TASK1よりも優先度が高いためにTASK3に切り換わってしまい，TASK1がslp_tskを呼べません。そこで一工夫して，TASK1がslp_tskを呼び出すと，TASK1より優先度の低いTASK2が実行されることを利用し，TASK2からTASK3を起動します。これで，優先度の高いTASK3から，TASK1をwup_tskすることができます。

つまり，処理の流れは次のようになります。

 (1) TASK1が```slp_tsk()```を発行 → TASK2に切り換わる
 (2) TASK2が```act_tsk(TASK3)```を発行 → TASK3に切り換わる
 (3) TASK3が```wup_tsk(TASK1)```を発行 → タスク切換えは起こらない
 (4) TASK3が```ext_tsk()```を発行 → TASK1に切り換わる
 (5) TASK1が実行されれば，テスト終了

ここで，(4)と(5)は，TASK1が待ち解除されたことを確認するための手順になります（上記の通り，ref_tskを用いて待ち解除されたことを確認する方法も考えられます）。

この一連の手順を，さきほどのテストプログラムに追加してみます。

```test_wuptsk.c
#include <kernel.h>
#include "syssvc/test_svc.h"
#include "kernel_cfg.h"
#include "test_wuptsk.h"

void
task1(EXINF exinf)
{
	check_point(1);
	slp_tsk();
	check_point(3);
	slp_tsk();
	check_finish(7);
}

void
task2(EXINF exinf)
{
	check_point(2);
	wup_tsk(TASK1);
	check_point(4);
	act_tsk(TASK3);
	check_assert(false);
}

void
task3(EXINF exinf)
{
	check_point(5);
	wup_tsk(TASK1);
	check_point(6);
	ext_tsk();
}
```

さて，このテストプログラムの動作がすぐに理解できるでしょうか？ もし理解できる方がいたら，リアルタイムOSを徹底的に使い込んでいる方だと思います（私もすぐには理解できません）。このテストプログラムをデバッグしろとか，変更しろと言われると，かなり苦痛なのは間違いないでしょう。

ちなみに，これをビルドして実行すると，7つのチェックポイントが順に実行されることを確認できます。

## テストプログラムの作成・保守を容易に！

さきほど書いた通り，上のテストプログラムは，作成するのも，理解するのも，デバッグするのも，変更するのも大変です。では，どうしたら容易にテストプログラムを作成・保守できるようになるでしょうか？

ヒントは，上に書いた「処理の流れ」にあります。例えば，テストプログラムが下のように書けたら，楽になるんじゃないでしょうか。

 - TASK1が実行される
  + チェックポイント1
  + slp_tsk()を発行
 - TASK2が実行される
  + チェックポイント2
  + wup_tsk(TASK1)を発行
 - TASK1が実行される
  + チェックポイント3
  + slp_tsk()を発行
 - TASK2が実行される
  + チェックポイント4
  + act_tsk(TASK3)を発行
 - TASK3が実行される
  + チェックポイント5
  + wup_tsk(TASK1)を発行
  + チェックポイント6
  + ext_tsk()を発行
 - TASK1が実行される
  + チェックポイント7
  + テスト終了

これを実現してくれるのが，テストコード生成ツール（utils/gentest.rb）です。テストコード生成ツールを使用する場合，上のような記述をコメントとして記述します。具体的には，以下のように記述します。

```test_wuptsk.c
/*
 *	== TASK1（優先度：中）==
 *	1:	slp_tsk()						...［NGKI1272］
 *	== TASK2（優先度：低）==
 *	2:	wup_tsk(TASK1)					...［NGKI1271］
 *	== TASK1（続き）==
 *	3:	slp_tsk()						...［NGKI1272］
 *	== TASK2（続き）==
 *	4:	act_tsk(TASK3)
 *	== TASK3（優先度：高）==
 *	5:	wup_tsk(TASK1)					...［NGKI1271］
 *	6:	ext_tsk()
 *	== TASK1（続き）==
 *	7:	END
 */

#include <kernel.h>
#include "syssvc/test_svc.h"
#include "kernel_cfg.h"
#include "test_wuptsk.h"

/* DO NOT DELETE THIS LINE -- gentest depends on it. */
```


コメントの後には，インクルード記述と，おまじないの1行を書きます。なお，```...［NGKI1272］```という記述はコメントです。

このファイルを，テストコード生成ツールにかけます。

```
../utils/gentest.rb test_wuptsk.c
```

この結果，test_wuptsk.cは以下のように書き換わります（元のファイルは，test_wuptsk.c.bakに残ります）。

```test_wuptsk.c
/*
 *	== TASK1（優先度：中）==
 *	1:	slp_tsk()						...［NGKI1272］
 *	== TASK2（優先度：低）==
 *	2:	wup_tsk(TASK1)					...［NGKI1271］
 *	== TASK1（続き）==
 *	3:	slp_tsk()						...［NGKI1272］
 *	== TASK2（続き）==
 *	4:	act_tsk(TASK3)
 *	== TASK3（優先度：高）==
 *	5:	wup_tsk(TASK1)					...［NGKI1271］
 *	6:	ext_tsk()
 *	== TASK1（続き）==
 *	7:	END
 */

#include <kernel.h>
#include "syssvc/test_svc.h"
#include "kernel_cfg.h"
#include "test_wuptsk.h"

/* DO NOT DELETE THIS LINE -- gentest depends on it. */

void
task1(EXINF exinf)
{
	ER_UINT	ercd;

	test_start(__FILE__);

	check_point(1);
	ercd = slp_tsk();
	check_ercd(ercd, E_OK);

	check_point(3);
	ercd = slp_tsk();
	check_ercd(ercd, E_OK);

	check_finish(7);
	check_assert(false);
}

void
task2(EXINF exinf)
{
	ER_UINT	ercd;

	check_point(2);
	ercd = wup_tsk(TASK1);
	check_ercd(ercd, E_OK);

	check_point(4);
	ercd = act_tsk(TASK3);
	check_ercd(ercd, E_OK);

	check_assert(false);
}

void
task3(EXINF exinf)
{
	ER_UINT	ercd;

	check_point(5);
	ercd = wup_tsk(TASK1);
	check_ercd(ercd, E_OK);

	check_point(6);
	ercd = ext_tsk();
	check_ercd(ercd, E_OK);

	check_assert(false);
}
```

おまじないの1行の後に，3つのタスクのコードが生成されていることがわかります。また，生成されたコードでは，サービスコールの戻り値がE_OKであることも確認していますので，NGKI1272についても，これでテストできていることになります。なお，E_OK以外が返ることを確認する場合には，

```
 *	1: slp_tsk() -> E_TMOUT
```

のように，テストシーケンス中に期待される戻り値を記述すればOKです。

これにより，テストプログラムの作成・保守が容易になっていると考えていますが，いかがでしょうか？

ちなみに，チェックポイントの番号が1からの連番になっていない場合，テストコード生成ツールが1からの連番に振り直してくれます。この機能があるので，途中にチェックポイントを追加するのも簡単にできます。

## まだまだ高機能なテストコード生成ツール

以上で紹介したのは，テストコード生成ツールの基本的な機能のみです。実際のテストコード生成ツールは，タスクだけでなく，割込みハンドラや各種のタイムイベントハンドラを記述することもできますし，使用する変数定義の自動生成にも対応しています（上の例でも，ercdの定義は生成していました）。さらに，マルチプロセッサにも対応しています。

ただし，現状では，テストコード生成ツールの使い方のドキュメントはありません！ 使う方は，見様見真似で使っていただくことになります。

なお，このように高機能なテストコード生成ツールですが，Rubyで記述されており，コメントや空行も入れて531行しかありません！ Rubyの強力な記述力のおかげです。

## 最後に

各カーネルのtestディレクトリに含まれるテストプログラムは，カーネルの網羅的なテストにはなっていません。カーネルに新しい機能を追加した場合には，原則として，それをテストするプログラムを作成して追加していますが，古くに実装された機能に対しては，テストプログラムが用意できていないのが現状です。

なお，TOPPERSプロジェクトでは，カーネルの網羅的なテストを行うものとして，[TTSP（TOPPERS Test Suite Package）](https://www.toppers.jp/ttsp.html)を開発しています。TTSPでは，ここで紹介したテストコード生成ツールより一歩進めて，テストの前条件（例えば「TASK1が起床待ち状態の時」）を記述すれば，カーネルをその状態にするためのサービスコールの列を自動生成してくれます。現時点では，TOPPERS第3世代カーネル向けのTTSPは公開していませんが，開発は進めており，将来的には公開する計画です。