💯

M5Stack PlatformIO で GoogleTest を使用する

2022/09/04に公開

概要

M5Stack でのアプリケーション作成環境として、 ArduinoIDEVisual Studio Code + PlatformIO があることはご存知かと思います。

PlatformIO にはユニットテストの為の機構が存在し、従来は Unity[1] というテストフレームワークがサポートされていました。また AUnit 等独自のユニットテストを提供する方々もいらっしゃいました。しかし PlatformIO の更新に伴い、6.0.0 より GoogleTest と DocTest の使用が可能になったようです。(更新履歴)

私は今まで PlatformIO 上のテスト機構を使用せず、テスト用のプロジェクトを作ってそこで GoogleTest を動かしていました。これだと Jenkins 等との連携がすんなりできませんし、自動化、省力化、効率化の観点からも不十分でした。
GooglTest が PlatformIO でテストフレームワークとして利用できるようになった事で、その辺りが解消できるようになったのではと思います。

この記事では GoogleTest を PlatformIO 上でテストフレームワークとして使用した際の知見を書き残したものです。
GoogleTest 以外のテストフレームワークを使う際にも参考になる点もあるかと思います。

Test Runner / テストランナー

https://docs.platformio.org/en/latest/advanced/unit-testing/runner.html#local-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 / ハイブリッド (ローカル / ターゲット両方でのテスト実行)

ホストマシンとターゲットデバイスで動作するテストです。

準備

  1. test ディレクトリ
    自分のプロジェクト、またはライブラリのディレクトリに test ディレクトリが必要です。存在しない場合は作成してください。
    test ディレクトリ以下のファイルがテスト実行時にビルドされ使用されます。

  2. 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/
  1. (必要ならば) ネイティブ環境の整備

https://docs.platformio.org/en/latest/platforms/native.html#platform-native
最低限 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_ で開始される名前でなければなりません。

https://docs.platformio.org/en/latest/advanced/unit-testing/structure/hierarchy.html#unit-testing-test-hierarchy

オプション

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

テストケースを実行するテストコマンドです。
追加のプログラム引数が必要な場合、これを記述することで渡す事ができます。(たぶん)ネイティブ以外では意味がないかと思われます。

https://docs.platformio.org/en/latest/projectconf/section_env_test.html

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

ヘルプメッセージが出力されます。

https://docs.platformio.org/en/latest/core/userguide/cmd_test.html

実例

goblib (Native テスト)

私のライブラリ、goblib (ハード依存無し) について、Native なテストを実行してみます。

platformio.ini の作成

platformio.ini が存在しないので作成します。
私の環境ではコンパイラのデフォルトは c++11 なので、 14,17用のセッティングを加えます。
また test_build_src = true とする事で、 ./src 以下のファイルをビルドの対象とします。
test_build_src については以下を参照してください。推奨されない方法ではありますが今回の場合は適していると思います。
https://docs.platformio.org/en/latest/advanced/unit-testing/structure/shared-code.html#unit-testing-shared-code

platformio.ini
[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 関数がありませんのでここに記述し、プログラムエントリとします。

test/main.cpp
#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 直下にテストケースを置きます。

test/foo.cpp
#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 による段階的なビルド出力を見た方が良いかも知れません。

pio test -e test_11 -vv 出力例
()
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 に接続する必要があるのでそのための設定も記述します。
対象ハードが複数ある場合はそれぞれの設定を整えます。

platformio.ini
[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 以下のファイルのみが使用されます。

リポジトリ

以上を踏まえたものが以下のものになります。

https://github.com/GOB52/goblib_test
https://github.com/GOB52/goblib
https://github.com/GOB52/goblib_m5s

脚注
  1. ゲームエンジンと同名ですが、違うものです。 ↩︎

Discussion