STM32CubeIDEにCppUTestを環境構築し、STM32マイコンでTDDする(2) ～シリアル通信でCppUTestを実行する～

# この記事は何?
以前STマイクロエレクトロニクスの統合開発環境STM32CubeIDEにテストフレームワークCppUTestを構築した記事を書きました。

 * [STM32CubeIDEにCppUTestを環境構築し、STM32マイコンでTDDする](https://qiita.com/juraruming/items/1f91db17ba93b40d3730)

今回は前回の環境を変更し、シリアルコンソールからの指令でCppUTestを実行できるようにしました。結果、ターゲットマイコンでテストがしやすくなりました。今回構築した環境について紹介します。

今回試したソースコードはつぎになります。

https://github.com/grace2riku/hello_stm32_cpputest

# 確認環境
環境は前回とほぼ同じです。

## ホストPC環境
ホストPC環境はつぎのとおりです。
 * macOS Ventura バージョン 13.4.1 (前回はmacOS Big Sur バージョン 11.6.4)

## STM32CubeIDE
STM32CubeIDEのバージョンはつぎのとおりです。

* Version: 1.9.0 Build: 12015_20220302_0855 (UTC)
※前回と同じ。

## CppUTest
テストフレームワークは前回と同じCppUTestです。

## ハードウェア
使用マイコンボードは前回と同じでNUCLEO-F446REです。

* NUCLEO-F446RE
* STM32F446RET6 64ピン
* Arm Cortex-M4コア 180MHz
* フラッシュ: 512Kbyte
* SRAM: 128Kbyte
* Arm Mbed対応 

![IMG_1054.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171866/1742d9de-e378-dcb2-b896-77fea62b0519.jpeg)

# 前回の環境の課題
前回はテスト結果をSTM32CubeIDEの**SWV ITM Data Console**に表示していました。STM32CubeIDE単体でテスト結果がわかるという利点がありましたが、SWV ITM Data Consoleに指令を出して任意のテストを実行する、という柔軟なことができませんでした。

# 今回の環境
今回は組込みソフトウェア開発のデバッグツールの定番シリアルコンソールへの入力をトリガにして、任意のタイミングで任意のテストを実行できるテスト環境を構築しました。

# 動作紹介
以降にテストコード実行結果を書きます。

## シリアルコンソール入力コマンド
シリアルコンソールでhelp, エンターキー押下でコマンド名が表示されます。
今回はCppUTestをおこなうのでcpputestというコマンド名にしました。

```
>help
help    :This is a description text string for help command.
info    :This is a description text string for info command.
read_userbutton :User button B1 reads.
write_led       :Write to LED LD2.
cpputest        :Exec CppUTest.
```

## cpputest実行(引数指定なし)
引数を指定せずにcpputestを実行した場合はつぎの表示になります。
全てのテストが実行されます。

```
>cpputest
..
../Core/Src/test_src.cpp:10: error: Failure in TEST(FirstTestGroup, FirstTest)
        FAIL: FirstTestGroup, FirstTest


.
Errors (1 failures, 3 tests, 3 ran, 3 checks, 0 ignored, 0 filtered out, 0 ms)
```

3つのテストのうち、1つが失敗した旨の表示です。
今回のテストコードはつぎになります。

```c++:test_src.cpp
TEST(FirstTestGroup, FirstTest)
{
	FAIL("FAIL: FirstTestGroup, FirstTest\n");
}

TEST(FirstTestGroup, SecondTest)
{
	CHECK_EQUAL_ZERO(0);
}

TEST(FirstTestGroup, IntSize)
{
	LONGS_EQUAL(4, sizeof(int));
}
```

FirstTestGroupというテストグループに3つのテストが書かれています。
テストFirstTestはFAILを呼び出しているので必ず失敗するテストです。
残りの2つのテストは成功する想定です。想定とうりの結果になっています。

## cpputest実行(引数"-v"指定)
引数に"v"を指定するとテストの詳細が表示されます。どのテストを実行しているか明確になりました。

```
>cpputest -v
TEST(FirstTestGroup, IntSize) - 0 ms
TEST(FirstTestGroup, SecondTest) - 0 ms
TEST(FirstTestGroup, FirstTest)
../Core/Src/test_src.cpp:10: error: Failure in TEST(FirstTestGroup, FirstTest)
        FAIL: FirstTestGroup, FirstTest


 - 0 ms

Errors (1 failures, 3 tests, 3 ran, 3 checks, 0 ignored, 0 filtered out, 0 ms)
```

## cpputest実行(引数"-v -r3"指定)
引数に"-v -r3"を指定します。
"-rn"はテストをn回繰り返します。今回はテストの実行回数でテスト結果は変わりません。実行回数で結果が変わるコードを確認する際は便利そうです。

```
>cpputest -v -r3
Test run 1 of 3
TEST(FirstTestGroup, IntSize) - 0 ms
TEST(FirstTestGroup, SecondTest) - 0 ms
TEST(FirstTestGroup, FirstTest)
../Core/Src/test_src.cpp:10: error: Failure in TEST(FirstTestGroup, FirstTest)
        FAIL: FirstTestGroup, FirstTest


 - 0 ms

Errors (1 failures, 3 tests, 3 ran, 3 checks, 0 ignored, 0 filtered out, 0 ms)

Test run 2 of 3
TEST(FirstTestGroup, IntSize) - 0 ms
TEST(FirstTestGroup, SecondTest) - 0 ms
TEST(FirstTestGroup, FirstTest)
../Core/Src/test_src.cpp:10: error: Failure in TEST(FirstTestGroup, FirstTest)
        FAIL: FirstTestGroup, FirstTest


 - 0 ms

Errors (1 failures, 3 tests, 3 ran, 3 checks, 0 ignored, 0 filtered out, 0 ms)

Test run 3 of 3
TEST(FirstTestGroup, IntSize) - 0 ms
TEST(FirstTestGroup, SecondTest) - 0 ms
TEST(FirstTestGroup, FirstTest)
../Core/Src/test_src.cpp:10: error: Failure in TEST(FirstTestGroup, FirstTest)
        FAIL: FirstTestGroup, FirstTest


 - 0 ms

Errors (1 failures,r 3 tests, 3 ran, 3 checks, 0 ignored, 0 filtered out, 0 ms)
```

## cpputest実行(引数"-gFirstTestGroup -nxxxTest"指定)
引数に"-gFirstTestGroup -nxxxTest"を指定した場合です。
"-nxxxTest"にはテストグループに属するテスト名を指定します。
テストグループの任意のテストだけを実行したい時は便利です。

### cpputest実行(引数"-gFirstTestGroup -nFirstTest"指定)
引数に"-gFirstTestGroup -nFirstTest"を指定した場合です。
期待とおりテストが失敗しています。

```
>cpputest -gFirstTestGroup -nFirstTest

../Core/Src/test_src.cpp:10: error: Failure in TEST(FirstTestGroup, FirstTest)
        FAIL: FirstTestGroup, FirstTest


.
Errors (1 failures, 3 tests, 1 ran, 1 checks, 0 ignored, 2 filtered out, 0 ms)
```

### cpputest実行(引数"-gFirstTestGroup -nSecondTest"指定)
引数に"-gFirstTestGroup -nSecondTest"を指定した場合です。
期待とおりテストが成功しています。

```
>cpputest -gFirstTestGroup -nSecondTest
.
OK (3 tests, 1 ran, 1 checks, 0 ignored, 2 filtered out, 0 ms)
```

### cpputest実行(引数"-gFirstTestGroup -nIntSize"指定)
引数に"-gFirstTestGroup -nIntSize"を指定した場合です。
期待とおりテストが成功しています。

```
>cpputest -gFirstTestGroup -nIntSize   
.
OK (3 tests, 1 ran, 1 checks, 0 ignored, 2 filtered out, 0 ms)
```

# 実現技術説明
シリアルコンソールからCppUTestを実行できるようになりました。
実現している技術要素について紹介します。

## printf対応
テスト結果をシリアルコンソールに出力・表示する際にprintfを使っています。
syscall.cの_writeで__io_putcharが呼び出されています。__io_putcharを定義し、シリアル送信を行うようにします。

```c++:main.cpp
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
extern "C" {
int __io_putchar(int ch) {
	HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 100);
	return ch;
}

}
```

HAL_UART_Transmitがシリアル送信関数です。
またつぎのコード **setbuf(stdout, NULL);** をmain.cppのmainに追加します。

```c++:main.cpp
int main(void)
{
  /* 省略 */

  /* USER CODE BEGIN 2 */
  setbuf(stdout, NULL);

  /* 省略 */

}
```

## シェル(NT-Shell)の組込み
シリアルコンソールから指令コマンドの受信、画面への表示を簡単におこなうためにNT-Shellを利用します。
NT-Shellを組み込むことでシェルからの指令で動くコマンドを簡単に実装できます。
NT-Shellは個人的によく利用させてもらっているOSSです。
NT-Shellの説明は過去に記事を書いているのでそちらを参照ください。

https://qiita.com/juraruming/items/107f93852ec0e2993b7d

CppUTestの実行はつぎのコードで実装しています。つぎのコードはシリアルコンソールに文字列 **cpputest** が入力されたときに実行されます。

```c++:usrcmd.cpp
static int usrcmd_cpputest(int argc, char **argv){
	CommandLineTestRunner::RunAllTests(argc, argv);
	return 0;
}
```

CppUTestのCommandLineTestRunner::RunAllTestsを実行しています。
CommandLineTestRunner::RunAllTestsの引数argcにシリアルコンソールに入力されたコマンド文字列の個数、argvにコマンド文字列が格納されます。


# 移植について
今回はSTM３２マイコンボードNUCLEO-F446REでCppUTest環境を構築しました。STM３２マイコンのシリアル送信関数はHALで既に用意されているのでNT-Shellとの組み合わせでサクッと環境構築できました。
他のSTM３２マイコンボードでも移植が比較的簡単にできると思います。

またSTM３２マイコンではない他のSoCメーカーのマイコンでもシリアル送受信関数を差し替えれば環境構築可能だと思います。

# おわり
最後まで読んでいただきありがとうございました。
なにか皆さんのテスト環境構築のお役に立てば嬉しいです。