27K Views
November 30, 22
スライド概要
ソフトウェアの品質向上にテストは欠かせません。
ROS2を使った開発を進めるにあたり、テスト工程を見据えて、ROS公式ドキュメントを参照します。
しかし、ROS公式の解説では、テストに関する記述が少なく、そもそもどういったテストが可能なのか、という疑問が生じます。
また、ROS公式の解説で実行可能なサンプルがほとんどなく、公式で動作確認されている記述方法がわからないという問題もあります。
そこで、本セミナーでは、ROS2 で使えるテストフレームワークの解説と、実践サンプルの紹介を行います。
<過去資料>
・ROS2自律走行実現に向けて 1 : https://www.docswell.com/s/fixstars/57VGDN-20221017
・ROS2自律走行実現に向けて 2: https://www.docswell.com/s/fixstars/K8G1N9-20221130
・Jetson活用 ROS2自律走行実現に向けて ~自律移動ロボット用ー自己位置推定のCUDA高速化: https://www.docswell.com/s/fixstars/KQ814X-20211104
・フィックスターズの自動車向けソフトウェア開発: https://at.fixstars.com/ja
フィックスターズは、コンピュータの性能を最大限に引き出すソフトウェア開発のスペシャリストです。車載、産業機器、金融、医療など、幅広い分野での開発経験があります。また、ディープラーニングや機械学習などの最先端技術にも力を入れています。 並列化や最適化技術を駆使して、マルチコアCPU、GPU、FPGA、量子アニーリングマシンなど、さまざまなハードウェアでソフトウェアを高速化するサービスを提供しています。さらに、長年の経験から培ったハードウェアの知識と最適化ノウハウを活かし、高精度で高性能なアルゴリズムの開発も行っています。 ・開催セミナー一覧:https://www.fixstars.com/ja/seminar ・技術ブログ :https://proc-cpuinfo.fixstars.com/
ROS2自律走行実現に向けて 次世代ロボット開発フレームワーク ROS2のテストフレームワーク徹底理解 Copyright © Fixstars Group
本日のAgenda ⚫ はじめに ⚫ フィックスターズのご紹介 ⚫ テストの必要性 ⚫ ROS2で可能なテスト/lint ⚫ サンプルアプリでのテスト実装 ⚫ その他 TIPS Copyright © Fixstars Group 2
はじめに Copyright © Fixstars Group
本セミナーの位置づけ ⚫ ウェビナー「ROS2自律走行実現に向けて」シリーズでは、 自律走行する車両型ロボットの実現に向け、 ROS2を使った開発に関連する、様々な情報を発信しています ⚫ vol.1(発表資料) ROS1上で開発した資産の活用 ○ 自己位置推定パッケージの CUDA高速化 vol.2(発表資料) ○ パッケージ開発に欠かせないビルドシステムの解説 今回の内容 ○ 品質向上に欠かせないテストフレームワークの解説 ○ ⚫ ⚫ ⚫ こんな方に向いています ○ ROS2 の開発をしたことがあり、ソフトウェアの品質を向上させたいとお考えの方 Copyright © Fixstars Group 4
発表者紹介 冨田 明彦 青木 修平 ソリューションカンパニー 執行役員 ソリューション第3事業部 シニアエンジニア 2008年に入社。金融、医療業界において、ソ フトウェア高速化業務に携わる。その後、新規 事業企画、半導体業界の事業を担当し、現職。 2018年に入社。主に ADAS 向けの画像処理ア ルゴリズムの開発や高速化、シミュレーション 環境構築を担当。 Copyright © Fixstars Group 5
フィックスターズの ご紹介 Copyright © Fixstars Group
フィックスターズの強み コンピュータの性能を最大限に引き出す、ソフトウェア高速化のエキスパート集団 ハードウェアの知見 アルゴリズム実装力 各産業・研究分野の知見 目的の製品に最適なハードウェアを見抜き、 その性能をフル活用するソフトウェアを開 発します。 ハードウェアの特徴と製品要求仕様に合わ せて、アルゴリズムを改良して高速化を実 現します。 開発したい製品に使える技術を見抜き、実 際に動作する実装までトータルにサポート します。 Copyright © Fixstars Group 7
サービス提供分野 半導体 産業機器 金融 自動車 ● NAND型フラッシュメモリ向けフ ァームウェア開発 ● 次世代AIチップの開発環境基盤 生命科学 ● Smart Factory実現への支援 ● マシンビジョンシステムの高速化 ● 自動運転の高性能化、実用化 ● ゲノム解析の高速化 ● 次世代パーソナルモビリティの 研究開発 ● 医用画像処理の高速化 Copyright © Fixstars Group ● デリバティブシステムの高速化 ● HFT(アルゴリズムトレード)の高速化 ● AI画像診断システムの研究開発 8
自動車向けソフトウェア開発 アルゴリズム開発から量産車ターゲット向けの高速化まで、 自動運転の実現に向けた統合的な技術開発を行っています。 ご支援内容 Copyright © Fixstars Group 9
組込み高速化 組込み機器製品の計算処理実装をお手伝いしています。 お客様の課題 ご支援内容 組込みシステムの目標性能が達成できない ターゲットデバイスの特性に合わせて、 性能要求を満たしたい 安価なハードウェアでも処理速度を維持し 製品にかかるコストを下げたい ターゲットデバイスの例 ARM/ TOSHIBA Visconti/ Renesas R-Car/ NXP S32/ Automotive Platform/ CEVA-XM6/ Texas Instruments © Fixstars Group C6000/ Cadence Vision DSP FamilyCopyright など 最適化方針のご提案 ボトルネック調査、最適化に向けた検討 システム設計コンサルティング ハードウェア選定を含めたシステム設計のご提案 アルゴリズムの改善と移植 既存アルゴリズムを改善して計算を高速化 組込みアルゴリズム開発 ターゲットデバイス向けに最適化されたアルゴリズムを実装 10
サービス領域一覧 様々な領域でソフトウェア高速化サービスを提供しています。大量データの高速処理は、 お客様の製品競争力の源泉となっています。 組込み高速化 GPU向け高速化 AI・深層学習 画像処理・アルゴリズム 開発 FPGAを活用した システム開発 分散並列システム開発 量子コンピューティング 自動車向け フラッシュメモリ向けフ ソフトウェア開発 ァームウェア開発 Copyright © Fixstars Group 11
ROS2の テストフレームワーク 徹底理解 Copyright © Fixstars Group
本セミナーのねらい ⚫ ソフトウェアの品質向上にはテストは欠かせないが、ROS2ではどういった テストが可能なのか、どのように実現されているか、どのように利用すれば 良いかの情報が少ない ⚫ そこで、本セミナーではサンプルを交えつつ、ROS2におけるテストについ て網羅的な解説を行う ⚫ サンプルは ROS2 Humble で動作確認済み Copyright © Fixstars Group 13
アジェンダ ⚫ テストとは ⚫ ROS2で可能なテスト/lint ⚫ サンプルアプリでのテスト実装 ⚫ その他TIPS Copyright © Fixstars Group 14
テストとは Copyright © Fixstars Group
テストとは ⚫ ソフトウェアが正しく作られているか確認する作業 ⚫ テストの分類 ○ 機能テスト ■ ○ 単体テスト、結合テスト、システムテスト、etc. 非機能テスト ■ パフォーマンステスト、ストレステスト、保守性テスト、etc. ⚫ テストがないと何が困る? ○ 関数やシステムが正しく動作しているか確認できない ⚫ とはいえ、手作業でテストを行うのは大変 ○ → テストフレームワークを使って自動化したい Copyright © Fixstars Group 16
テストとは ⚫ ソフトウェアが正しく作られているか確認する作業 ⚫ テストの分類 ○ 機能テスト ■ ○ 単体テスト、結合テスト、システムテスト、etc. 非機能テスト ■ パフォーマンステスト、ストレステスト、保守性テスト、etc. ⚫ テストがないと何が困る? ○ 関数やシステムが正しく動作しているか確認できない ⚫ とはいえ、手作業でテストを行うのは大変 ○ → テストフレームワークを使って自動化したい ROS2におけるテスト/テストフレームワークの使い方を解説する Copyright © Fixstars Group 17
ROS2でのテストの実行方法 ⚫ ros2 では colcon test でテストを実行する ○ テストに成功した場合 Copyright © Fixstars Group 18
ROS2でのテストの実行方法 ⚫ ros2 では colcon test でテストを実行する ○ テストに失敗した場合 Copyright © Fixstars Group 19
ROS2でのテストの実行方法 ⚫ colcon test-result で成否結果を得られる 終了ステータスが非ゼロであれば 失敗 Copyright © Fixstars Group 20
ROS2でのテストの実行方法 ⚫ colcon test-result で成否結果を得られる XML形式でレポートを得られる Copyright © Fixstars Group 21
ROS2でのテストの実行方法 ⚫ 詳細を見る時は colcon test-result --verbose … Copyright © Fixstars Group 22
… Copyright © Fixstars Group 23
ROS2で可能なテスト /lint Copyright © Fixstars Group
ROS2で可能なテスト/lint ⚫ 公式で提供されているもの ament_cmake ament_python ⚫ https://github.com/ament/ament_cmake ⚫ ament_cmakeの拡張としてテストルー ルが存在 ⚫ 基本的に python のテストの仕組みに則っ ている ⚫ pytest と unittest が使用可能 共通 ⚫ https://github.com/ros2/launch ⚫ launch システムを使用したテストフレームワークが存在 ⚫ https://github.com/ament/ament_lint ⚫ lint のための python ツールと ament_cmake 拡張が存在 Copyright © Fixstars Group 25
ROS2で可能なテスト/lint ⚫ 一覧(解説するもの) ament_cmake launch integration test lint (その2) add_test launch_testing ament_clang_tidy ament_cmake_test launch_pytest ament_cppcheck ament_cmake_gtest ament_cmake_gmock ament_cpplint lint (その1) ament_pclint ament_xmllint ament_flake8 ament_lint_cmake ament_mypy ament_copyright ament_pep257 pytest ament_clang_format ament_pycodestyle unittest ament_uncrustify ament_pyflakes ament_cmake_pytest ament_python Copyright © Fixstars Group 26
ROS2で可能なテスト/lint ⚫ 公式で提供されているもの ament_cmake ament_python ⚫ https://github.com/ament/ament_cmake ⚫ ament_cmakeの拡張としてテストルー ルが存在 ⚫ 基本的に python のテストの仕組みに則っ ている ⚫ pytest と unittest が使用可能 共通 ⚫ https://github.com/ros2/launch ⚫ launch システムを使用したテストフレームワークが存在 ⚫ https://github.com/ament/ament_lint ⚫ lint のための python ツールと ament_cmake 拡張が存在 Copyright © Fixstars Group 27
ament_cmake でのテスト 名前 ターゲット 概要 add_test any ctest (cmake のテストフレームワーク) の機能 ament_cmake でのテストは最終的に add_test の呼び出しとなる ament_cmake_test any ament_cmake が提供するテスト用のマクロ 他のテスト拡張から呼ぶことが想定されているように見える ament_cmake_gtest C/C++ gtest を使うための ament_cmake 拡張 ament_cmake_gmock C/C++ gmock を使うための ament_cmake 拡張 ament_cmake_pytest Python pytest を使うための ament_cmake 拡張 ament_cmake_nose Python テストフレームワークの nose 向けの拡張 nose自体2015年で更新が止まっているので解説を省略 Copyright © Fixstars Group 28
● add_test ● ament_cmake_test ament_cmakeでのテスト ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest Copyright © Fixstars Group 29
概要 ⚫ CMakeに付属しているテストフレームワークである ctest にテストを登録 ○ ○ https://cmake.org/cmake/help/latest/manual/ctest.1.html https://cmake.org/cmake/help/latest/command/add_test.html ○ ○ 以降の全ての ament_cmake 向けのテストは最終的に add_test の呼び出しに繋がっている 任意のテストを作りたい場合も add_test を使えば実行されるようになる ■ とはいえ、あまり使う場面はなさそう ⚫ ament_cmake (純粋なCMakeも) において colcon test は ctest の実行となる ⚫ CMake記述例 # 成功 add_test( NAME test_success COMMAND bash -c "exit 0" ) # 失敗 add_test( NAME test_failure COMMAND bash -c "exit 1" ) 合否判定は単純で、プログラムの終了コードを見ている • 0 → 成功 • 0 以外 → 失敗 Copyright © Fixstars Group 30
実行例 $ colcon test –packages-select example_ctest –event-handlers console_direct+ … test 1 Start 1: test_success 1: Test command: /usr/bin/bash “-c” “exit 0” 1: Test timeout computed to be: 1500 1/2 Test #1: test_success ..................... Passed test 2 Start 2: test_failure 2: Test command: /usr/bin/bash "-c" "exit 1" 2: Test timeout computed to be: 1500 2/2 Test #2: test_failure .....................***Failed ←成功 0.00 sec ←失敗 0.00 sec 50% tests passed, 1 tests failed out of 2 Total Test time (real) = 0.00 sec The following tests FAILED: 2 - test_failure (Failed) Errors while running CTest … Copyright © Fixstars Group 31
● add_test ● ament_cmake_test ament_cmakeでのテスト ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest Copyright © Fixstars Group 32
概要 ⚫ ament_cmake が提供するテスト用のマクロ ○ ○ https://github.com/ament/ament_cmake/blob/rolling/ament_cmake_test/cmake/ament_add_test.cmake 自前でテストを実装したい時以外は使う機会はなさそう ⚫ 最終的には add_test の呼び出しになるが、追加の設定やオプションがある ○ テスト実行を打ち切るまでのタイムアウト (デフォルト60秒) ○ テストをスキップするかどうかの選択 ○ etc. (詳しくは CMakeマクロのドキュメントコメントを参照) ← bash ベースのテストフレームワークの bats を使用 Copyright © Fixstars Group 33
CMake記述例
⚫ bash ベースのテストフレームワークである bats を使用
CMakeLists.txt
set(result_dir ${AMENT_TEST_RESULTS_DIR}/${PROJECT_NAME}/test_bats)
ament_add_test(test_bats
# 実行コマンド
COMMAND bats -F junit -o ${result_dir} test.bats
# 標準出力の出力先
OUTPUT_FILE ${CMAKE_BINARY_DIR}/ament_cmake_test/test_bats.txt
# 実行結果(テストレポート)の保存先
RESULT_FILE ${result_dir}/TestReport-test.bats.xml
# テスト実行のタイムアウト
TIMEOUT 10
# テスト実行ディレクトリ
WORKING_DIRECTORY $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/test>
)
Copyright © Fixstars Group
34
実行例
$ colcon test --packages-select example_ament_cmake_test
…
$ colcon test-result --verbose
build/example_ament_cmake_test/Testing/20221127-1617/Test.xml: 2 tests, 0 errors, 1 failure, 0 skipped
- test_bats
<<< failure message
-- run_test.py: invoking following command in '/workspaces/test-methodlogy/src/one-byone/03_example_ament_cmake_test/test':
- bats -F junit -o /workspaces/testmethodlogy/build/example_ament_cmake_test/test_results/example_ament_cmake_test/test_bats test.bats
1..2
ok 1 Test success in 0sec
not ok 2 Test failure in 0sec
-- run_test.py: return code 1
-- run_test.py: verify result file '/workspaces/testmethodlogy/build/example_ament_cmake_test/test_results/example_ament_cmake_test/test_bats/TestReport-test.bats.xml'
>>>
build/example_ament_cmake_test/test_results/example_ament_cmake_test/test_bats/TestReport-test.bats.xml: 2 tests, 0
errors, 1 failure, 0 skipped
- test.bats Test failure
<<< failure message
>>>
Summary: 5 tests, 0 errors, 2 failures, 0 skipped
Copyright © Fixstars Group
35
C++でテストを記述する場合 CMakeLists.txt add_executable(test_executable test/test.cpp ) CMakeの方法の則って実行ファイルを 作る ament_add_test(test_doctest COMMAND $<TARGET_FILE:test_executable> ) 実行ファイルを COMMAND として渡す。 $<TARGET_FILE:test_executable>は Generator Expressions で、 test_executable へのパスを得られる。 Copyright © Fixstars Group 36
● add_test ● ament_cmake_test ament_cmakeでのテスト ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest Copyright © Fixstars Group 37
概要 ⚫ gtest (GoogleTest) を使うための ament_cmake 拡張 ○ https://github.com/ament/ament_cmake/blob/rolling/ament_cmake_gtest/cmake/ament_add_gtest.cmake ⚫ gtest は C/C++向けのテストフレームワーク ○ ○ https://github.com/google/googletest OpenCVにある入門ガイド : http://opencv.jp/googletestdocs/primer.html ⚫ ROS2でC++コードをテストする際はおおよそこれを使うことになる Copyright © Fixstars Group 38
CMake記述例 add_library(myadd SHARED src/add.cpp ) target_include_directories(myadd PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> ) ライブラリ myadd を作成 if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) ament_add_gtest(myadd_gtest test/add_test.cpp) target_link_libraries(myadd_gtest myadd) endif() Copyright © Fixstars Group myadd をテストする myadd_gtest を作成 39
テストの記述例
example_ament_cmake_gtest/add.hpp
#pragma once
int add(int a, int b);
test/add_test.cpp
テストの書き方はgtestの作法に従う
#include <example_ament_cmake_gtest/add.hpp>
#include <gtest/gtest.h>
TEST(add, success) {
ASSERT_EQ(add(1, 2), 3);
}
成功するテスト
add(1, 2) を呼び出した時の結果は3になるべき
TEST(add, failure) {
ASSERT_EQ(add(1, 2), 4);
}
失敗するテスト
add(1, 2) を呼び出した時の結果は4になるべき
※テストに失敗した時の表示を見るためのもので実際はこのようなテストは書かない
Copyright © Fixstars Group
40
実行例(1/2) $ colcon test --packages-select example_ament_cmake_gtest … $ colcon test-result –verbose build/example_ament_cmake_gtest/Testing/20221113-0648/Test.xml: 1 test, 0 errors, 1 failure, 0 skipped - myadd_gtest <<< failure message -- run_test.py: invoking following command in '/workspaces/test-methodlogy/build/example_ament_cmake_gtest': - /workspaces/test-methodlogy/build/example_ament_cmake_gtest/myadd_gtest --gtest_output=xml:/workspaces/testmethodlogy/build/example_ament_cmake_gtest/test_results/example_ament_cmake_gtest/myadd_gtest.gtest.xml Running main() from /opt/ros/humble/src/gtest_vendor/src/gtest_main.cc [==========] Running 2 tests from 1 test suite. [----------] Global test environment set-up. [----------] 2 tests from add [ RUN ] add.success [ OK ] add.success (0 ms) [ RUN ] add.failure /workspaces/test-methodlogy/src/one-by-one/04_example_ament_cmake_gtest/test/add_test.cpp:9: Failure Expected equality of these values: add(1, 2) Which is: 3 4 [ FAILED ] add.failure (0 ms) [----------] 2 tests from add (0 ms total) Copyright © Fixstars Group 41
実行例(2/2)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] add.failure
1 FAILED TEST
-- run_test.py: return code 1
-- run_test.py: inject classname prefix into gtest result file '/workspaces/testmethodlogy/build/example_ament_cmake_gtest/test_results/example_ament_cmake_gtest/myadd_gtest.gtest.xml'
-- run_test.py: verify result file '/workspaces/testmethodlogy/build/example_ament_cmake_gtest/test_results/example_ament_cmake_gtest/myadd_gtest.gtest.xml'
>>>
build/example_ament_cmake_gtest/test_results/example_ament_cmake_gtest/myadd_gtest.gtest.xml: 2 tests, 0 errors, 1 failure, 0 skipped
- example_ament_cmake_gtest.add failure
<<< failure message
/workspaces/test-methodlogy/src/one-by-one/04_example_ament_cmake_gtest/test/add_test.cpp:9
Expected equality of these values:
add(1, 2)
Which is: 3
4
>>>
Summary: 3 tests, 0 errors, 2 failures, 0 skipped
Copyright © Fixstars Group
42
● add_test ● ament_cmake_test ament_cmakeでのテスト ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest Copyright © Fixstars Group 43
概要 ⚫ gmock (GoogleMock) を使うための ament_cmake 拡張 ○ https://github.com/ament/ament_cmake/blob/rolling/ament_cmake_gmock/cmake/ament_add_gmock.cmake ⚫ gmock は C/C++向けのモックフレームワーク ○ ○ https://github.com/google/googletest (gtest に含まれる) OpenCVにある入門ガイド : http://opencv.jp/googlemockdocs/fordummies.html ⚫ ROS2でC++でモックを使ったテストをする際はだいたいこれを使うことに なる Copyright © Fixstars Group 44
CMake記述例 add_library(mylib SHARED src/foo.cpp ) target_include_directories(mylib PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> ) if(BUILD_TESTING) find_package(ament_cmake_gmock REQUIRED) ament_add_gmock(my_ament_cmake_gmock test/foo_mock.cpp) target_link_libraries(my_ament_cmake_gmock mylib) endif() Copyright © Fixstars Group 45
テストの記述例
example_ament_cmake_gmock/foo.hpp
#pragma once
class Foo
{
public:
virtual bool func(int a) = 0;
};
テスト対象は 関数 bar
bar は、
1. クラス Foo のインスタンス fooを引数として取り
2. 0 を引数として foo->func を呼び出し
3. foo->func の返り値を bar の返り値として返す
bool bar(Foo * foo);
bar のテストで確認したいのは、
1. func が呼び出されていること
2. その引数が0であること
3. func が true を返した時、 bar も true を返すこと
src/foo.cpp
#include "example_ament_cmake_gmock/foo.hpp"
bool bar(Foo *foo) { return foo->func(0); }
Copyright © Fixstars Group
46
テストの記述例
test/foo_mock.cpp
#include <example_ament_cmake_gmock/foo.hpp>
#include <gmock/gmock.h>
class MockFoo : public Foo
{
public:
MOCK_METHOD1(func, bool(int));
};
TEST(bar, success) {
using namespace testing;
MockFoo mock_foo;
EXPECT_CALL(mock_foo, func(0))
.Times(1)
.WillOnce(Return(true));
モッククラスの作成
//
//
//
//
EXPECT_EQ(bar(&mock_foo), true);
}
テストの書き方は
gmockの作法に従う
mock_fooの使われ方の想定は、
0を引数としてfuncが呼び出されること
1度だけ呼ばれること
funcはtrueを返す
モックオブジェクトを設定
テスト
Copyright © Fixstars Group
47
実行例(1/2)
$ colcon test --packages-select example_ament_cmake_gmock
…
$ colcon test-result --verbose
build/example_ament_cmake_gmock/Testing/20221113-1555/Test.xml: 1 test, 0 errors, 1 failure, 0 skipped
- my_ament_cmake_gmock
<<< failure message
-- run_test.py: invoking following command in '/workspaces/test-methodlogy/build/example_ament_cmake_gmock':
- /workspaces/test-methodlogy/build/example_ament_cmake_gmock/my_ament_cmake_gmock --gtest_output=xml:/workspaces/test失敗するケースも追加
methodlogy/build/example_ament_cmake_gmock/test_results/example_ament_cmake_gmock/my_ament_cmake_gmock.gtest.xml
Running main() from gmock_main.cc
EXPECT_CALL(mock_foo, func(0))
[==========] Running 2 tests from 1 test suite.
.Times(2) // 2回呼ばれること
[----------] Global test environment set-up.
[----------] 2 tests from bar
.WillRepeatedly(Return(true));
[ RUN
] bar.success
[
OK ] bar.success (0 ms)
[ RUN
] bar.failure
/workspaces/test-methodlogy/src/one-by-one/05_example_ament_cmake_gmock/test/foo_mock.cpp:23: Failure
Actual function call count doesn't match EXPECT_CALL(mock_foo, func(0))...
Expected: to be called twice
Actual: called once - unsatisfied and active
[ FAILED ] bar.failure (0 ms)
[----------] 2 tests from bar (0 ms total)
Copyright © Fixstars Group
48
実行例(2/2)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] bar.failure
1 FAILED TEST
-- run_test.py: return code 1
-- run_test.py: inject classname prefix into gtest result file '/workspaces/testmethodlogy/build/example_ament_cmake_gmock/test_results/example_ament_cmake_gmock/my_ament_cmake_gmock.gtest.xml'
-- run_test.py: verify result file '/workspaces/testmethodlogy/build/example_ament_cmake_gmock/test_results/example_ament_cmake_gmock/my_ament_cmake_gmock.gtest.xml'
>>>
build/example_ament_cmake_gmock/test_results/example_ament_cmake_gmock/my_ament_cmake_gmock.gtest.xml: 2 tests, 0 errors, 1 failure, 0
skipped
- example_ament_cmake_gmock.bar failure
<<< failure message
/workspaces/test-methodlogy/src/one-by-one/05_example_ament_cmake_gmock/test/foo_mock.cpp:23
Actual function call count doesn't match EXPECT_CALL(mock_foo, func(0))...
Expected: to be called twice
Actual: called once - unsatisfied and active
>>>
Summary: 3 tests, 0 errors, 2 failures, 0 skipped
Copyright © Fixstars Group
49
● add_test ● ament_cmake_test ament_cmakeでのテスト ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest Copyright © Fixstars Group 50
概要 ⚫ pytest を使うための ament_cmake 拡張 ○ https://github.com/ament/ament_cmake/tree/rolling/ament_cmake_pytest ⚫ pytest は python のテストフレームワーク ○ https://docs.pytest.org/en/stable/ ⚫ ament_cmake は基本的に C/C++ を扱うが、そこに pytest によるテストを混 ぜたい時に使う Copyright © Fixstars Group 51
CMake記述例 find_package(ament_cmake_pytest) ament_add_pytest_test(example_pytest test ) ファイルかディレクトリを指定 python3 -u -m pytest ${CMAKE_CURRENT_SOURCE_DIR}/test … が実行される ↓ディレクトリ構成 . ├─ package.xml ├─ CMakeLists.txt ├─ test │ └─ test_add.py ... Copyright © Fixstars Group 52
テストの記述例 example_ament_cmake_pytest/add.py def add(a, b): return a + b test/test_add.py import pytest from example_ament_cmake_pytest.add import add def test_success(): # 成功するテスト assert add(1, 2) == 3 def test_failure(): # 失敗するテスト assert add(1, 2) == 4 Copyright © Fixstars Group 53
実行例(1/2) $ colcon test --packages-select example_ament_cmake_pytest … $ colcon test-result --verbose build/example_ament_cmake_pytest/Testing/20221113-1623/Test.xml: 1 test, 0 errors, 1 failure, 0 skipped - example_pytest <<< failure message -- run_test.py: invoking following command in '/workspaces/test-methodlogy/build/example_ament_cmake_pytest': - /usr/bin/python3.10 -u -m pytest /workspaces/test-methodlogy/src/one-by-one/09_example_ament_cmake_pytest/test -o cache_dir=/workspaces/test-methodlogy/build/example_ament_cmake_pytest/ament_cmake_pytest/example_pytest/.cache --junitxml=/workspaces/test-methodlogy/build/example_ament_cmake_pytest/test_results/example_ament_cmake_pytest/example_pytest.xunit.xml --junitprefix=example_ament_cmake_pytest ============================= test session starts ============================== platform linux -- Python 3.10.6, pytest-6.2.5, py-1.10.0, pluggy-0.13.0 cachedir: build/example_ament_cmake_pytest/ament_cmake_pytest/example_pytest/.cache rootdir: /workspaces/test-methodlogy plugins: ament-pep257-0.12.4, ament-xmllint-0.12.4, launch-testing-ros-0.19.3, ament-copyright-0.12.4, ament-flake8-0.12.4, ament-lint-0.12.4, launch-pytest-1.0.3, launch-testing-1.0.3, colcon-core-0.10.0 collected 2 items ⚫ 実行例 ../../src/one-by-one/09_example_ament_cmake_pytest/test/test_add.py .F [100%] =================================== FAILURES =================================== _________________________________ test_failure _________________________________ Copyright © Fixstars Group 54
実行例(2/2)
def test_failure(): # 失敗するテスト
assert add(1, 2) == 4
assert 3 == 4
+ where 3 = add(1, 2)
⚫ 実行例
>
E
E
../../src/one-by-one/09_example_ament_cmake_pytest/test/test_add.py:10: AssertionError
- generated xml file: /workspaces/testmethodlogy/build/example_ament_cmake_pytest/test_results/example_ament_cmake_pytest/example_pytest.xunit.xml =========================== short test summary info ============================
FAILED ../../src/one-by-one/09_example_ament_cmake_pytest/test/test_add.py::test_failure
========================= 1 failed, 1 passed in 0.05s ==========================
-- run_test.py: return code 1
-- run_test.py: verify result file '/workspaces/testmethodlogy/build/example_ament_cmake_pytest/test_results/example_ament_cmake_pytest/example_pytest.xunit.xml'
>>>
build/example_ament_cmake_pytest/test_results/example_ament_cmake_pytest/example_pytest.xunit.xml: 2 tests, 0 errors, 1 failure, 0 skipped
- example_ament_cmake_pytest.src.one-by-one.09_example_ament_cmake_pytest.test.test_add test_failure
<<< failure message
assert 3 == 4
+ where 3 = add(1, 2)
>>>
Summary: 3 tests, 0 errors, 2 failures, 0 skipped
Copyright © Fixstars Group
55
ROS2で可能なテスト/lint ⚫ 公式で提供されているもの ament_cmake ament_python ⚫ https://github.com/ament/ament_cmake ⚫ ament_cmakeの拡張としてテストルー ルが存在 ⚫ 基本的に python のテストの仕組みに則っ ている ⚫ pytest と unittest が使用可能 共通 ⚫ https://github.com/ros2/launch ⚫ launch システムを使用したテストフレームワークが存在 ⚫ https://github.com/ament/ament_lint ⚫ lint のための python ツールと ament_cmake 拡張が存在 Copyright © Fixstars Group 56
ament_pythonでのテスト ● ament_pythonでのテスト ● pytest ● unittest Copyright © Fixstars Group 57
ament_pythonでのテスト
⚫ 基本的に python のテストフレームワークに則る
○
pytest か unittest
○
pytest の場合は python3 –m pytest ... が呼ばれる
○
unittest の場合は python3 –m unittest ... が呼ばれる
⚫ colcon test を実行した時、pytest と unittest のどちらが使用されるか?
○
setup.py の tests_require に pytest が存在していれば pytest
○
そうでなければ unittest
○
あるいは、 colcon test の --python-testing オプションで指定可能
# colcon test –help より
Arguments for 'python' packages:
--python-testing {pytest,setuppy_test}
The Python testing framework to use (default: determined based on the packages `tests_require`)
* pytest: Use `pytest` to test Python packages
* setuppy_test: Use `unittest` to test packages
Copyright © Fixstars Group
58
ament_pythonでのテスト ● ament_pythonでのテスト ● pytest ● unittest Copyright © Fixstars Group 59
概要 ⚫ pytest は python 向けのテストフレームワーク ○ https://docs.pytest.org/en/stable/ ○ 標準ライブラリではないが現在主流な模様 ⚫ 必要な準備 setup.py from setuptools import setup package_name = 'example_ament_python_pytest' setup( name=package_name, ... tests_require=['pytest'], ... ) tests_require に pytest を追加 Copyright © Fixstars Group 60
テストの記述例 ⚫ テスト対象の関数 example_ament_python_pytest/add.py def add(a, b): return a + b ⚫ テスト test/test_add.py import pytest from example_ament_python_pytest.add import add def test_success(): # 成功するテスト assert add(1, 2) == 3 def test_failure(): # 失敗するテスト assert add(1, 2) == 4 Copyright © Fixstars Group 61
実行例 $ colcon test --packages-select example_ament_python_pytest … $ colcon test-result --verbose build/example_ament_python_pytest/pytest.xml: 2 tests, 0 errors, 1 failure, 0 skipped - example_ament_python_pytest.test.test_add test_failure <<< failure message assert 3 == 4 + where 3 = add(1, 2) >>> Summary: 2 tests, 0 errors, 1 failure, 0 skipped Copyright © Fixstars Group 62
ament_pythonでのテスト ● ament_pythonでのテスト ● pytest ● unittest Copyright © Fixstars Group 63
ament_python - unittest ⚫ python の標準ライブラリに含まれているテストフレームワーク ○ https://docs.python.org/ja/3/library/unittest.html ⚫ 必要な準備 ○ 特になし ○ tests_require に pytest を追加しないこと ⚫ 注意点 ○ XML形式のレポートが生成されず、 colcon test-result でテスト結果を得ることができない ○ colcon test 時に標準エラーに unittest のメッセージが出力される ○ 失敗時は colcon test の終了コードが非ゼロになる Copyright © Fixstars Group 64
ament_python - unittest test/test_add.py import unittest from example_ament_python_unittest.add import add class AddTestCase(unittest.TestCase): def test_success(self): self.assertEqual(add(1, 2), 3) def test_failure(self): self.assertEqual(add(1, 2), 4) Copyright © Fixstars Group 65
実行例
$ colcon test --packages-select example_ament_python_unittest
Starting >>> example_ament_python_unittest
--- stderr: example_ament_python_unittest
test_failure (test.test_add.AddTestCase) ... FAIL
test_success (test.test_add.AddTestCase) ... ok
======================================================================
FAIL: test_failure (test.test_add.AddTestCase)
---------------------------------------------------------------------Traceback (most recent call last):
File "/workspaces/test-methodlogy/src/one-by-one/08_example_ament_python_unittest/test/test_add.py", line 9, in test_failure
self.assertEqual(add(1, 2), 4)
AssertionError: 3 != 4
---------------------------------------------------------------------Ran 2 tests in 0.000s
FAILED (failures=1)
--Failed <<< example_ament_python_unittest [0.30s, exited with code 1]
Summary: 0 packages finished [0.53s]
1 package failed: example_ament_python_unittest
1 package had stderr output: example_ament_python_unittest
Copyright © Fixstars Group
66
ROS2で可能なテスト/lint ⚫ 公式で提供されているもの ament_cmake ament_python ⚫ https://github.com/ament/ament_cmake ⚫ ament_cmakeの拡張としてテストルー ルが存在 ⚫ 基本的に python のテストの仕組みに則っ ている ⚫ pytest と unittest が使用可能 共通 ⚫ https://github.com/ros2/launch ⚫ launch システムを使用したテストフレームワークが存在 ⚫ https://github.com/ament/ament_lint ⚫ lint のための python ツールと ament_cmake 拡張が存在 Copyright © Fixstars Group 67
launch integration test ● launch integration test とは ● launch_testing ● launch_pytest Copyright © Fixstars Group 68
launch integration test とは ⚫ launch システムを使ったテストを作成できるフレームワーク ⚫ 機能(リポジトリ説明より※) ○ テストで実行する全てのプロセスの終了コードの取得 ○ プロセスが正常終了したかの確認、あるいは特定の終了コードで終わったかの確認 ○ プロセスが意図せず死んだ時にテストを失敗させることができる ○ 全てのプロセスの標準出力/標準エラーを取得 ○ 任意のコマンドラインをテストに使用できる ○ テスト自体はプロセスの起動と並行に実行でき、実行中のプロセスとやりとりできる ⚫ Python で作られている ⚫ launch_testing と launch_pytest がある ※ https://github.com/ros2/launch/tree/rolling/launch_testing Copyright © Fixstars Group 69
launch integration test とは ⚫ 処理の概要(例) Test System PUT launch process under test ROS2のノードなど publish subscribe test shutdown Copyright © Fixstars Group 70
launch integration test ● launch integration test とは ● launch_testing ● launch_pytest Copyright © Fixstars Group 71
概要 ⚫ unittest ベースとした launch integration test のフレームワーク ○ https://github.com/ros2/launch/tree/rolling/launch_testing ⚫ 例題 ○ Int32 の値をサブスクライブし、2倍にしてパブリッシュする twice ノードをテストする ○ やりたいのは、 ■ twice ノードを起動する ■ テスト用のノードを作り、 Int32 の値を送る ■ twice ノードから Int32 の値を受け取り、それが送った値の2倍になっているか検証する Copyright © Fixstars Group 72
テストの記述例(launch部分) @pytest.mark.launch_test def generate_test_description(): return launch.LaunchDescription([ launch_ros.actions.Node( package="example_launch_testing_ament_cmake", executable="twice", ), launch_testing.actions.ReadyToTest(), ]) Copyright © Fixstars Group generate_test_description という関数を定義する。 記述方法は ROS2 の launch(python版)と同じ。 73
テストの記述例(launch部分) @pytest.mark.launch_test def generate_test_description(): return launch.LaunchDescription([ launch_ros.actions.Node( package="example_launch_testing_ament_cmake", executable="twice", ), launch_testing.actions.ReadyToTest(), ]) twice ノードを起動 するアクション テスト用のアクション Copyright © Fixstars Group 74
テストの記述例(テスト用ノード(1/2)) class DummyTestNode(Node): def __init__(self): super().__init__("test_node") self._msgs = [] self._pub = self.create_publisher(Int32, "src", 10) self._sub = self.create_subscription( Int32, "dst", lambda msg: self._msgs.append(msg), 10 ) /src に送って、 /dst から受け取る assert self._wait_for_connect() def _wait_for_connect(self, timeout_s=5): end_time = time.time() + timeout_s while time.time() < end_time: cnt = self._pub.get_subscription_count() if cnt > 0: return True time.sleep(0.1) return False ... Copyright © Fixstars Group 接続されている subscription の数を数えることで、接続が 確立されたことを確認する 75
テストの記述例(テスト用ノード(2/2)) class DummyTestNode(Node): ... def publish(self, data): self._pub.publish(data) def get_message(self, timeout_s=5.0): start_len = len(self._msgs) executor = rclpy.executors.SingleThreadedExecutor() executor.add_node(self) try: end_time = time.time() + timeout_s while time.time() < end_time: executor.spin_once(timeout_sec=0.1) if start_len != len(self._msgs): break Executor の spin_onceを使い、 受信データをポーリング finally: executor.remove_node(self) executor.shutdown() assert start_len != len(self._msgs) return self._msgs[-1] Copyright © Fixstars Group 76
テストの記述例(unittest部分) class TestTwice(unittest.TestCase): @classmethod def setUpClass(cls): rclpy.init() setUpClass each test cases launch PUT @classmethod def tearDownClass(cls): rclpy.shutdown() setUp def setUp(self): self._node = DummyTestNode() test_xxx def tearDown(self): self._node.destroy_node() shutdown PUT def test_success(self): test_data = Int32(data=1) self._node.publish(test_data) tearDown ans = self._node.get_message() self.assertEqual(ans, Int32(data=2)) def test_success2(self): ... tearDownClass Copyright © Fixstars Group 77
テストの記述例(unittest部分) class TestTwice(unittest.TestCase): @classmethod def setUpClass(cls): rclpy.init() setUpClass each test cases launch PUT @classmethod def tearDownClass(cls): rclpy.shutdown() setUp def setUp(self): self._node = DummyTestNode() test_xxx def tearDown(self): self._node.destroy_node() shutdown PUT def test_success(self): test_data = Int32(data=1) self._node.publish(test_data) tearDown ans = self._node.get_message() self.assertEqual(ans, Int32(data=2)) def test_success2(self): ... tearDownClass Copyright © Fixstars Group 78
実行例
コマンドラインツールの
launch_test を使用
$ launch_test test/test_twice_launch.py
[INFO] [launch]: All log files can be found below
/home/vscode/.ros/log/2022-11-14-15-24-26-445770-62e34440bc2c-2166924
[INFO] [launch]: Default logging verbosity is set to INFO
test_failure (test_twice_launch.TestTwice) ... [INFO] [twice-1]: process
started with pid [2166932]
FAIL
test_success (test_twice_launch.TestTwice) ... ok
test_success2 (test_twice_launch.TestTwice) ... ok
======================================================================
FAIL: test_failure (test_twice_launch.TestTwice)
---------------------------------------------------------------------Traceback (most recent call last):
File "/workspaces/test-methodlogy/src/one-byone/10_example_launch_testing_ament_cmake/test/test_twice_launch.py", line
77, in test_failure
self.assertEqual(ans, Int32(data=3))
AssertionError: std_msgs.msg.Int32(data=2) != std_msgs.msg.Int32(data=3)
---------------------------------------------------------------------Ran 3 tests in 0.471s
FAILED (failures=1)
[INFO] [twice-1]: sending signal 'SIGINT' to process[twice-1]
[twice-1] [INFO] [1668439466.941480504] [rclcpp]: signal_handler(signum=2)
[INFO] [twice-1]: process has finished cleanly [pid 2166932]
---------------------------------------------------------------------Ran 0 tests in 0.000s
OK
Copyright © Fixstars Group
79
ament_cmakeから使う場合 ⚫ launch_testing_ament_cmake を利用する ⚫ CMake記述例 if(BUILD_TESTING) find_package(launch_testing_ament_cmake REQUIRED) add_launch_test(test/test_twice_launch.py) endif() ctest 経由で launch_test が実行される。 launch_test には XML形式のレポート作成機能があるため、それがテストの結果として得られる。 Copyright © Fixstars Group 80
ament_pythonから使う場合 ⚫ package.xml の test_depend に launch_testing を追加する ⚫ generate_test_description に @pytest.mark.launch_test をデコレータとして 付ける ⚫ ファイル名を test_xxx.py か xxx_test.py (pytest のルール)にする Copyright © Fixstars Group 81
launch integration test ● launch integration test とは ● launch_testing ● launch_pytest Copyright © Fixstars Group 82
概要 ⚫ pytest ベースとした launch integration test のフレームワーク ○ humble で導入された ○ https://github.com/ros2/launch/tree/rolling/launch_pytest ⚫ launch_testing との違いは何か ○ ○ 純粋な pytest 拡張である ■ テストケースを名前でフィルターして実行できる ■ 失敗し得るテストケースというマークを付けられる ■ pytest が提供するエラーレポートの仕組みを使える launch integration test を実現するという目的に違いはない Copyright © Fixstars Group 83
テストの記述例(launch部分) @launch_pytest.fixture def generate_test_description(): return launch.LaunchDescription( [ launch_ros.actions.Node( package="example_launch_pytest", executable="twice", ), ] ) Copyright © Fixstars Group launch_testing と同じく launch を 記述する 84
テストの記述例(launch部分) @launch_pytest.fixture デコレータ をつける @launch_pytest.fixture def generate_test_description(): 関数名は何でも良い(明示的に指定するため) return launch.LaunchDescription( [ launch_ros.actions.Node( package="example_launch_pytest", executable="twice", ), ] launch_testing.actions.ReadyToTest() は省略可。 ) なければ自動的に追加される。 Copyright © Fixstars Group 85
テストの記述例(テスト用ノード) class DummyTestNode(Node): def __init__(self): super().__init__("test_node") self._pub = self.create_publisher(Int32, "src", 10) self._sub = self.create_subscription(Int32, "dst", self._msg_received, 10) self.msgs = [] self.msg_event_object = Event() def start(self): self._ros_spin_thread = Thread( target=lambda node: rclpy.spin(node), args=(self,) ) self._ros_spin_thread.start() assert self._wait_for_connect() def publish(self, data): self._pub.publish(data) launch_testing の時と凡そ同じだが、 rclpy.spin を別スレッドで実行するよ うにしている。 また、 Event を使ったスレッドコン トロールも活用 def _msg_received(self, msg): self.msgs.append(msg) self.msg_event_object.set() Copyright © Fixstars Group 86
テストの記述例(pytest部分) each test cases @pytest.fixture def make_test_node(): rclpy.init() node = DummyTestNode() node.start() yield node rclpy.shutdown() launch PUT テストノードを作るフィクスチャ set up test_xxx @pytest.mark.launch(fixture=generate_test_description) def test_success(make_test_node): node = make_test_node node.publish(Int32(data=2)) assert node.msg_event_object.wait(timeout=5.0), "Did not receive msgs" assert node.msgs[0] == Int32(data=4) shutdown PUT tear down テストケース Copyright © Fixstars Group 87
テストの記述例(pytest部分) ⚫ 実行順 each test cases launch PUT @pytest.fixture def make_test_node(): rclpy.init() node = DummyTestNode() node.start() yield node rclpy.shutdown() set up test_xxx @pytest.mark.launch(fixture=generate_test_description) def test_success(make_test_node): node = make_test_node node.publish(Int32(data=2)) assert node.msg_event_object.wait(timeout=5.0), "Did not receive msgs" assert node.msgs[0] == Int32(data=4) Copyright © Fixstars Group shutdown PUT tear down 88
テストの記述例(pytest部分) ⚫ フィクスチャ周り @pytest.fixture def make_test_node(): rclpy.init() node = DummyTestNode() node.start() yield node rclpy.shutdown() each test cases launch PUT set up LaunchDescriptionを返すフィクスチャを指定 test_xxx @pytest.mark.launch(fixture=generate_test_description) def test_success(make_test_node): node = make_test_node 名前指定でフィクスチャが使用される node.publish(Int32(data=2)) assert node.msg_event_object.wait(timeout=5.0), "Did not receive msgs" assert node.msgs[0] == Int32(data=4) shutdown PUT tear down yield で返されるオブジェクトを得られる Copyright © Fixstars Group 89
実行例
pytest で実行可能
$ python3 -m pytest test -q --show-capture no
..F
[100%]
============================================ FAILURES
=============================================
__________________________________________ test_failure
___________________________________________
make_test_node = <test_twice_launch.DummyTestNode object at
0x7f8f5b844d60>
@pytest.mark.launch(fixture=generate_test_description)
def test_failure(make_test_node):
node = make_test_node
node.publish(Int32(data=3))
assert node.msg_event_object.wait(timeout=5.0), "Did not receive msgs"
>
assert node.msgs[0] == Int32(data=5)
E
assert std_msgs.msg.Int32(data=6) == std_msgs.msg.Int32(data=5)
E
+ where std_msgs.msg.Int32(data=5) = Int32(data=5)
test/test_twice_launch.py:48: AssertionError
===================================== short test summary info
=====================================
FAILED test/test_twice_launch.py::test_failure - assert std_msgs.msg.Int32(data=6)
== std_msgs.m...
1 failed, 2 passed in 1.37s
Copyright © Fixstars Group
90
使用方法 ⚫ ament_cmake から使う場合 ○ pytest 拡張であるため ament_cmake_pytest を併用する CMakeLists.txt package.xml <test_depend>ament_cmake_pytest</test_depend> <test_depend>launch_pytest</test_depend> find_package(ament_cmake_pytest) ament_add_pytest_test(pytest test ) ⚫ ament_pythonから使う場合 ○ package.xml の test_depend に launch_pytest を追加する Copyright © Fixstars Group 91
ROS2で可能なテスト/lint ⚫ 公式で提供されているもの ament_cmake ament_python ⚫ https://github.com/ament/ament_cmake ⚫ ament_cmakeの拡張としてテストルー ルが存在 ⚫ 基本的に python のテストの仕組みに則っ ている ⚫ pytest と unittest が使用可能 共通 ⚫ https://github.com/ros2/launch ⚫ launch システムを使用したテストフレームワークが存在 ⚫ https://github.com/ament/ament_lint ⚫ lint のための python ツールと ament_cmake 拡張が存在 Copyright © Fixstars Group 92
● ROS2でのlint ● ament_cmake での使い方 ● ament_python での使い方 lint Copyright © Fixstars Group 93
lintとは ⚫ 静的解析ツールを意味する ○ コードフォーマットの確認 ○ バグの原因になりそうな部分の検知(未使用変数であったり) ○ etc. ⚫ ROS2では以下の形で提供されている ○ Pythonライブラリ ○ コマンドラインツール ○ ament_cmake 拡張 Copyright © Fixstars Group 94
lint一覧 名前 ターゲット 概要 ament_xmllint XML xmllint によるスタイルチェック ament_lint_cmake CMake CMakeLint によるスタイルチェック ament_copyright C/C++/CMake/Python copyright と license が書かれているかチェック ament_clang_format C/C++ clang-format によるスタイルチェック(ルールファイルが含まれている) ament_uncrustify C/C++ Uncrustifyによるスタイルチェック ament_clang_tidy C/C++ clang-tidy によるコードチェック 未使用変数のチェック、暗黙的なキャストのチェックなど。(項目は沢山ある) ament_cppcheck C/C++ CppCheckによる静的解析 メモリリークの可能性などを指摘できる ament_cpplint C/C++ cpplintによるスタイルチェック Google C++コーディングスタイルに準じているかチェックする ament_pclint C/C++ PCLintによる静的解析 MISRAなどが含まれる。有料 ament_flake8 Python flake8によるシンタックスとスタイルチェック ament_mypy Python mypyによるシンタックスとスタイルチェック ament_pep257 Python pep257による docstring スタイルチェック ament_pycodestyle Python pycodesstyleによるスタイルチェック ament_pyflakes Python Copyright © Fixstars Group Pyflakesによるスタイルチェック 共通 C/C++向け Python向け 95
共通 ⚫ これらは必要に応じて入れれば良く、lint同士が競合するということはない 名前 ターゲット 概要 ament_xmllint XML xmllint によるスタイルチェック。 XMLスキーマの妥当性を確認する。 ROS2では http://download.ros.org/schema/package_format3.xsd を使用 ament_lint_cmake CMake CMakeLint によるスタイルチェック。 詳細は https://github.com/cmake-lint/cmake-lint わかりやすいところでは余計な空白がないか、など ament_copyright C/C++/CMake/Python copyright と license が書かれているかチェック。 c, .cc, .cpp, .cxx, .h, .hh, .hpp, .hxx, .cmake, .py のファイルが検証される。 ライセンスの表記方法にテンプレート(※)があり、それに従っているかどうか検証される。 ※ https://github.com/ament/ament_lint/tree/rolling/ament_copyright/ament_copyright/template Copyright © Fixstars Group 96
C/C++ ⚫ コードフォーマット。排他的でどちらか1つ使用する 名前 概要 ament_clang_format コードがフォーマット済みであるかを検証。clang-format を使用 ament_uncrustify コードがフォーマット済みであるかを検証。Uncrustify を使用 ⚫ その他静的解析。排他的ではなく、それぞれ検証項目が違うと考えれば良い 名前 概要 ament_clang_tidy clang-tidy によるコードチェック。 未使用変数のチェック、暗黙的なキャストのチェックなど。(項目は沢山ある) ament_cppcheck CppCheckによる静的解析 メモリリークの可能性などを指摘できる ament_cpplint cpplintによるスタイルチェック Google C++コーディングスタイルに準じているかチェックする ament_pclint PC-Lintによる静的解析 MISRAなどが含まれる。有料 Copyright © Fixstars Group 97
Python ⚫ コードフォーマット ○ → 存在しない ⚫ その他静的解析 ○ Pythonにはコードの体裁に関する規約があり、それらがPEP8やPEP257となっている ○ 必要に応じて選択すれば良いが、総括すると flake8 と mypy、 pep257 を入れると被りがない 名前 ターゲット 概要 ament_flake8 論理エラー/PEP8/循環参照 PyFlakesとpycodestyleとmccabeのラッパー ament_mypy Type Hints (PEP484) 型ヒントのチェック ament_pep257 Docstring (PEP257) Docstring のチェック ament_pycodestyle Style Guide for Python Code(PEP8) コーディングスタイルのチェック ament_pyflakes 論理エラー 論理的なエラー(未使用ライブラリや未定義のシンボルなど)を検出 Copyright © Fixstars Group 98
結局何を使えば良いか? ⚫ 必要に応じて選択する ○ ros2 pkg create ... では以下がデフォルトになっている ament_cmake ⚫ ament_lint_common ⚫ ROS2公式で推奨されている※ ⚫ 以下のパッケージの組み合わせ ⚫ ament_cmake_copyright ⚫ ament_cmake_cppcheck ⚫ ament_cmake_cpplint ⚫ ament_cmake_flake8 ⚫ ament_cmake_lint_cmake ⚫ ament_cmake_pep257 ⚫ ament_cmake_uncrustify ⚫ ament_cmake_xmllint ⚫ ros2 pkg create 直後では copyright と cpplint は無効化 ament_python ⚫ ament_copyright ⚫ ⚫ ⚫ ros2 pkg create 直後では無効化 ament_flake8 ament_pep257 ※ https://docs.ros.org/en/rolling/How-To-Guides/Ament-CMake-Documentation.html#linting Copyright © Fixstars Group 99
コマンドラインでの使用例
$ ament_uncrustify
No code style divergence in file 'include/example_ament_cmake_lint/add.hpp'
Code style divergence in file 'src/add.cpp':
--- src/add.cpp
+++ src/add.cpp.uncrustify
@@ -8 +8 @@
-int add(int a, int b) {return a
+ b;}
+int add(int a, int b) {return a + b;}
1 files with code style divergence
解析だけでなくコード修正可能
なものもある
$ ament_uncrustify --reformat
No code style divergence in file 'include/example_ament_cmake_lint/add.hpp'
Code style divergence in file 'src/add.cpp': reformatted file
1 files with code style divergence
Copyright © Fixstars Group
100
● ROS2でのlint ● ament_cmake での使い方 ● ament_python での使い方 lint Copyright © Fixstars Group 101
ament_cmake での使い方 ⚫ 基本的にament_lint_auto を利用すれば良い package.xml <test_depend>ament_lint_auto</test_depend> <test_depend>ament_cmake_copyright</test_depend> <test_depend>ament_cmake_uncrustify</test_depend> ⚫ ament_lint_auto のマクロは package.xml を読み取る ○ test_depend で指定された lint は自動的にロードされ、lintが実行されるようになる CMakeLists.txt find_package(ament_lint_auto REQUIRED) ament_lint_auto_find_test_dependencies() Copyright © Fixstars Group 102
応用 ⚫ 特定の lint を ament_lint_auto での自動実行から外したい場合 ○ AMENT_LINT_AUTO_EXCLUDE を使用する CMakeLists.txt ament_cmake_uncrustify を除外 set(AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify) ament_lint_auto_find_test_dependencies() ament_uncrustify( CONFIG_FILE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/resource/ament_code_style.cfg> ) 設定を変えて lint を実行 指定可能なオプションは CMakeマクロのドキュメントコメントを見る。↓は ament_uncrustifyの場合。 https://github.com/ament/ament_lint/blob/rolling/ament_cmake_uncrustify/cmake/ament_uncrustify.cmake Copyright © Fixstars Group 103
実行例(1/2)
$ colcon test --packages-select example_ament_cmake_lint
...
$ colcon test-result --verbose
build/example_ament_cmake_lint/Testing/20221115-1302/Test.xml: 2 tests, 0 errors, 1 failure, 0 skipped
- uncrustify
<<< failure message
-- run_test.py: invoking following command in '/workspaces/test-methodlogy/src/lint/example_ament_cmake_lint':
- /opt/ros/humble/bin/ament_uncrustify --xunit-file /workspaces/testmethodlogy/build/example_ament_cmake_lint/test_results/example_ament_cmake_lint/uncrustify.xunit.xml
Code style divergence in file 'src/add.cpp':
--- src/add.cpp
+++ src/add.cpp.uncrustify
@@ -8 +8 @@
-int add(int a, int b) {return a
+ b;}
+int add(int a, int b) {return a + b;}
1 files with code style divergence
No code style divergence in file 'include/example_ament_cmake_lint/add.hpp'
-- run_test.py: return code 1
-- run_test.py: verify result file '/workspaces/testmethodlogy/build/example_ament_cmake_lint/test_results/example_ament_cmake_lint/uncrustify.xunit.xml'
Copyright © Fixstars Group
104
実行例(2/2) >>> build/example_ament_cmake_lint/test_results/example_ament_cmake_lint/uncrustify.xunit.xml: 2 tests, 0 errors, 1 failure, 0 skipped - example_ament_cmake_lint.uncrustify src/add.cpp <<< failure message Diff with 5 lines >>> Summary: 6 tests, 0 errors, 2 failures, 0 skipped Copyright © Fixstars Group 105
● ROS2でのlint ● ament_cmake での使い方 ● ament_python での使い方 lint Copyright © Fixstars Group 106
ament_pythonでの使い方 ⚫ 基本的に pytest で実装する ○ lintパッケージはPythonライブラリとして使用できる ■ main関数を利用 package.xml <test_depend>ament_xmllint</test_depend> <test_depend>ament_flake8</test_depend> <test_depend>python3-pytest</test_depend> Copyright © Fixstars Group 107
xmllintのテスト test/test_fxmllint.py from ament_xmllint.main import main import pytest @pytest.mark.xmllint @pytest.mark.linter def test_xmllint(): rc = main(argv=['.']) assert rc == 0, 'Found errors' Copyright © Fixstars Group 108
flake8のテスト test/test_flake8.py from ament_flake8.main import main_with_errors import pytest flake8だけ main_with_errors があるの でそちらを使用 (foxy以降) @pytest.mark.flake8 @pytest.mark.linter def test_flake8(): rc, errors = main_with_errors(argv=[]) assert rc == 0, ¥ 'Found %d code style errors / warnings:¥n' % len(errors) + ¥ '¥n'.join(errors) Copyright © Fixstars Group 109
実行例
$ colcon test --packages-select example_ament_python_lint
...
$ colcon test-result --verbose
build/example_ament_python_lint/pytest.xml: 2 tests, 0 errors, 1 failure, 0 skipped
- example_ament_python_lint.test.test_flake8 test_flake8
<<< failure message
AssertionError: Found 1 code style errors / warnings:
./example_ament_python_lint/__init__.py:1:1: F401 'os' imported but unused
assert 1 == 0
>>>
Summary: 2 tests, 0 errors, 1 failure, 0 skipped
Copyright © Fixstars Group
110
サンプルアプリでのテ スト実装 Copyright © Fixstars Group
● 概要 サンプルアプリでのテスト 実装 ● cpp_calc ● py_accum ● integration Copyright © Fixstars Group 112
サンプルアプリでのテスト実装 ⚫ C++, Python それぞれで簡単なノードを作成しそれらを組み合わせたシステ ムを作る。 ○ ○ ○ C++ → cpp_calc パッケージ Python → py_accum パッケージ 組み合わせる → integration パッケージ ⚫ 各パッケージについて以下の順番で説明 ○ ○ ○ ○ ○ パッケージ概要 パッケージ作成 lintの設定 ロジック実装及び単体テスト ノード実装及び launch integration test Copyright © Fixstars Group cpp_calc/twice node integration py_accum/accum node 113
● 概要 サンプルアプリでのテスト 実装 ● cpp_calc ● py_accum ● integration Copyright © Fixstars Group 114
概要 ⚫ 入力値を2倍にして出力するtwiceノードを実装 ○ ament_cmake/c++を使用 ⚫ 最終的なフォルダ構成 . ├── ├── ├── │ │ │ ├── │ │ │ └── cpp_calc/twice node package.xml CMakeLists.txt include └── cpp_calc ├── twice.hpp └── twice_node.hpp src ├── main.cpp ├── twice.cpp └── twice_node.cpp test ├── launch │ └── twice_node_test.py └── unittest └── twice_test.cpp integration py_accum/accum node Copyright © Fixstars Group 115
パッケージ作成 ros2 pkg create --build-type ament_cmake cpp_calc package.xml <?xml version="1.0"?> <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <package format="3"> <name>cpp_calc</name> <version>0.0.0</version> <description>TODO: Package description</description> <maintainer email="[email protected]">Shuhei Aoki</maintainer> <license>MIT</license> ライセンス設定 <buildtool_depend>ament_cmake_auto</buildtool_depend> 簡易化のためにament_cmake_auto を使用 <depend>rclcpp</depend> <depend>std_msgs</depend> <test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_common</test_depend> <test_depend>ament_cmake_gtest</test_depend> <test_depend>launch_testing_ament_cmake</test_depend> <export> <build_type>ament_cmake</build_type> </export> </package> Copyright © Fixstars Group gtest と launch_testing を追加 116
lintの設定 ⚫ デフォルトでは copyright と cpplint は無効 ⚫ 今回は使用したいので有効化 CMakeLists.txt if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) # the following line skips the linter which checks for copyrights # comment the line when a copyright and license is added to all source files set(ament_cmake_copyright_FOUND TRUE) # the following line skips cpplint (only works in a git repo) # comment the line when this package is in a git repo and when # a copyright and license is added to all source files set(ament_cmake_cpplint_FOUND TRUE) ament_lint_auto_find_test_dependencies() endif() Copyright © Fixstars Group 削除 117
ロジック実装
⚫ ロジックとROS2のノード実装と分離することでロジックの単体テストを
可能とする
include/cpp_calc/twice.hpp
// Copyright (c) 2022 Fixstars inc.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#pragma once
#include <cstdint>
namespace cpp_calc
{
int32_t do_twice(int32_t v);
} // namespace cpp_calc
src/twice.cpp
// Copyright (c) 2022 Fixstars inc.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#include "cpp_calc/twice.hpp"
namespace cpp_calc
{
int32_t do_twice(int32_t v)
{
return v * 2;
}
} // namespace cpp_calc
Copyright © Fixstars Group
118
単体テスト
⚫ この段階で do_twice の単体テストを実行可能
test/unittest/twice_test.cpp
// Copyright (c) 2022 Fixstars inc.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
#include <cpp_calc/twice.hpp>
#include <gtest/gtest.h>
CMakeLists.txt
find_package(ament_cmake_auto REQUIRED)
ament_auto_find_build_dependencies()
ament_auto_add_library(twice_lib
SHARED
src/twice.cpp
)
if(BUILD_TESTING)
ament_auto_find_test_dependencies()
TEST(do_twice, two_sohuld_be_four) {
ASSERT_EQ(cpp_calc::do_twice(2), 4);
}
ament_auto_add_gtest(twice_test
test/unittest/twice_test.cpp
)
endif()
Copyright © Fixstars Group
119
ノード実装
⚫ do_twice を使うノードを作成
include/cpp_calc/twice_node.hpp
#pragma once
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/int32.hpp>
namespace cpp_calc
{
using Int32 = std_msgs::msg::Int32;
class TwiceNode : public rclcpp::Node
{
public:
TwiceNode();
private:
rclcpp::Subscription<Int32>::SharedPtr sub_;
rclcpp::Publisher<Int32>::SharedPtr pub_;
};
} // namespace cpp_calc
src/twice_node.cpp
#include "cpp_calc/twice_node.hpp"
#include "cpp_calc/twice.hpp"
namespace cpp_calc
{
TwiceNode::TwiceNode()
: rclcpp::Node("twice", "")
{
pub_ = this->create_publisher<Int32>("dst", 10);
sub_ = this->create_subscription<Int32>(
"src", 10, [this](const Int32::ConstSharedPtr src) -> void {
auto output = Int32();
output.data = do_twice(src->data);
this->pub_->publish(output);
});
}
}
// namespace cpp_calc
※コピーライトも必要だが長くなるので以降は省略
Copyright © Fixstars Group
120
ノード実装
⚫ do_twice を使うノードを作成
src/main.cpp
#include <rclcpp/rclcpp.hpp>
#include <cpp_calc/twice_node.hpp>
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<cpp_calc::TwiceNode>());
rclcpp::shutdown();
return 0;
}
Copyright © Fixstars Group
CMakeLists.txt
ament_auto_add_library(twice_lib
SHARED
src/twice.cpp
src/twice_node.cpp
)
ament_auto_add_executable(twice
src/main.cpp
)
121
launch integration test (launch_testing) ⚫ これによりノードとしての振舞いをテストできる CMakeLists.txt if(BUILD_TESTING) ament_auto_find_test_dependencies() ament_auto_add_gtest(twice_test test/unittest/twice_test.cpp ) add_launch_test(test/launch/twice_node_test.py) endif() Copyright © Fixstars Group 122
test/launch/twice_node_test.py(1/3) import unittest import time import launch import launch_ros.actions import launch_testing import launch_testing.actions import pytest from rclpy.node import Node import rclpy import rclpy.executors from std_msgs.msg import Int32 @pytest.mark.launch_test def generate_test_description(): return launch.LaunchDescription( [ launch_ros.actions.Node( package="cpp_calc", executable="twice", ), launch_testing.actions.ReadyToTest(), ] ) Copyright © Fixstars Group 123
test/launch/twice_node_test.py(2/3) class TestTwice(unittest.TestCase): @classmethod def setUpClass(cls): rclpy.init() @classmethod def tearDownClass(cls): rclpy.shutdown() def setUp(self): self._node = DummyTestNode() def tearDown(self): self._node.destroy_node() def test_success(self): test_data = Int32(data=1) self._node.publish(test_data) ans = self._node.get_message() self.assertEqual(ans, Int32(data=2)) Copyright © Fixstars Group 124
test/launch/twice_node_test.py(3/3)
class DummyTestNode(Node):
def __init__(self):
super().__init__("test_node")
self._msgs = []
self._pub = self.create_publisher(Int32, "src", 10)
self._sub = self.create_subscription(
Int32, "dst", lambda msg: self._msgs.append(msg), 10
)
def publish(self, data):
self._pub.publish(data)
def get_message(self, timeout_s=5.0):
start_len = len(self._msgs)
executor = rclpy.executors.SingleThreadedExecutor()
executor.add_node(self)
try:
end_time = time.time() + timeout_s
while time.time() < end_time:
executor.spin_once(timeout_sec=0.1)
if start_len != len(self._msgs):
break
assert self._wait_for_connect()
def _wait_for_connect(self, timeout_s=5):
end_time = time.time() + timeout_s
while time.time() < end_time:
cnt = self._pub.get_subscription_count()
if cnt > 0:
return True
time.sleep(0.1)
return False
finally:
executor.remove_node(self)
executor.shutdown()
assert start_len != len(self._msgs)
return self._msgs[-1]
Copyright © Fixstars Group
125
● 概要 サンプルアプリでのテスト 実装 ● cpp_calc ● py_accum ● integration Copyright © Fixstars Group 126
概要 ⚫ 入力値を累積し、累積値を出力する ○ ament_python/Python を使用 ⚫ 最終的なフォルダ構成 . ├── ├── ├── ├── │ │ │ ├── │ └── package.xml setup.cfg setup.py py_accum ├── __init__.py ├── accumulator.py └── accumulator_node.py resource └── py_accum test ├── launch │ └── test_accum.py ├── test_copyright.py ├── test_flake8.py ├── test_pep257.py └── unittest └── test_accumulator.py cpp_calc/twice node integration py_accum/accum node Copyright © Fixstars Group 127
パッケージ作成 ros2 pkg create --build-type ament_python py_accum package.xml <?xml version="1.0"?> <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <package format="3"> <name>py_accum</name> <version>0.0.0</version> <description>TODO: Package description</description> <maintainer email="[email protected]">Shuhei Aoki</maintainer> <license>MIT</license> ライセンス設定 <depend>rclpy</depend> <depend>std_msgs</depend> <test_depend>ament_copyright</test_depend> <test_depend>ament_flake8</test_depend> <test_depend>ament_pep257</test_depend> <test_depend>python3-pytest</test_depend> <test_depend>launch_pytest</test_depend> launch_pytest を追加 <export> <build_type>ament_python</build_type> </export> </package> Copyright © Fixstars Group 128
lintの設定 ⚫ ros2 pkg create の段階で必要なテストは作成されている ⚫ copyright は無効になっているので有効化する test/test_copyright.py from ament_copyright.main import main import pytest 削除 # Remove the `skip` decorator once the source file(s) have a copyright header @pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') @pytest.mark.copyright @pytest.mark.linter def test_copyright(): rc = main(argv=['.', 'test']) assert rc == 0, 'Found errors' Copyright © Fixstars Group 129
ロジック実装 ⚫ ロジックとROS2のノード実装と分離する ⚫ ことでロジックの単体テストを可能とす 単体テスト実装 ○ この段階で Accumulatorクラスのテストが 可能 る py_accum/accumulator.py test/unittest/test_accumulator.py class Accumulator: def __init__(self): self._data = 0 from py_accum.accumulator import Accumulator def test_success(): acc = Accumulator() for i in range(10): acc.add(i) def add(self, v): self._data += v def get(self): return self._data assert acc.get() == 45 Copyright © Fixstars Group 130
ノード実装(1/2) ⚫ py_accum/accumulator_node.py import rclpy from rclpy.node import Node Accumulator を使うノー from std_msgs.msg import Int32 ドを実装 from .accumulator import Accumulator class AccumulatorNode(Node): def __init__(self): super().__init__("accum") self._acc = Accumulator() self._pub = self.create_publisher(Int32, "dst", 10) self._sub = self.create_subscription(Int32, "src", self.callback, 10) def callback(self, msg): self._acc.add(msg.data) self._pub.publish(Int32(data=self._acc.get())) def main(args=None): rclpy.init(args=args) node = AccumulatorNode() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == "__main__": Copyright © Fixstars Group main() 131
ノード実装(2/2) ⚫ エントリーポイントに追加 setup.py entry_points={ "console_scripts": ["accum=py_accum.accumulator_node:main"], }, Copyright © Fixstars Group 132
launch integration test (launch_pytest) ⚫ test/launch/test_accum.py を作成 ⚫ これによりノードとしての振舞いをテストできる Copyright © Fixstars Group 133
test/launch/test_accum.py import launch import launch_ros.actions import pytest import launch_pytest import rclpy from rclpy.node import Node from std_msgs.msg import Int32 import time from threading import Thread @launch_pytest.fixture def generate_test_description(): return launch.LaunchDescription( [ launch_ros.actions.Node( package="py_accum", executable="accum", ), ] ) @pytest.mark.launch(fixture=generate_test_description) def test_accumulation(make_test_node): node = make_test_node for i in range(10): node.publish(Int32(data=i)) time.sleep(0.01) end_time = time.time() + 5 while time.time() < end_time: if len(node.msgs) == 10: break time.sleep(0.1) assert len(node.msgs) == 10 assert node.msgs[-1] == Int32(data=45) @pytest.fixture def make_test_node(): rclpy.init() node = DummyTestNode() node.start() yield node node.destroy_node() rclpy.shutdown() Copyright © Fixstars Group 134
test/launch/test_accum.py class DummyTestNode(Node): def __init__(self): super().__init__("test_node") self._pub = self.create_publisher(Int32, "src", 10) self._sub = self.create_subscription(Int32, "dst", self._msg_received, 10) self.msgs = [] def _msg_received(self, msg): self.msgs.append(msg) def _wait_for_connect(self, timeout_s=5): end_time = time.time() + timeout_s while time.time() < end_time: cnt = self._pub.get_subscription_count() if cnt > 0: return True time.sleep(0.1) return False def start(self): self._ros_spin_thread = Thread( target=lambda node: rclpy.spin(node), args=(self,) ) self._ros_spin_thread.start() self._wait_for_connect() def publish(self, data): self._pub.publish(data) Copyright © Fixstars Group 135
● 概要 サンプルアプリでのテスト 実装 ● cpp_calc ● py_accum ● integration Copyright © Fixstars Group 136
概要 ⚫ twice と accum を組み合わせる → 2倍にして累積する ○ launchファイルとして実装 ⚫ テストでは rosbag をテストデータとして使用する ⚫ 最終的なフォルダ構成 ○ cpp_calc/twice node ament_cmake を使用 . ├── ├── ├── │ └── integration package.xml CMakeLists.txt launch └── integration.launch.yaml test ├── data │ └── testdata.bag └── launch └── test_integration.py py_accum/accum node Copyright © Fixstars Group 137
パッケージ作成 ros2 pkg create --build-type ament_cmake integration package.xml <?xml version="1.0"?> <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <package format="3"> <name>integration</name> <version>1.0.0</version> <description>Integration package</description> <maintainer email="[email protected]">Shuhei Aoki</maintainer> ライセンス設定 <license>MIT</license> <buildtool_depend>ament_cmake_auto</buildtool_depend> <depend>py_accum</depend> <depend>cpp_calc</depend> <test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_common</test_depend> <test_depend>ament_cmake_pytest</test_depend> <test_depend>launch_pytest</test_depend> launch_pytest を追加 <export> <build_type>ament_cmake</build_type> </export> </package> Copyright © Fixstars Group lintの設定は cpp_calc と同様なので省略 138
launchファイル launch/integration.launch.yaml /src launch: - node: pkg: cpp_calc exec: twice remap: - from: /dst to: /calc_to_accum - node: pkg: py_accum exec: accum remap: - from: /src to: /calc_to_accum cpp_calc/twice node /calc_to_accum py_accum/accum node /dst Copyright © Fixstars Group 139
launch integration test (launch_pytest) ⚫ rosbagをテストデータとして使用する ○ 0 から 9 の値が入っている ■ → integration が出力する最終値は 90 になるべき CMakeLists.txt if(BUILD_TESTING) ament_auto_find_test_dependencies() find_package(ament_cmake_pytest) ament_add_pytest_test(pytest test ) endif() Copyright © Fixstars Group 140
test/launch/test_integration.py import pytest import launch_pytest from launch import LaunchDescription from launch.actions import IncludeLaunchDescription, ExecuteProcess from launch.launch_description_sources import AnyLaunchDescriptionSource from ament_index_python import get_package_share_directory from pathlib import Path import rclpy import rclpy.node from std_msgs.msg import Int32 from threading import Thread Copyright © Fixstars Group 141
test/launch/test_integration.py @pytest.fixture def integration_launch(): return IncludeLaunchDescription( launchファイルを起動する action AnyLaunchDescriptionSource( str( Path(get_package_share_directory("integration")) / "launch" / "integration.launch.yaml" @pytest.fixture ) def testdata(): ) path_to_test = Path(__file__).parent.parent ) ros2 bag play を実行する action • 起動してから1秒待ってから再生開始さ せる(humble以降) • 送ったデータが全て受信されたことを 確認してから終了する(humble以降) return ExecuteProcess( cmd=[ "ros2", "bag", "play", str(path_to_test / "data" / "testdata.bag"), "--delay", "1", "--wait-for-all-acked", "1000", ], shell=True, ) Copyright © Fixstars Group 142
test/launch/test_integration.py @pytest.fixture def receiver(): rclpy.init() node = ReceiverNode() node.start() yield node node.destroy_node() rclpy.shutdown() class ReceiverNode(rclpy.node.Node): def __init__(self): super().__init__("test_receiver") self.msgs = [] self._sub = self.create_subscription( Int32, "dst", lambda msg: self.msgs.append(msg), 10 ) def start(self): self._ros_spin_thread = Thread( target=lambda node: rclpy.spin(node), args=(self,) ) self._ros_spin_thread.start() Copyright © Fixstars Group 143
test/launch/test_integration.py @launch_pytest.fixture def generate_test_description(testdata, integration_launch): return LaunchDescription( [ testdata, integration_launch, ] ) LaunchDescription を作成 • testdata と integration_launch は fixture のもの • 2つ前のページで定義した関数の結果が得られる @pytest.mark.launch(fixture=generate_test_description) async def test_should_be_90(testdata, receiver): ros2 bag play のプロセスを得られる。 await testdata.get_asyncio_future() yield await testdata.get_asyncio_future() でプロセス終了待ち、つまりテスト assert receiver.msgs[-1] == Int32(data=90) データ再生終了待ちができる Copyright © Fixstars Group 144
test/launch/test_integration.py each test cases @launch_pytest.fixture def generate_test_description(testdata, integration_launch): return LaunchDescription( [ testdata, integration_launch, ] ) @pytest.mark.launch(fixture=generate_test_description) async def test_should_be_90(testdata, receiver): await testdata.get_asyncio_future() yield assert receiver.msgs[-1] == Int32(data=90) yield で一旦 launch が終了する。 launchが終了した後、yield 以降が実行される → launch終了後の状態を得られる launch PUT set up test_xxx shutdown PUT post test_xxx tear down Copyright © Fixstars Group 145
その他TIPS Copyright © Fixstars Group
TIPS ⚫ ラベルとマーカー ⚫ テスト時のROS_DOMAIN_ID ⚫ CIでの自動テスト Copyright © Fixstars Group 147
ラベルとマーカー ⚫ ctest のラベルと pytest のマーカーを使うことで実行するテストを絞ること ができる ○ gtest だけ実行したい、lint だけ実行したい、といったケースに活用できる ⚫ ament_cmake では ament_add_test_label で ctest ラベルを付与 ○ テストフレームワーク毎に自動で設定されているものもある ⚫ pytest のマーカーの例 ⚫ コマンド例 @pytest.mark.flake8 # ← flake8 マーカー @pytest.mark.linter # ← linter マーカー def test_flake8(): ... # ctest colcon test --ctest-args -L gtest # pytest colcon test --pytest-args -m copyright Copyright © Fixstars Group 148
ROS_DOMAIN_ID ⚫ これまで紹介したテストは、1つ1つ独立して実行する場合問題ないが、同時 に実行すると問題が出るケースがある ○ launch integration test ではトピック通信を扱っていたため、トピック名が重複すると通信が 混線する ■ 確率で失敗する、という状態になる ⚫ この問題を回避するために ROS_DOMAIN_ID を使用したい ○ → しかしどのようにROS_DOMAIN_IDを設定するか? Copyright © Fixstars Group 149
ROS_DOMAIN_ID ⚫ domain_coordinatorを使用する ○ https://github.com/ros2/ament_cmake_ros/tree/rolling/domain_coordinator ○ 重複しないように ROS_DOMAIN_ID を選択する Python パッケージ ○ 実装としてはソケットによるポートロックを応用したもので、使われている ROS_DOMAIN_IDを検知しているわけではないことに注意 Copyright © Fixstars Group 150
ROS_DOMAIN_ID
⚫ ament_cmake の場合
○
https://github.com/ros2/ament_cmake_ros/tree/rolling/ament_cmake_ros を使う
■
内部的に domain_coordinator を使用
■
gtest, gmock, pytest は専用の cmakeマクロがあるが、launch_testing にはないので、
launch_testing の場合 runner スクリプトだけ拝借する形になる
実行例
CMakeLists.txt
# 要 <test_depend>ament_cmake_ros</test_depend>
find_package(ament_cmake_ros REQUIRED)
add_launch_test(test/test_twice_launch.py
RUNNER "${ament_cmake_ros_DIR}/run_test_isolated.py"
)
$ colcon test --packages-select
example_launch_testing_ament_cmake --event-handlers
console_direct+
...
1: Running with ROS_DOMAIN_ID 1
...
Copyright © Fixstars Group
151
ROS_DOMAIN_ID ⚫ ament_python の場合 ○ domain_coordinator をPythonライブラリとして使う形になりそう ○ 以下のようなコードを最初に書く test/launch/test_twice_launch.py import domain_coordinator import contextlib import os stack = contextlib.ExitStack() if "ROS_DOMAIN_ID" not in os.environ and "DISABLE_ROS_ISOLATION" not in os.environ: domain_id = stack.enter_context(domain_coordinator.domain_id()) os.environ["ROS_DOMAIN_ID"] = str(domain_id) Copyright © Fixstars Group 152
CIでの自動テスト ⚫ コードをコミットし、リモートリポジトリにプッシュする毎に自動的にテス トが実行されるようになると、継続的な検証が可能になる ⚫ ここでは GitLab CI/CDでの一例を示す ⚫ CIで行うこと ○ ビルド/テスト環境の構築 ○ ビルド/テスト Copyright © Fixstars Group 153
CIでの自動テスト ⚫ ビルド/テスト環境の構築 ○ 使用しているDockerfileをビルドしてイメージを GitLab のレジストリに登録 .gitlab-ci.yml (1/2) docker: image: docker:latest variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "/certs" services: - docker:dind stage: build script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -t $CI_REGISTRY/tech/ros2-seminar/test-methodology/devenv:latest docker - docker push $CI_REGISTRY/tech/ros2-seminar/test-methodology/devenv:latest Copyright © Fixstars Group 154
CIでの自動テスト ⚫ ビルド/テストの実行 ○ 作ったイメージを使ってビルドとテストを実行 ○ CIの結果として、buildとinstall、logディレクトリを artifacts に指定 .gitlab-ci.yml (2/2) test: image: $CI_REGISTRY/tech/ros2-seminar/test-methodology/devenv:latest variables: PYTHONWARNINGS: ignore:::setuptools.command.install,ignore:::setuptools.command.easy_install script: - source /opt/ros/$ROS_DISTRO/setup.bash - colcon build - colcon test - colcon test-result --verbose artifacts: paths: - build - install - log expire_in: 1 week Copyright © Fixstars Group 155
CIでの自動テスト ⚫ 実行例(抜粋) GitLabの画面より Copyright © Fixstars Group 156
参考文献 Copyright © Fixstars Group
参考文献 • colcon • https://colcon.readthedocs.io/en/released/index.html • https://github.com/colcon/colcon-core • https://github.com/colcon/colcon-cmake • ament • https://github.com/colcon/colcon-ros • https://github.com/ament/ament_cmake • https://github.com/ros2/ament_cmake_ros • https://github.com/ros2/launch • test • https://cmake.org/cmake/help/latest/manual/ctest.1.html • https://cmake.org/cmake/help/latest/command/add_test.html • https://github.com/google/googletest • http://opencv.jp/googletestdocs/primer.html • http://opencv.jp/googlemockdocs/fordummies.html • https://docs.pytest.org/en/stable • https://docs.python.org/ja/3/library/unittest.html • lint • https://github.com/ament/ament_lint • https://zenn.dev/yhay81/articles/yhay81-202102-pythonlint • https://siderlabs.com/blog/ja/python-lint-pickup-5tools/ • その他 • https://docs.gitlab.com/ee/ci/ Copyright © Fixstars Group 158
Thank you! お問い合わせ窓口 : [email protected] Copyright © Fixstars Group