M5Stack PlatformIO で GoogleTest を使用する
概要
M5Stack でのアプリケーション作成環境として、 ArduinoIDE と Visual Studio Code + PlatformIO があることはご存知かと思います。
PlatformIO にはユニットテストの為の機構が存在し、従来は Unity[1] というテストフレームワークがサポートされていました。また AUnit 等独自のユニットテストを提供する方々もいらっしゃいました。しかし PlatformIO の更新に伴い、6.0.0 より GoogleTest と DocTest の使用が可能になったようです。(更新履歴)
私は今まで PlatformIO 上のテスト機構を使用せず、テスト用のプロジェクトを作ってそこで GoogleTest を動かしていました。これだと Jenkins 等との連携がすんなりできませんし、自動化、省力化、効率化の観点からも不十分でした。
GooglTest が PlatformIO でテストフレームワークとして利用できるようになった事で、その辺りが解消できるようになったのではと思います。
この記事では GoogleTest を PlatformIO 上でテストフレームワークとして使用した際の知見を書き残したものです。
GoogleTest 以外のテストフレームワークを使う際にも参考になる点もあるかと思います。
Test Runner / テストランナー
Local Test Runner / ローカルテストランナー
ローカルのホストマシンかターゲットデバイス(今回は M5Stack ) でのテストを実行できます。
CLI からだと pio test コマンドによって実行されます。
PROJECT TASKS からだと Advanced - Test によって実行されます。
Remote Test Runner / リモートテストランナー
リモートマシン上に接続されたターゲットデバイスでのテストを実行できます。リモート開発が前提となる(= PkatformIO のサーバー経由) ようで、おそらく PlatfromIO の会員登録を経て諸々準備が必要だと思われます。
今回はこの辺りを試していないので、申し訳ありませんが詳細は割愛します。
こちらのページが参考になるかと思います。PlatformIOのremote機能を使ってみた
CLI からだと pio remote test コマンドによって実行されます。
PROJECT TASKS からだと Remote Devlopment - Remote Test によって実行されます。
Test Type / テストの種類
Native / ネイティブ (ローカルホストマシンでのテスト実行)
実デバイスではなく、ローカルマシン上でのテストを行います。ハード依存しないもののテストや、モックを利用しての擬似テストを行います。但しデフォルトの環境ではネイティブ環境はインストールされていないので、自分で環境を整備する必要があります。
Embedded / エンベデッド (ターゲットデバイス上でのテスト実行)
ローカルのホストマシンに接続した実デバイス(今回は M5Stack )へ、転送しその上で動作させてテストを行います。大抵の場合はこのテストを行うことになるでしょう。ネイティブと違って実機上で動作させるので特別な場合を除いてモックなどを用意しなくても良いはずです。
通常のビルドとアップロードと同様に、シリアル接続されたデバイスへバイナリを転送し実行します。シリアル出力は収集され、テストの結果として整形、出力されます。
Hybrid / ハイブリッド (ローカル / ターゲット両方でのテスト実行)
ホストマシンとターゲットデバイスで動作するテストです。
準備
-
test ディレクトリ
自分のプロジェクト、またはライブラリのディレクトリに test ディレクトリが必要です。存在しない場合は作成してください。
test ディレクトリ以下のファイルがテスト実行時にビルドされ使用されます。 -
platform.ini
プロジェクトの場合は既に存在していますが、ライブラリの場合は存在しない場合があるかと思います。
その場合はライブラリディレクトリにテストの為の platform.ini を作成しましょう。
- プロジェクトの場合の構成例
your_project/
your_project/platform.ini
your_project/src/
your_project/test/
- ライブラリの場合の構成例
your_library/
your_library/library.json
your_library/library.properties
your_library/platform.ini
your_library/examples/
your_library/src/
your_library/test/
- (必要ならば) ネイティブ環境の整備
最低限 GCC (g++) が動作すればよいようです。
Mac では Clang が g++ にエイリアスされていますので、GCC でコンパイルを行いたい場合は必要なバージョンの GCC をインストールし、 g++ を置き換えましょう。併用したい場合はシンボリックリンクの差し替えでなく、 alias で切り替えてもいいかもしれません。
Mac 特有の問題は本題ではないので割愛します。
test ディレクトリ階層
test ディレトクリ以下には所定のルールが適用されます。
- test_dir 直下と後述のオプション test_filter, test_ignoe によるフィルタリングでマッチしたディレクトリは CPPPATH に自動で追加されます。 (#include "foo.h" の対象となる)
- test_dir 以下には任意のディレクトリを作ることで別々のテストアプリケーションを作成できます。
但しテストアプリケーションは test_ で開始される名前でなければなりません。
オプション
platformio.ini
test_dir
ユニットテストにおいて探索されるディレクトリです。テストケース等が置かれることが想定れています。default では test です。
環境変数 PLATFORMIO_TEST_DIR にて設定も可能です。
test_framework
使用するテストフレームワークを指定します。今回は GoogleTest を使用するので
test_framework = googletest
と指定することになります。
test_filter
test_dir からの相対パスが、指定されたパターンとマッチするディレクトリ内のテストのみが処理されます。
test_ignore
test_dir からの相対パスが、指定されたパターンとマッチするディレクトリ内のテストが無視されます。
test_port
Embedded なテストの場合、指定されたポートが Test Runner とターゲットデバイス間の通信経路として使用されます。記述は upload_port と同様です。
無指定の場合は upload 同様に自動で検出しようとします。検出に時間がかかりすぎてテスト結果の出力をとりこぼしてしまう場合は指定した方が良いでしょう。
test_speed
test_port でのボーレートを指定します。 記述は monitor_speed と同様です。
test_build_src
yes が指定された場合、src_dir もテストでのビルドの対象となります。
test_testing_command
テストケースを実行するテストコマンドです。
追加のプログラム引数が必要な場合、これを記述することで渡す事ができます。(たぶん)ネイティブ以外では意味がないかと思われます。
pio test オプション
-e, --environment TEXT
テストを実行する platform.ini に書かれた [env:foo] を指定します。複数の指定も可能です。
無指定の場合は GUI での default - Advanced - Test と同様の動作となります。 その場合 [platformio] default_envs = foo,bar... の記述があれば、書かれている env のみが実行されるでしょう。
pio test -e foo -e bar
pio test --environment foo --environment bar
-f, --filter PATTERN
platform.ini での test_filter の指定と同様です。
-i, --ignore PATTERN
platform.ini での test_ignore の指定と同様です。
--upload-port TEXT
platform.ini での upload_port の指定と同様です。
--test-port TEXT
platform.ini での test_port の指定と同様です。
-d, --project-dir DIRECTORY
プロジェクトディレクトリのパスを指定します。デフォルトでは作業ディレクトリと同等です。
-c, --project-conf FILE
指定したファイルを platformio.ini と見做して処理します。
--without-building
ビルドしません。
--without-uploading
アップロードしません。
--without-testing
テストしません。
--no-reset
ターゲットデバイスのリセットを抑制します。この場合アップロード後に手動でターゲットデバイスのリセットしなければなりません。
ソフトウェアリセットがサポートされていないターゲットの場合は、テストの開始関数の実行前に 2 秒以上のディレイを入れて、ホストとターゲット間の通信が確立するのを待つ必要があります。
--monitor-rts
シリアル通信の RTS(Request to Send) の初期状態を指定(0 or 1)します。デフォルトは 1 です。
--monitor-dtr
シリアル通信の DTR(Data Terminal Ready) の初期状態を指定(0 or 1)します。デフォルトは 1 です。
-a, --program-arg TEXT
テストプログラムに与える追加の引数を指定します。
GoogleTest のテストケースのフィルタ指定はここで行えます。
pio test --program-arg "--gtest_filter=FooTest.*-FooTest.Bar"
--list-tests
テストを実行せずに、テストスイートを出力します。
--json-output-path --junit-output-path の指定と合わせる事で利用できるテストスイートを取得できます。
--json-output-path PATH
テストレポートを JSON 形式で指定のパスに出力します。ディレクトリを指定した場合は自動で命名されたファイルが出力されます。
--junit-output-path PATH
テストレポートを JUNIT XML 形式で指定のパスに出力します。ディレクトリを指定した場合は自動で命名されたファイルが出力されます。
Jenkins との連携はこのファイルで可能となりますね。
-v, --verbose
過程の出力レベルを指定します。
Level | Option | Description |
---|---|---|
1 | -v | テストフレームワークからの出力のみ |
2 | -vv | ビルドとアップロードの出力も含む(通常のビルド相当) |
3 | -vvv | さらに追加された出力 |
-h, --help
ヘルプメッセージが出力されます。
実例
goblib (Native テスト)
私のライブラリ、goblib (ハード依存無し) について、Native なテストを実行してみます。
platformio.ini の作成
platformio.ini が存在しないので作成します。
私の環境ではコンパイラのデフォルトは c++11 なので、 14,17用のセッティングを加えます。
また test_build_src = true とする事で、 ./src 以下のファイルをビルドの対象とします。
test_build_src については以下を参照してください。推奨されない方法ではありますが今回の場合は適していると思います。
[platformio]
default_envs = test_11, test_14, test_17
[env]
build_flags = -DGOBLIB_ENABLE_PROFILE -DGOBLIB_TEST_STUB -DGOBLIB_CPP_VERSION_DETECTION -Wall -Wextra -Wreturn-stack-address
test_framework = googletest
test_build_src = true
platform = native
; compiler default is -std=gnu++11 in my environment.
[env:test_11]
build_type = release
[env:test_14]
build_type = release
build_unflags = -std=gnu++11
build_flags = ${env.build_flags}
-std=gnu++14
[env:test_17]
build_type = release
build_unflags = -std=gnu++11
build_flags = ${env.build_flags}
-std=gnu++17
build_flags 等、ビルド向けの記述も有効です。 適切な設定を記述しましょう。
今回は c++11,14,17,20 での設定を列挙し、 default では 11,14,17 のテストが行われるようにしました。
main.cpp
ライブラリがテスト対象なので test_dir/main.cpp を用意します。 ./src には main 関数がありませんのでここに記述し、プログラムエントリとします。
#include <gtest/gtest.h>
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
(void)RUN_ALL_TESTS();
// Always return zero-code and allow PlatformIO to parse results
return 0;
}
テストケース
今回は goblib での native テストのみ(フィルタリングしない)ですので、 test_dir 直下にテストケースを置きます。
#include "gtest/gtest.h"
#include <gob_random.hpp>
using namespace goblib;
TEST(Random, Basic)
{
constexpr std::uint32_t seed = 52;
EXPECT_TRUE(goblib::template_helper::is_rng<std::mt19937>::value);
EXPECT_FALSE(goblib::template_helper::is_rng<std::vector<int>>::value);
EXPECT_TRUE(goblib::template_helper::is_rng<std::ranlux24>::value);
std::ranlux24 lux24(seed);
std::uniform_int_distribution<> d_int(-100, +100);
std::uniform_real_distribution<> d_real(-123.f, +123.f);
Rng<std::ranlux24> rng_lux24(seed); // using same seed.
EXPECT_EQ(d_int(lux24), rng_lux24(-100,100));
EXPECT_FLOAT_EQ(d_real(lux24), rng_lux24(-123.f, 123.f));
EXPECT_EQ(d_int(lux24), rng_lux24(-100,100));
EXPECT_EQ(d_int(lux24), rng_lux24(-100,100));
EXPECT_FLOAT_EQ(d_real(lux24), rng_lux24(-123.f, 123.f));
EXPECT_FLOAT_EQ(d_real(lux24), rng_lux24(-123.f, 123.f));
EXPECT_EQ(lux24(), rng_lux24());
lux24.discard(100);
rng_lux24.discard(99);
EXPECT_NE(lux24(), rng_lux24());
rng_lux24.discard(1);
EXPECT_EQ(d_int(lux24), rng_lux24(-100,100));
}
テスト実行
PIO Home - Open で 当該 platformio.ini のある場所を開きましょう。
GUI から 実行したい env の Advanced - Test、または CLI から pio test -e your_env でビルド後実行され、結果が表示されます。
GUI からの場合はビルド過程の出力が抑制されていますので、 CLI で -v, -vv, -vvv による段階的なビルド出力を見た方が良いかも知れません。
(略)
Testing...
[==========] Running 55 tests from 17 test suites.
[----------] Global test environment set-up.
[----------] 1 test from App
[ RUN ] App.Basic
[ OK ] App.Basic (2140 ms)
[----------] 1 test from App (2140 ms total)
(略)
[----------] Global test environment tear-down
[==========] 55 tests from 17 test suites ran. (3217 ms total)
[ PASSED ] 55 tests.
------------------------------------------------- test_11:* [PASSED] Took 4.44 seconds -------------------------------------------------
================================================================ SUMMARY ================================================================
Environment Test Status Duration
------------- ------ -------- ------------
test_11 * PASSED 00:00:04.441
goblib_m5s (Embedded テスト)
私のライブラリ、goblib_m5s (M5Stack 依存 について、Embedded なテストを実行してみます。
platformio.ini の作成
platformio.ini が存在しないので作成します。今回は対象の M5Stack に接続する必要があるのでそのための設定も記述します。
対象ハードが複数ある場合はそれぞれの設定を整えます。
[env] ; 共通設定
platform = espressif32@3.4.0
framework = arduino ; native ではなく対象のハードのもの
board_build.flash_mode = qio
board_build.f_flash = 80000000L
lib_deps = m5stack/M5Stack@0.4.0
greiman/SdFat
lovyan03/LovyanGFX
google/googletest
https://github.com/GOB52/goblib
build_flags = -DGOBLIB_ENABLE_PROFILE -DGOBLIB_TEST_STUB -DGOBLIB_CPP_VERSION_DETECTION -DGOBLIB_ENABLE_PRAGMA_MESSAGE -Wall -Wextra -Wreturn-local-addr
upload_speed = 921600
test_framework = googletest
test_speed = 115200
test_build_src = true
; core 用テスト
[env:core_test_11]
board = m5stack-core-esp32
build_type = release
upload_port=; core 接続先
test_port=;core 接続先
; core2 用テスト
[env:core2_test_11]
board = m5stack-core2
build_type = release
upload_port=; core2 接続先
test_port=;core2 接続先
テストケース、実行は goblib と同様ですが、今回は M5Stack を接続した状態で実行します。ビルド後転送され、実機上でのテストが行われ結果が出力されます。
(付) goblib / goblib_m5s のテストの共用化
実機上で goblib と goblib_m5s のテストケースを両方実行させたかったので色々試行してみました。
要件
- goblib 上のテストでは native テストを行う。
- goblib_m5s 上のテストでは embededd テストとして、 goblib とと goblib_m5s のテストを行う。
- 共通するテストケースは、同一ソースとする。
テストの独立化と submodule
当初は goblib_m5s に goblib/test 以下を sparse checkout する事で実現しようとしたのですが、利用するユーザーがいちいち .git に対する編集を行わなければならないのが微妙です。そこで test_dir を独立したリポジトリにして、 submodule として各々取り込む事にしました。(git clone --recursive / gut clone, git submodule update --init --recursive 等で取得可能)
test_filter / test_igone によるフィルタリングを利用する事でそれぞれ必要なテストケースのみをビルド、実行させます。
goblib_test
goblilb/test goblib_m5s/test にて submodule となります。
リポジトリ goblib_test の構成概要は以下の様な感じになります。
goblib_test/main.cpp <= goblib native 用 main
/test_foo.cpp <= goblib 用テストケース
/embedeed/test_m5s/test_hoge.cpp <= goblib_m5s 用テストケース
/dummy/test_dummy <= ダミー
./main.cpp
#include <gtest/gtest.h>
__attribute__((weak)) int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
(void)RUN_ALL_TESTS();
// Always return zero-code and allow PlatformIO to parse results
return 0;
}
goblib / goblib_m5s 共にビルドされるファイルですが、 main 関数を weak symbol 化します。
goblib(native) では他に main 関数が存在しないのでここが使用され、 goblib_m5s(embedded) では framework に main 関数が含まれているのでそちらが使用されます。
./dummy/test_dummy
test ディレクトリ階層 で説明しましたが、フィルタリングにマッチしたものがないとテストが動作しないので、ダミーを用意します。
native ではこれをマッチさせる事で test_dir 直下のファイルをビルド対象とします。
./embedded/test_m5s
M5Stack 用のテストケースをここに置きます。
goblib/platgorm.ini
test_filter=dummy/test_dummy
test_dummy (空)がマッチして test_dir 直下のファイルのみが使用されます。
goblib_m5s/platgorm.ini
test_filter=embedded/test_m5s
embedded/test_m5s がマッチして test_dir 直下と embedded/test_m5s 以下のファイルのみが使用されます。
リポジトリ
以上を踏まえたものが以下のものになります。
-
ゲームエンジンと同名ですが、違うものです。 ↩︎
Discussion