1. はじめに
前回は Twister(Test Runner)を hello_world で試行し、ビルドが正常に通るかどうか、動作時の出力を正規表現で判定してテストが通るかどうかの駆動部分を紹介しました。
今回はテスト内容そのものを書く ZTest を紹介し、Twister と組み合わせて関数単位で検証する機能を紹介いたします。
公式のページは以下にあります。
2. ZTest と Twister の関係
前回のおさらいとなりますが、Twister と ZTest の関係性について確認しておきます。
- Twister は、ZTest を含む、ビルドやテストを駆動させ、その結果を判定・レポートする
- ZTest は、入力・期待値・アサーションが規定通りになるかのチェックをするための機能・フレームワーク
そのため、基本的には Twister との連携を行う事を基本としており、実運用としてはさらに Twister を Github Actions から Twister を呼ぶような連携が想定されます。
3. eventfd サンプルにテストを追加する
Zephyr 公式のツリーには数多くの sample コードがあり、今回はその中の eventfd を対象にテストコードを追加していきます。
このサンプルは eventfd() で作ったカウンタに値を書き込み、まとめて読み出すものです。既存の sample.yaml では出力された文字列を console harness で判定しています。
変更するファイルは以下のとおりです。
samples/posix/eventfd/
├── CMakeLists.txt # 変更
├── sample.yaml # 変更
└── src/
├── main.c # 変更
└── test_eventfd.c # 追加
3.1. ZTest を有効にする手順
テストコードは CONFIG_ZTEST=y が設定されていると機能が使えるようになります。
target_sources(app PRIVATE src/main.c)
+target_sources_ifdef(CONFIG_ZTEST app PRIVATE src/test_eventfd.c)
また、ZTest は自身の main() を持っているため、CONFIG_ZTEST が有効なときはプリプロセッサで main() を除外するようにしておきます。
+#ifndef CONFIG_ZTEST
int main(int argc, char *argv[])
{
argv = input_argv;
:
:
return 0;
}
+#endif /* !CONFIG_ZTEST */
3.2. テストコードの書き方
writer() / reader() と efd などのグローバル変数は static ではないので、テストから extern で参照できます。
下記は eventfd/src/main.c に実装されている writer() と reader() に対するテストコードです。
#include <errno.h>
#include <stdint.h>
#include <sys/eventfd.h>
#include <unistd.h>
#include <zephyr/ztest.h>
extern int efd;
extern int g_argc;
extern char **g_argv;
void writer(void);
void reader(void);
static char *test_argv[] = {"argv0", "1", "2", "3", "4"};
static void eventfd_before(void *fixture)
{
ARG_UNUSED(fixture);
efd = eventfd(0, EFD_NONBLOCK);
zassert_not_equal(efd, -1, "eventfd failed: %d", errno);
g_argc = ARRAY_SIZE(test_argv);
g_argv = test_argv;
}
static void eventfd_after(void *fixture)
{
ARG_UNUSED(fixture);
zassert_ok(close(efd));
}
ZTEST_SUITE(eventfd, NULL, NULL, eventfd_before, eventfd_after, NULL);
ZTEST(eventfd, test_writer_accumulates)
{
uint64_t u;
writer();
zassert_equal(read(efd, &u, sizeof(u)), sizeof(u));
zassert_equal(u, 10);
}
ZTEST(eventfd, test_writer_single_value)
{
uint64_t u;
g_argc = 2;
writer();
zassert_equal(read(efd, &u, sizeof(u)), sizeof(u));
zassert_equal(u, 1);
}
ZTEST(eventfd, test_reader_drains_counter)
{
uint64_t u = 42;
zassert_equal(write(efd, &u, sizeof(u)), sizeof(u));
reader();
zassert_equal(read(efd, &u, sizeof(u)), -1);
zassert_equal(errno, EAGAIN);
}
ZTEST(eventfd, test_read_empty_would_block)
{
uint64_t u;
zassert_equal(read(efd, &u, sizeof(u)), -1);
zassert_equal(errno, EAGAIN);
}
| テストケース | テスト内容 |
|---|---|
| test_writer_accumulates |
writer() の後に read() すると 1+2+3+4 = 10 が得られる |
| test_writer_single_value | 1 回だけ書き込むと値は 1 |
| test_reader_drains_counter |
reader() の後はカウンタが空になり、次の read() が EAGAIN になる |
| test_read_empty_would_block | 空の eventfd を読むと EAGAIN になる |
3.3 ZTEST_SUITE()
ZTEST_SUITE() はテストスイート、つまりテストケースをまとめるグループを定義して登録するマクロです。
#define ZTEST_SUITE(SUITE_NAME, PREDICATE, setup_fn, before_fn, after_fn, teardown_fn)
| 引数 | 説明 | 今回の指定 |
|---|---|---|
| SUITE_NAME | スイートの名前。文字列ではなくそのまま記述する | eventfd |
| PREDICATE | スイートを実行するかどうかを判定する関数 | NULL |
| setup_fn | スイートの開始時に 1 回呼ばれる関数 | NULL |
| before_fn | 各テストケースの実行前に呼ばれる関数 | eventfd_before |
| after_fn | 各テストケースの実行後に呼ばれる関数 | eventfd_after |
| teardown_fn | スイートの終了時に 1 回呼ばれる関数 | NULL |
使わない関数には NULL を指定します。すべて NULL にして ZTEST_SUITE(eventfd, NULL, NULL, NULL, NULL, NULL); と書くこともできます。
今回は before_fn と after_fn を使い、テストケースごとに eventfd を作り直しています。こうすることで、前のケースで書き込んだカウンタの値が次のケースに残りません。
setup_fn は void * を返し、その戻り値が before_fn / after_fn / teardown_fn の引数 fixture に渡されます。今回は setup_fn が NULL で fixture を使わないので、ARG_UNUSED() で未使用であることを示しています。
3.4 ZTEST()
ZTEST() はテストケースを 1 つ定義し、指定したスイートに登録するマクロです。
#define ZTEST(suite, fn) Z_ZTEST(suite, fn, 0)
| 引数 | 説明 | 今回の例 |
|---|---|---|
| suite | 登録先のスイートの名前。ZTEST_SUITE() の SUITE_NAME と揃える |
eventfd |
| fn | テストケースの名前 | test_writer_accumulates |
ZTEST() の直後に { } で関数の本体を続けて書きます。引数も戻り値もない関数として展開され、実際の関数名は スイート名_ケース名 になります。
ZTEST() で定義した関数は ZTEST_SUITE() と同様に自動で登録されるので、main() や一覧表などから呼び出す必要はありません。ZTest が提供する main() が、登録されたスイートとケースを順に実行します。また、スイートの定義と同じファイルに書く必要もないので、ケースが増えてきたらファイルを分けることもできます。
テストケース内では、zassert_equal() などのアサーションで結果を検証しています。
3.5. アサーションの種類
ZTest で利用できるアサーションは、大きく分けると以下のように分類されます。
| 種類 | 失敗したとき | 用途 |
|---|---|---|
| zassert_* | そのケースを FAIL として打ち切り | 通常の検証 |
| zassume_* | そのケースを SKIP | 前提条件が満たされないとき |
| zexpect_* | 記録して続行、最後に FAIL | 複数項目をまとめて確認 |
詳細は以下を参照してください。
3.6. sample.yaml
tests: に ZTest 用のシナリオを追加します。既存のシナリオはそのままです。
- "Read 10 \\(0xa\\) from efd"
- "Finished"
+ sample.posix.eventfd.ztest:
+ min_ram: 32
+ tags: posix
+ extra_configs:
+ - CONFIG_ZTEST=y
harness: を指定しない場合の既定値は test で、ZTest の出力をテストケース単位で判定します。
89 "harness": {"type": "str", "default": "test"},
4. 実行と結果の確認
-p でプラットフォームを native_sim に絞って実行します。native_sim は Linux アプリケーションとして動作するので、実機や QEMU を用意しなくても試すことができます。
west twister -T zephyr/samples/posix/eventfd/ -p native_sim
INFO - 2 test scenarios (2 configurations) selected, 0 configurations filtered (0 by static filter, 0 at runtime).
INFO - 2 of 2 executed test configurations passed (100.00%), 0 built (not run), 0 failed, 0 errored, with no warnings in 30.96 seconds.
INFO - 5 of 5 executed test cases passed (100.00%) on 1 out of total 1473 platforms (0.07%).
前回の hello_world ではシナリオ 1 つがテストケース 1 件でしたが、今回は ZTEST() の数だけ数えられ、console の 1 件と合わせて 5 件になっています。
各ケースの結果は、twister-out 以下の handler.log で確認できます。
*** Booting Zephyr OS build v4.4.0 ***
Running TESTSUITE eventfd
===================================================================
START - test_read_empty_would_block
PASS - test_read_empty_would_block in 0.000 seconds
===================================================================
START - test_reader_drains_counter
About to read
Read 42 (0x2a) from efd
PASS - test_reader_drains_counter in 1.010 seconds
===================================================================
START - test_writer_accumulates
Writing 1 to efd
Writing 2 to efd
Writing 3 to efd
Writing 4 to efd
Completed write loop
PASS - test_writer_accumulates in 0.000 seconds
===================================================================
START - test_writer_single_value
Writing 1 to efd
Completed write loop
PASS - test_writer_single_value in 0.000 seconds
===================================================================
TESTSUITE eventfd succeeded
------ TESTSUITE SUMMARY START ------
SUITE PASS - 100.00% [eventfd]: pass = 4, fail = 0, skip = 0, total = 4 duration = 1.010 seconds
- PASS - [eventfd.test_read_empty_would_block] duration = 0.000 seconds
- PASS - [eventfd.test_reader_drains_counter] duration = 1.010 seconds
- PASS - [eventfd.test_writer_accumulates] duration = 0.000 seconds
- PASS - [eventfd.test_writer_single_value] duration = 0.000 seconds
------ TESTSUITE SUMMARY END ------
===================================================================
RunID: 42643d2c40e2f8c4eac4405a6a50abe9
PROJECT EXECUTION SUCCESSFUL
twister.json では test_ が外れ、sample.posix.eventfd.ztest.eventfd.writer_accumulates のように シナリオ名.スイート名.ケース名 で記録されます。
4.1. わざとバグを入れてみる
writer() に、書き込む値が 1 ずつずれるバグを入れて実行してみます。
- u = strtoull(g_argv[j], NULL, 0);
+ u = strtoull(g_argv[j], NULL, 0) + 1;
INFO - 0 of 1 executed test configurations passed (0.00%), 0 built (not run), 1 failed, 0 errored, with no warnings in 21.96 seconds.
INFO - 2 of 4 executed test cases passed (50.00%), 2 failed on 1 out of total 1473 platforms (0.07%).
Assertion failed at CMAKE_SOURCE_DIR/src/test_eventfd.c:50: eventfd_test_writer_accumulates: u not equal to 10
FAIL - test_writer_accumulates in 0.000 seconds
Assertion failed at CMAKE_SOURCE_DIR/src/test_eventfd.c:62: eventfd_test_writer_single_value: u not equal to 1
FAIL - test_writer_single_value in 0.000 seconds
writer() に関係する 2 ケースだけが FAIL になり、テストコードのファイル名と行番号から失敗した箇所がわかります。
5. まとめ
前回の Twister と今回の ZTest を組み合わせることで、ビルド〜関数単位のテストを一連の動作として実行できるようになりました。
2026.10.28 には Zephyr 4.5.0 もリリースされており、面白そうなキーワードも並んでいますね。
ここまで基本的な機能について紹介してきましたが、今後はこういった新しい機能などにも触れていけたらと考えております。