Slide 1

Slide 1 text

Copyright © Fixstars Group ROS2自律走行実現に向けて 次世代ロボット開発フレームワーク ROS2のテストフレームワーク徹底理解

Slide 2

Slide 2 text

Copyright © Fixstars Group 本日のAgenda ⚫ はじめに ⚫ フィックスターズのご紹介 ⚫ テストの必要性 ⚫ ROS2で可能なテスト/lint ⚫ サンプルアプリでのテスト実装 ⚫ その他 TIPS 2

Slide 3

Slide 3 text

Copyright © Fixstars Group はじめに

Slide 4

Slide 4 text

Copyright © Fixstars Group 本セミナーの位置づけ ⚫ ウェビナー「ROS2自律走行実現に向けて」シリーズでは、 自律走行する車両型ロボットの実現に向け、 ROS2を使った開発に関連する、様々な情報を発信しています ⚫ vol.1(発表資料) ○ ROS1上で開発した資産の活用 ○ 自己位置推定パッケージの CUDA高速化 ⚫ vol.2(発表資料) ○ パッケージ開発に欠かせないビルドシステムの解説 ⚫ 今回の内容 ○ 品質向上に欠かせないテストフレームワークの解説 ⚫ こんな方に向いています ○ ROS2 の開発をしたことがあり、ソフトウェアの品質を向上させたいとお考えの方 4

Slide 5

Slide 5 text

Copyright © Fixstars Group 発表者紹介 5 冨田 明彦 ソリューションカンパニー 執行役員 2008年に入社。金融、医療業界において、ソ フトウェア高速化業務に携わる。その後、新規 事業企画、半導体業界の事業を担当し、現職。 青木 修平 ソリューション第3事業部 シニアエンジニア 2018年に入社。主に ADAS 向けの画像処理ア ルゴリズムの開発や高速化、シミュレーション 環境構築を担当。

Slide 6

Slide 6 text

Copyright © Fixstars Group フィックスターズの ご紹介

Slide 7

Slide 7 text

Copyright © Fixstars Group フィックスターズの強み コンピュータの性能を最大限に引き出す、ソフトウェア高速化のエキスパート集団 ハードウェアの知見 アルゴリズム実装力 各産業・研究分野の知見 7 目的の製品に最適なハードウェアを見抜き、 その性能をフル活用するソフトウェアを開 発します。 ハードウェアの特徴と製品要求仕様に合わ せて、アルゴリズムを改良して高速化を実 現します。 開発したい製品に使える技術を見抜き、実 際に動作する実装までトータルにサポート します。

Slide 8

Slide 8 text

Copyright © Fixstars Group サービス提供分野 8 半導体 自動車 産業機器 生命科学 金融 ●NAND型フラッシュメモリ向けフ ァームウェア開発 ●次世代AIチップの開発環境基盤 ●自動運転の高性能化、実用化 ●次世代パーソナルモビリティの 研究開発 ●Smart Factory実現への支援 ●マシンビジョンシステムの高速化 ●ゲノム解析の高速化 ●医用画像処理の高速化 ●AI画像診断システムの研究開発 ●デリバティブシステムの高速化 ●HFT(アルゴリズムトレード)の高速化

Slide 9

Slide 9 text

Copyright © Fixstars Group 自動車向けソフトウェア開発 アルゴリズム開発から量産車ターゲット向けの高速化まで、 自動運転の実現に向けた統合的な技術開発を行っています。 ご支援内容 9

Slide 10

Slide 10 text

Copyright © Fixstars Group 組込み高速化 組込み機器製品の計算処理実装をお手伝いしています。 お客様の課題 組込みシステムの目標性能が達成できない ターゲットデバイスの特性に合わせて、 性能要求を満たしたい 安価なハードウェアでも処理速度を維持し 製品にかかるコストを下げたい ターゲットデバイスの例 ARM/ TOSHIBA Visconti/ Renesas R-Car/ NXP S32/ Automotive Platform/ CEVA-XM6/ Texas Instruments C6000/ Cadence Vision DSP Family など システム設計コンサルティング ハードウェア選定を含めたシステム設計のご提案 アルゴリズムの改善と移植 既存アルゴリズムを改善して計算を高速化 組込みアルゴリズム開発 ターゲットデバイス向けに最適化されたアルゴリズムを実装 ご支援内容 最適化方針のご提案 ボトルネック調査、最適化に向けた検討 10

Slide 11

Slide 11 text

Copyright © Fixstars Group サービス領域一覧 様々な領域でソフトウェア高速化サービスを提供しています。大量データの高速処理は、 お客様の製品競争力の源泉となっています。 11 組込み高速化 画像処理・アルゴリズム 開発 分散並列システム開発 GPU向け高速化 FPGAを活用した システム開発 量子コンピューティング AI・深層学習 自動車向け ソフトウェア開発 フラッシュメモリ向けフ ァームウェア開発

Slide 12

Slide 12 text

Copyright © Fixstars Group ROS2の テストフレームワーク 徹底理解

Slide 13

Slide 13 text

Copyright © Fixstars Group 本セミナーのねらい ⚫ ソフトウェアの品質向上にはテストは欠かせないが、ROS2ではどういった テストが可能なのか、どのように実現されているか、どのように利用すれば 良いかの情報が少ない ⚫ そこで、本セミナーではサンプルを交えつつ、ROS2におけるテストについ て網羅的な解説を行う ⚫ サンプルは ROS2 Humble で動作確認済み 13

Slide 14

Slide 14 text

Copyright © Fixstars Group アジェンダ ⚫ テストとは ⚫ ROS2で可能なテスト/lint ⚫ サンプルアプリでのテスト実装 ⚫ その他TIPS 14

Slide 15

Slide 15 text

Copyright © Fixstars Group テストとは

Slide 16

Slide 16 text

Copyright © Fixstars Group テストとは ⚫ ソフトウェアが正しく作られているか確認する作業 ⚫ テストの分類 ○ 機能テスト ■ 単体テスト、結合テスト、システムテスト、etc. ○ 非機能テスト ■ パフォーマンステスト、ストレステスト、保守性テスト、etc. ⚫ テストがないと何が困る? ○ 関数やシステムが正しく動作しているか確認できない ⚫ とはいえ、手作業でテストを行うのは大変 ○ → テストフレームワークを使って自動化したい 16

Slide 17

Slide 17 text

Copyright © Fixstars Group テストとは ⚫ ソフトウェアが正しく作られているか確認する作業 ⚫ テストの分類 ○ 機能テスト ■ 単体テスト、結合テスト、システムテスト、etc. ○ 非機能テスト ■ パフォーマンステスト、ストレステスト、保守性テスト、etc. ⚫ テストがないと何が困る? ○ 関数やシステムが正しく動作しているか確認できない ⚫ とはいえ、手作業でテストを行うのは大変 ○ → テストフレームワークを使って自動化したい 17 ROS2におけるテスト/テストフレームワークの使い方を解説する

Slide 18

Slide 18 text

Copyright © Fixstars Group ROS2でのテストの実行方法 ⚫ ros2 では colcon test でテストを実行する ○ テストに成功した場合 18

Slide 19

Slide 19 text

Copyright © Fixstars Group ROS2でのテストの実行方法 ⚫ ros2 では colcon test でテストを実行する ○ テストに失敗した場合 19

Slide 20

Slide 20 text

Copyright © Fixstars Group ROS2でのテストの実行方法 ⚫ colcon test-result で成否結果を得られる 20 終了ステータスが非ゼロであれば 失敗

Slide 21

Slide 21 text

Copyright © Fixstars Group ROS2でのテストの実行方法 ⚫ colcon test-result で成否結果を得られる 21 XML形式でレポートを得られる

Slide 22

Slide 22 text

Copyright © Fixstars Group ROS2でのテストの実行方法 ⚫ 詳細を見る時は colcon test-result --verbose 22 …

Slide 23

Slide 23 text

Copyright © Fixstars Group 23 …

Slide 24

Slide 24 text

Copyright © Fixstars Group ROS2で可能なテスト /lint

Slide 25

Slide 25 text

Copyright © Fixstars Group ROS2で可能なテスト/lint ⚫ 公式で提供されているもの 25 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 拡張が存在

Slide 26

Slide 26 text

Copyright © Fixstars Group ROS2で可能なテスト/lint ⚫ 一覧(解説するもの) 26 ament_cmake add_test ament_cmake_test ament_cmake_gtest ament_cmake_gmock ament_cmake_pytest ament_python pytest unittest launch integration test launch_testing launch_pytest lint (その2) ament_clang_tidy ament_cppcheck ament_cpplint ament_pclint ament_flake8 ament_mypy ament_pep257 ament_pycodestyle ament_pyflakes lint (その1) ament_xmllint ament_lint_cmake ament_copyright ament_clang_format ament_uncrustify

Slide 27

Slide 27 text

Copyright © Fixstars Group ROS2で可能なテスト/lint ⚫ 公式で提供されているもの 27 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 拡張が存在

Slide 28

Slide 28 text

Copyright © Fixstars Group 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年で更新が止まっているので解説を省略 28

Slide 29

Slide 29 text

Copyright © Fixstars Group ament_cmakeでのテスト ● add_test ● ament_cmake_test ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest 29

Slide 30

Slide 30 text

Copyright © Fixstars Group 概要 ⚫ CMakeに付属しているテストフレームワークである ctest にテストを登録 ○ https://cmake.org/cmake/help/latest/manual/ctest.1.html ○ https://cmake.org/cmake/help/latest/command/add_test.html ⚫ ament_cmake (純粋なCMakeも) において colcon test は ctest の実行となる ○ 以降の全ての ament_cmake 向けのテストは最終的に add_test の呼び出しに繋がっている ○ 任意のテストを作りたい場合も add_test を使えば実行されるようになる ■ とはいえ、あまり使う場面はなさそう ⚫ CMake記述例 # 成功 add_test( NAME test_success COMMAND bash -c "exit 0" ) # 失敗 add_test( NAME test_failure COMMAND bash -c "exit 1" ) 合否判定は単純で、プログラムの終了コードを見ている • 0 → 成功 • 0 以外 → 失敗 30

Slide 31

Slide 31 text

Copyright © Fixstars Group 実行例 $ 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 0.00 sec 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 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 … 31 ←成功 ←失敗

Slide 32

Slide 32 text

Copyright © Fixstars Group ament_cmakeでのテスト ● add_test ● ament_cmake_test ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest 32

Slide 33

Slide 33 text

Copyright © Fixstars Group 概要 ⚫ ament_cmake が提供するテスト用のマクロ ○ https://github.com/ament/ament_cmake/blob/rolling/ament_cmake_test/cmake/ament_add_test.cmake ○ 自前でテストを実装したい時以外は使う機会はなさそう ⚫ 最終的には add_test の呼び出しになるが、追加の設定やオプションがある ○ テスト実行を打ち切るまでのタイムアウト (デフォルト60秒) ○ テストをスキップするかどうかの選択 ○ etc. (詳しくは CMakeマクロのドキュメントコメントを参照) 33 ← bash ベースのテストフレームワークの bats を使用

Slide 34

Slide 34 text

Copyright © Fixstars Group CMake記述例 ⚫ bash ベースのテストフレームワークである bats を使用 34 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 $ ) CMakeLists.txt

Slide 35

Slide 35 text

Copyright © Fixstars Group 実行例 $ 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-by- one/03_example_ament_cmake_test/test': - bats -F junit -o /workspaces/test- methodlogy/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/test- methodlogy/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 35

Slide 36

Slide 36 text

Copyright © Fixstars Group C++でテストを記述する場合 add_executable(test_executable test/test.cpp ) ament_add_test(test_doctest COMMAND $ ) CMakeの方法の則って実行ファイルを 作る 実行ファイルを COMMAND として渡す。 $は Generator Expressions で、 test_executable へのパスを得られる。 36 CMakeLists.txt

Slide 37

Slide 37 text

Copyright © Fixstars Group ament_cmakeでのテスト ● add_test ● ament_cmake_test ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest 37

Slide 38

Slide 38 text

Copyright © Fixstars Group 概要 ⚫ 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++コードをテストする際はおおよそこれを使うことになる 38

Slide 39

Slide 39 text

Copyright © Fixstars Group CMake記述例 39 add_library(myadd SHARED src/add.cpp ) target_include_directories(myadd PUBLIC $ ) 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() ライブラリ myadd を作成 myadd をテストする myadd_gtest を作成

Slide 40

Slide 40 text

Copyright © Fixstars Group テストの記述例 40 #pragma once int add(int a, int b); #include #include TEST(add, success) { ASSERT_EQ(add(1, 2), 3); } TEST(add, failure) { ASSERT_EQ(add(1, 2), 4); } example_ament_cmake_gtest/add.hpp test/add_test.cpp 成功するテスト add(1, 2) を呼び出した時の結果は3になるべき 失敗するテスト add(1, 2) を呼び出した時の結果は4になるべき ※テストに失敗した時の表示を見るためのもので実際はこのようなテストは書かない テストの書き方はgtestの作法に従う

Slide 41

Slide 41 text

Copyright © Fixstars Group 実行例(1/2) 41 $ 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/test- methodlogy/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)

Slide 42

Slide 42 text

Copyright © Fixstars Group 実行例(2/2) 42 [----------] 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/test- methodlogy/build/example_ament_cmake_gtest/test_results/example_ament_cmake_gtest/myadd_gtest.gtest.xml' -- run_test.py: verify result file '/workspaces/test- methodlogy/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

Slide 43

Slide 43 text

Copyright © Fixstars Group ament_cmakeでのテスト ● add_test ● ament_cmake_test ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest 43

Slide 44

Slide 44 text

Copyright © Fixstars Group 概要 ⚫ 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++でモックを使ったテストをする際はだいたいこれを使うことに なる 44

Slide 45

Slide 45 text

Copyright © Fixstars Group CMake記述例 45 add_library(mylib SHARED src/foo.cpp ) target_include_directories(mylib PUBLIC $ ) 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()

Slide 46

Slide 46 text

Copyright © Fixstars Group テストの記述例 46 #pragma once class Foo { public: virtual bool func(int a) = 0; }; bool bar(Foo * foo); example_ament_cmake_gmock/foo.hpp #include "example_ament_cmake_gmock/foo.hpp" bool bar(Foo *foo) { return foo->func(0); } src/foo.cpp テスト対象は 関数 bar bar は、 1. クラス Foo のインスタンス fooを引数として取り 2. 0 を引数として foo->func を呼び出し 3. foo->func の返り値を bar の返り値として返す bar のテストで確認したいのは、 1. func が呼び出されていること 2. その引数が0であること 3. func が true を返した時、 bar も true を返すこと

Slide 47

Slide 47 text

Copyright © Fixstars Group テストの記述例 47 #include #include class MockFoo : public Foo { public: MOCK_METHOD1(func, bool(int)); }; TEST(bar, success) { using namespace testing; MockFoo mock_foo; // mock_fooの使われ方の想定は、 EXPECT_CALL(mock_foo, func(0)) // 0を引数としてfuncが呼び出されること .Times(1) // 1度だけ呼ばれること .WillOnce(Return(true)); // funcはtrueを返す EXPECT_EQ(bar(&mock_foo), true); } test/foo_mock.cpp モッククラスの作成 モックオブジェクトを設定 テスト テストの書き方は gmockの作法に従う

Slide 48

Slide 48 text

Copyright © Fixstars Group 実行例(1/2) 48 $ 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 [==========] Running 2 tests from 1 test suite. [----------] Global test environment set-up. [----------] 2 tests from bar [ 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) 失敗するケースも追加 EXPECT_CALL(mock_foo, func(0)) .Times(2) // 2回呼ばれること .WillRepeatedly(Return(true));

Slide 49

Slide 49 text

Copyright © Fixstars Group 実行例(2/2) 49 [----------] 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/test- methodlogy/build/example_ament_cmake_gmock/test_results/example_ament_cmake_gmock/my_ament_cmake_gmock.gtest.xml' -- run_test.py: verify result file '/workspaces/test- methodlogy/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

Slide 50

Slide 50 text

Copyright © Fixstars Group ament_cmakeでのテスト ● add_test ● ament_cmake_test ● ament_cmake_gtest ● ament_cmake_gmock ● ament_cmake_pytest 50

Slide 51

Slide 51 text

Copyright © Fixstars Group 概要 ⚫ 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 によるテストを混 ぜたい時に使う 51

Slide 52

Slide 52 text

Copyright © Fixstars Group CMake記述例 52 find_package(ament_cmake_pytest) ament_add_pytest_test(example_pytest test ) . ├─ package.xml ├─ CMakeLists.txt ├─ test │ └─ test_add.py ... ファイルかディレクトリを指定 python3 -u -m pytest ${CMAKE_CURRENT_SOURCE_DIR}/test … が実行される ↓ディレクトリ構成

Slide 53

Slide 53 text

Copyright © Fixstars Group テストの記述例 53 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 test/test_add.py def add(a, b): return a + b example_ament_cmake_pytest/add.py

Slide 54

Slide 54 text

Copyright © Fixstars Group 実行例(1/2) ⚫ 実行例 54 $ 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 --junit- xml=/workspaces/test-methodlogy/build/example_ament_cmake_pytest/test_results/example_ament_cmake_pytest/example_pytest.xunit.xml --junit- prefix=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 _________________________________

Slide 55

Slide 55 text

Copyright © Fixstars Group 実行例(2/2) ⚫ 実行例 55 def test_failure(): # 失敗するテスト > assert add(1, 2) == 4 E assert 3 == 4 E + where 3 = add(1, 2) ../../src/one-by-one/09_example_ament_cmake_pytest/test/test_add.py:10: AssertionError - generated xml file: /workspaces/test- methodlogy/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/test- methodlogy/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

Slide 56

Slide 56 text

Copyright © Fixstars Group ROS2で可能なテスト/lint ⚫ 公式で提供されているもの 56 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 拡張が存在

Slide 57

Slide 57 text

Copyright © Fixstars Group ament_pythonでのテスト ● ament_pythonでのテスト ● pytest ● unittest 57

Slide 58

Slide 58 text

Copyright © Fixstars Group 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 オプションで指定可能 58 # 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

Slide 59

Slide 59 text

Copyright © Fixstars Group ament_pythonでのテスト ● ament_pythonでのテスト ● pytest ● unittest 59

Slide 60

Slide 60 text

Copyright © Fixstars Group 概要 ⚫ pytest は python 向けのテストフレームワーク ○ https://docs.pytest.org/en/stable/ ○ 標準ライブラリではないが現在主流な模様 ⚫ 必要な準備 60 from setuptools import setup package_name = 'example_ament_python_pytest' setup( name=package_name, ... tests_require=['pytest'], ... ) setup.py tests_require に pytest を追加

Slide 61

Slide 61 text

Copyright © Fixstars Group テストの記述例 ⚫ テスト対象の関数 ⚫ テスト 61 def add(a, b): return a + b example_ament_python_pytest/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 test/test_add.py

Slide 62

Slide 62 text

Copyright © Fixstars Group 実行例 62 $ 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

Slide 63

Slide 63 text

Copyright © Fixstars Group ament_pythonでのテスト ● ament_pythonでのテスト ● pytest ● unittest 63

Slide 64

Slide 64 text

Copyright © Fixstars Group 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 の終了コードが非ゼロになる 64

Slide 65

Slide 65 text

Copyright © Fixstars Group ament_python - unittest 65 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) test/test_add.py

Slide 66

Slide 66 text

Copyright © Fixstars Group 実行例 66 $ 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

Slide 67

Slide 67 text

Copyright © Fixstars Group ROS2で可能なテスト/lint ⚫ 公式で提供されているもの 67 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 拡張が存在

Slide 68

Slide 68 text

Copyright © Fixstars Group launch integration test ● launch integration test とは ● launch_testing ● launch_pytest 68

Slide 69

Slide 69 text

Copyright © Fixstars Group launch integration test とは ⚫ launch システムを使ったテストを作成できるフレームワーク ⚫ 機能(リポジトリ説明より※) ○ テストで実行する全てのプロセスの終了コードの取得 ○ プロセスが正常終了したかの確認、あるいは特定の終了コードで終わったかの確認 ○ プロセスが意図せず死んだ時にテストを失敗させることができる ○ 全てのプロセスの標準出力/標準エラーを取得 ○ 任意のコマンドラインをテストに使用できる ○ テスト自体はプロセスの起動と並行に実行でき、実行中のプロセスとやりとりできる ⚫ Python で作られている ⚫ launch_testing と launch_pytest がある 69 ※ https://github.com/ros2/launch/tree/rolling/launch_testing

Slide 70

Slide 70 text

Copyright © Fixstars Group launch integration test とは ⚫ 処理の概要(例) 70 Test System PUT launch publish subscribe test shutdown process under test ROS2のノードなど

Slide 71

Slide 71 text

Copyright © Fixstars Group launch integration test ● launch integration test とは ● launch_testing ● launch_pytest 71

Slide 72

Slide 72 text

Copyright © Fixstars Group 概要 ⚫ unittest ベースとした launch integration test のフレームワーク ○ https://github.com/ros2/launch/tree/rolling/launch_testing ⚫ 例題 ○ Int32 の値をサブスクライブし、2倍にしてパブリッシュする twice ノードをテストする ○ やりたいのは、 ■ twice ノードを起動する ■ テスト用のノードを作り、 Int32 の値を送る ■ twice ノードから Int32 の値を受け取り、それが送った値の2倍になっているか検証する 72

Slide 73

Slide 73 text

Copyright © Fixstars Group テストの記述例(launch部分) 73 @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(), ]) generate_test_description という関数を定義する。 記述方法は ROS2 の launch(python版)と同じ。

Slide 74

Slide 74 text

Copyright © Fixstars Group テストの記述例(launch部分) 74 @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 ノードを起動 するアクション テスト用のアクション

Slide 75

Slide 75 text

Copyright © Fixstars Group テストの記述例(テスト用ノード(1/2)) 75 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 ) 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 ... /src に送って、 /dst から受け取る 接続されている subscription の数を数えることで、接続が 確立されたことを確認する

Slide 76

Slide 76 text

Copyright © Fixstars Group テストの記述例(テスト用ノード(2/2)) 76 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 finally: executor.remove_node(self) executor.shutdown() assert start_len != len(self._msgs) return self._msgs[-1] Executor の spin_onceを使い、 受信データをポーリング

Slide 77

Slide 77 text

Copyright © Fixstars Group テストの記述例(unittest部分) 77 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)) def test_success2(self): ... setUpClass setUp test_xxx tearDown tearDownClass launch PUT shutdown PUT each test cases

Slide 78

Slide 78 text

Copyright © Fixstars Group テストの記述例(unittest部分) 78 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)) def test_success2(self): ... setUpClass setUp test_xxx tearDown tearDownClass launch PUT shutdown PUT each test cases

Slide 79

Slide 79 text

Copyright © Fixstars Group 実行例 79 $ 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-by- one/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 コマンドラインツールの launch_test を使用

Slide 80

Slide 80 text

Copyright © Fixstars Group ament_cmakeから使う場合 ⚫ launch_testing_ament_cmake を利用する ⚫ CMake記述例 80 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形式のレポート作成機能があるため、それがテストの結果として得られる。

Slide 81

Slide 81 text

Copyright © Fixstars Group ament_pythonから使う場合 ⚫ package.xml の test_depend に launch_testing を追加する ⚫ generate_test_description に @pytest.mark.launch_test をデコレータとして 付ける ⚫ ファイル名を test_xxx.py か xxx_test.py (pytest のルール)にする 81

Slide 82

Slide 82 text

Copyright © Fixstars Group launch integration test ● launch integration test とは ● launch_testing ● launch_pytest 82

Slide 83

Slide 83 text

Copyright © Fixstars Group 概要 ⚫ pytest ベースとした launch integration test のフレームワーク ○ humble で導入された ○ https://github.com/ros2/launch/tree/rolling/launch_pytest ⚫ launch_testing との違いは何か ○ 純粋な pytest 拡張である ■ テストケースを名前でフィルターして実行できる ■ 失敗し得るテストケースというマークを付けられる ■ pytest が提供するエラーレポートの仕組みを使える ○ launch integration test を実現するという目的に違いはない 83

Slide 84

Slide 84 text

Copyright © Fixstars Group テストの記述例(launch部分) 84 @launch_pytest.fixture def generate_test_description(): return launch.LaunchDescription( [ launch_ros.actions.Node( package="example_launch_pytest", executable="twice", ), ] ) launch_testing と同じく launch を 記述する

Slide 85

Slide 85 text

Copyright © Fixstars Group テストの記述例(launch部分) 85 @launch_pytest.fixture def generate_test_description(): return launch.LaunchDescription( [ launch_ros.actions.Node( package="example_launch_pytest", executable="twice", ), ] ) @launch_pytest.fixture デコレータ をつける 関数名は何でも良い(明示的に指定するため) launch_testing.actions.ReadyToTest() は省略可。 なければ自動的に追加される。

Slide 86

Slide 86 text

Copyright © Fixstars Group テストの記述例(テスト用ノード) 86 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) def _msg_received(self, msg): self.msgs.append(msg) self.msg_event_object.set() launch_testing の時と凡そ同じだが、 rclpy.spin を別スレッドで実行するよ うにしている。 また、 Event を使ったスレッドコン トロールも活用

Slide 87

Slide 87 text

Copyright © Fixstars Group テストの記述例(pytest部分) 87 set up test_xxx tear down launch PUT shutdown PUT each test cases @pytest.fixture def make_test_node(): rclpy.init() node = DummyTestNode() node.start() yield node rclpy.shutdown() @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) テストノードを作るフィクスチャ テストケース

Slide 88

Slide 88 text

Copyright © Fixstars Group テストの記述例(pytest部分) ⚫ 実行順 88 set up test_xxx tear down launch PUT shutdown PUT each test cases @pytest.fixture def make_test_node(): rclpy.init() node = DummyTestNode() node.start() yield node rclpy.shutdown() @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)

Slide 89

Slide 89 text

Copyright © Fixstars Group テストの記述例(pytest部分) ⚫ フィクスチャ周り 89 set up test_xxx tear down launch PUT shutdown PUT each test cases @pytest.fixture def make_test_node(): rclpy.init() node = DummyTestNode() node.start() yield node rclpy.shutdown() @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) yield で返されるオブジェクトを得られる 名前指定でフィクスチャが使用される LaunchDescriptionを返すフィクスチャを指定

Slide 90

Slide 90 text

Copyright © Fixstars Group 実行例 90 $ python3 -m pytest test -q --show-capture no ..F [100%] ============================================ FAILURES ============================================= __________________________________________ test_failure ___________________________________________ make_test_node = @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 pytest で実行可能

Slide 91

Slide 91 text

Copyright © Fixstars Group 使用方法 ⚫ ament_cmake から使う場合 ○ pytest 拡張であるため ament_cmake_pytest を併用する ⚫ ament_pythonから使う場合 ○ package.xml の test_depend に launch_pytest を追加する 91 find_package(ament_cmake_pytest) ament_add_pytest_test(pytest test ) CMakeLists.txt ament_cmake_pytest launch_pytest package.xml

Slide 92

Slide 92 text

Copyright © Fixstars Group ROS2で可能なテスト/lint ⚫ 公式で提供されているもの 92 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 拡張が存在

Slide 93

Slide 93 text

Copyright © Fixstars Group lint ● ROS2でのlint ● ament_cmake での使い方 ● ament_python での使い方 93

Slide 94

Slide 94 text

Copyright © Fixstars Group lintとは ⚫ 静的解析ツールを意味する ○ コードフォーマットの確認 ○ バグの原因になりそうな部分の検知(未使用変数であったり) ○ etc. ⚫ ROS2では以下の形で提供されている ○ Pythonライブラリ ○ コマンドラインツール ○ ament_cmake 拡張 94

Slide 95

Slide 95 text

Copyright © Fixstars Group 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 Pyflakesによるスタイルチェック 95 共通 C/C++向け Python向け

Slide 96

Slide 96 text

Copyright © Fixstars Group 共通 ⚫ これらは必要に応じて入れれば良く、lint同士が競合するということはない 96 名前 ターゲット 概要 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

Slide 97

Slide 97 text

Copyright © Fixstars Group C/C++ ⚫ コードフォーマット。排他的でどちらか1つ使用する ⚫ その他静的解析。排他的ではなく、それぞれ検証項目が違うと考えれば良い 名前 概要 ament_clang_tidy clang-tidy によるコードチェック。 未使用変数のチェック、暗黙的なキャストのチェックなど。(項目は沢山ある) ament_cppcheck CppCheckによる静的解析 メモリリークの可能性などを指摘できる ament_cpplint cpplintによるスタイルチェック Google C++コーディングスタイルに準じているかチェックする ament_pclint PC-Lintによる静的解析 MISRAなどが含まれる。有料 97 名前 概要 ament_clang_format コードがフォーマット済みであるかを検証。clang-format を使用 ament_uncrustify コードがフォーマット済みであるかを検証。Uncrustify を使用

Slide 98

Slide 98 text

Copyright © Fixstars Group 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 論理エラー 論理的なエラー(未使用ライブラリや未定義のシンボルなど)を検出 98

Slide 99

Slide 99 text

Copyright © Fixstars Group 結局何を使えば良いか? ⚫ 必要に応じて選択する ○ 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 は無効化 ※ https://docs.ros.org/en/rolling/How-To-Guides/Ament-CMake-Documentation.html#linting ament_python ⚫ ament_copyright ⚫ ros2 pkg create 直後では無効化 ⚫ ament_flake8 ⚫ ament_pep257 99

Slide 100

Slide 100 text

Copyright © Fixstars Group コマンドラインでの使用例 100 $ 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 解析だけでなくコード修正可能 なものもある

Slide 101

Slide 101 text

Copyright © Fixstars Group lint ● ROS2でのlint ● ament_cmake での使い方 ● ament_python での使い方 101

Slide 102

Slide 102 text

Copyright © Fixstars Group ament_cmake での使い方 ⚫ 基本的にament_lint_auto を利用すれば良い ⚫ ament_lint_auto のマクロは package.xml を読み取る ○ test_depend で指定された lint は自動的にロードされ、lintが実行されるようになる 102 ament_lint_auto ament_cmake_copyright ament_cmake_uncrustify package.xml find_package(ament_lint_auto REQUIRED) ament_lint_auto_find_test_dependencies() CMakeLists.txt

Slide 103

Slide 103 text

Copyright © Fixstars Group 応用 ⚫ 特定の lint を ament_lint_auto での自動実行から外したい場合 ○ AMENT_LINT_AUTO_EXCLUDE を使用する 103 set(AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify) ament_lint_auto_find_test_dependencies() ament_uncrustify( CONFIG_FILE $ ) CMakeLists.txt ament_cmake_uncrustify を除外 設定を変えて lint を実行 指定可能なオプションは CMakeマクロのドキュメントコメントを見る。↓は ament_uncrustifyの場合。 https://github.com/ament/ament_lint/blob/rolling/ament_cmake_uncrustify/cmake/ament_uncrustify.cmake

Slide 104

Slide 104 text

Copyright © Fixstars Group 実行例(1/2) 104 $ 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/test- methodlogy/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/test- methodlogy/build/example_ament_cmake_lint/test_results/example_ament_cmake_lint/uncrustify.xunit.xml'

Slide 105

Slide 105 text

Copyright © Fixstars Group 実行例(2/2) 105 >>> 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

Slide 106

Slide 106 text

Copyright © Fixstars Group lint ● ROS2でのlint ● ament_cmake での使い方 ● ament_python での使い方 106

Slide 107

Slide 107 text

Copyright © Fixstars Group ament_pythonでの使い方 ⚫ 基本的に pytest で実装する ○ lintパッケージはPythonライブラリとして使用できる ■ main関数を利用 107 ament_xmllint ament_flake8 python3-pytest package.xml

Slide 108

Slide 108 text

Copyright © Fixstars Group xmllintのテスト 108 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' test/test_fxmllint.py

Slide 109

Slide 109 text

Copyright © Fixstars Group flake8のテスト 109 from ament_flake8.main import main_with_errors import pytest @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) test/test_flake8.py flake8だけ main_with_errors があるの でそちらを使用 (foxy以降)

Slide 110

Slide 110 text

Copyright © Fixstars Group 実行例 110 $ 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

Slide 111

Slide 111 text

Copyright © Fixstars Group サンプルアプリでのテ スト実装

Slide 112

Slide 112 text

Copyright © Fixstars Group サンプルアプリでのテスト 実装 ● 概要 ● cpp_calc ● py_accum ● integration 112

Slide 113

Slide 113 text

Copyright © Fixstars Group サンプルアプリでのテスト実装 ⚫ C++, Python それぞれで簡単なノードを作成しそれらを組み合わせたシステ ムを作る。 ○ C++ → cpp_calc パッケージ ○ Python → py_accum パッケージ ○ 組み合わせる → integration パッケージ ⚫ 各パッケージについて以下の順番で説明 ○ パッケージ概要 ○ パッケージ作成 ○ lintの設定 ○ ロジック実装及び単体テスト ○ ノード実装及び launch integration test 113 cpp_calc/twice node py_accum/accum node integration

Slide 114

Slide 114 text

Copyright © Fixstars Group サンプルアプリでのテスト 実装 ● 概要 ● cpp_calc ● py_accum ● integration 114

Slide 115

Slide 115 text

Copyright © Fixstars Group 概要 ⚫ 入力値を2倍にして出力するtwiceノードを実装 ○ ament_cmake/c++を使用 ⚫ 最終的なフォルダ構成 115 . ├── 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 cpp_calc/twice node py_accum/accum node integration

Slide 116

Slide 116 text

Copyright © Fixstars Group パッケージ作成 116 cpp_calc 0.0.0 TODO: Package description Shuhei Aoki MIT ament_cmake_auto rclcpp std_msgs ament_lint_auto ament_lint_common ament_cmake_gtest launch_testing_ament_cmake ament_cmake gtest と launch_testing を追加 ライセンス設定 簡易化のためにament_cmake_auto を使用 ros2 pkg create --build-type ament_cmake cpp_calc package.xml

Slide 117

Slide 117 text

Copyright © Fixstars Group lintの設定 ⚫ デフォルトでは copyright と cpplint は無効 ⚫ 今回は使用したいので有効化 117 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() 削除 CMakeLists.txt

Slide 118

Slide 118 text

Copyright © Fixstars Group ロジック実装 ⚫ ロジックとROS2のノード実装と分離することでロジックの単体テストを 可能とする 118 // 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 namespace cpp_calc { int32_t do_twice(int32_t v); } // namespace cpp_calc // 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 include/cpp_calc/twice.hpp src/twice.cpp

Slide 119

Slide 119 text

Copyright © Fixstars Group 単体テスト ⚫ この段階で do_twice の単体テストを実行可能 119 // 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 #include TEST(do_twice, two_sohuld_be_four) { ASSERT_EQ(cpp_calc::do_twice(2), 4); } 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() ament_auto_add_gtest(twice_test test/unittest/twice_test.cpp ) endif() test/unittest/twice_test.cpp CMakeLists.txt

Slide 120

Slide 120 text

Copyright © Fixstars Group ノード実装 ⚫ do_twice を使うノードを作成 120 #pragma once #include #include namespace cpp_calc { using Int32 = std_msgs::msg::Int32; class TwiceNode : public rclcpp::Node { public: TwiceNode(); private: rclcpp::Subscription::SharedPtr sub_; rclcpp::Publisher::SharedPtr pub_; }; } // namespace cpp_calc #include "cpp_calc/twice_node.hpp" #include "cpp_calc/twice.hpp" namespace cpp_calc { TwiceNode::TwiceNode() : rclcpp::Node("twice", "") { pub_ = this->create_publisher("dst", 10); sub_ = this->create_subscription( "src", 10, [this](const Int32::ConstSharedPtr src) -> void { auto output = Int32(); output.data = do_twice(src->data); this->pub_->publish(output); }); } } // namespace cpp_calc ※コピーライトも必要だが長くなるので以降は省略 src/twice_node.cpp include/cpp_calc/twice_node.hpp

Slide 121

Slide 121 text

Copyright © Fixstars Group ノード実装 ⚫ do_twice を使うノードを作成 121 #include #include int main(int argc, char * argv[]) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared()); rclcpp::shutdown(); return 0; } ament_auto_add_library(twice_lib SHARED src/twice.cpp src/twice_node.cpp ) ament_auto_add_executable(twice src/main.cpp ) CMakeLists.txt src/main.cpp

Slide 122

Slide 122 text

Copyright © Fixstars Group launch integration test (launch_testing) ⚫ これによりノードとしての振舞いをテストできる 122 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() CMakeLists.txt

Slide 123

Slide 123 text

Copyright © Fixstars Group test/launch/twice_node_test.py(1/3) 123 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(), ] )

Slide 124

Slide 124 text

Copyright © Fixstars Group test/launch/twice_node_test.py(2/3) 124 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))

Slide 125

Slide 125 text

Copyright © Fixstars Group test/launch/twice_node_test.py(3/3) 125 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 ) 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 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 finally: executor.remove_node(self) executor.shutdown() assert start_len != len(self._msgs) return self._msgs[-1]

Slide 126

Slide 126 text

Copyright © Fixstars Group サンプルアプリでのテスト 実装 ● 概要 ● cpp_calc ● py_accum ● integration 126

Slide 127

Slide 127 text

Copyright © Fixstars Group 概要 ⚫ 入力値を累積し、累積値を出力する ○ ament_python/Python を使用 ⚫ 最終的なフォルダ構成 127 . ├── 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 py_accum/accum node integration

Slide 128

Slide 128 text

Copyright © Fixstars Group パッケージ作成 128 py_accum 0.0.0 TODO: Package description Shuhei Aoki MIT rclpy std_msgs ament_copyright ament_flake8 ament_pep257 python3-pytest launch_pytest ament_python launch_pytest を追加 ライセンス設定 ros2 pkg create --build-type ament_python py_accum package.xml

Slide 129

Slide 129 text

Copyright © Fixstars Group lintの設定 ⚫ ros2 pkg create の段階で必要なテストは作成されている ⚫ copyright は無効になっているので有効化する 129 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' 削除 test/test_copyright.py

Slide 130

Slide 130 text

Copyright © Fixstars Group ロジック実装 ⚫ ロジックとROS2のノード実装と分離する ことでロジックの単体テストを可能とす る ⚫ 単体テスト実装 ○ この段階で Accumulatorクラスのテストが 可能 130 class Accumulator: def __init__(self): self._data = 0 def add(self, v): self._data += v def get(self): return self._data from py_accum.accumulator import Accumulator def test_success(): acc = Accumulator() for i in range(10): acc.add(i) assert acc.get() == 45 py_accum/accumulator.py test/unittest/test_accumulator.py

Slide 131

Slide 131 text

Copyright © Fixstars Group ノード実装(1/2) ⚫ Accumulator を使うノー ドを実装 131 import rclpy from rclpy.node import Node 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__": main() py_accum/accumulator_node.py

Slide 132

Slide 132 text

Copyright © Fixstars Group ノード実装(2/2) ⚫ エントリーポイントに追加 132 entry_points={ "console_scripts": ["accum=py_accum.accumulator_node:main"], }, setup.py

Slide 133

Slide 133 text

Copyright © Fixstars Group launch integration test (launch_pytest) ⚫ test/launch/test_accum.py を作成 ⚫ これによりノードとしての振舞いをテストできる 133

Slide 134

Slide 134 text

Copyright © Fixstars Group test/launch/test_accum.py 134 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()

Slide 135

Slide 135 text

Copyright © Fixstars Group test/launch/test_accum.py 135 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)

Slide 136

Slide 136 text

Copyright © Fixstars Group サンプルアプリでのテスト 実装 ● 概要 ● cpp_calc ● py_accum ● integration 136

Slide 137

Slide 137 text

Copyright © Fixstars Group 概要 ⚫ twice と accum を組み合わせる → 2倍にして累積する ○ launchファイルとして実装 ⚫ テストでは rosbag をテストデータとして使用する ⚫ 最終的なフォルダ構成 ○ ament_cmake を使用 137 cpp_calc/twice node py_accum/accum node integration . ├── package.xml ├── CMakeLists.txt ├── launch │ └── integration.launch.yaml └── test ├── data │ └── testdata.bag └── launch └── test_integration.py

Slide 138

Slide 138 text

Copyright © Fixstars Group パッケージ作成 138 integration 1.0.0 Integration package Shuhei Aoki MIT ament_cmake_auto py_accum cpp_calc ament_lint_auto ament_lint_common ament_cmake_pytest launch_pytest ament_cmake launch_pytest を追加 ライセンス設定 ros2 pkg create --build-type ament_cmake integration package.xml lintの設定は cpp_calc と同様なので省略

Slide 139

Slide 139 text

Copyright © Fixstars Group launchファイル 139 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 py_accum/accum node /src /dst /calc_to_accum launch/integration.launch.yaml

Slide 140

Slide 140 text

Copyright © Fixstars Group launch integration test (launch_pytest) ⚫ rosbagをテストデータとして使用する ○ 0 から 9 の値が入っている ■ → integration が出力する最終値は 90 になるべき 140 if(BUILD_TESTING) ament_auto_find_test_dependencies() find_package(ament_cmake_pytest) ament_add_pytest_test(pytest test ) endif() CMakeLists.txt

Slide 141

Slide 141 text

Copyright © Fixstars Group test/launch/test_integration.py 141 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

Slide 142

Slide 142 text

Copyright © Fixstars Group test/launch/test_integration.py 142 @pytest.fixture def integration_launch(): return IncludeLaunchDescription( AnyLaunchDescriptionSource( str( Path(get_package_share_directory("integration")) / "launch" / "integration.launch.yaml" ) ) ) @pytest.fixture def testdata(): path_to_test = Path(__file__).parent.parent return ExecuteProcess( cmd=[ "ros2", "bag", "play", str(path_to_test / "data" / "testdata.bag"), "--delay", "1", "--wait-for-all-acked", "1000", ], shell=True, ) launchファイルを起動する action ros2 bag play を実行する action • 起動してから1秒待ってから再生開始さ せる(humble以降) • 送ったデータが全て受信されたことを 確認してから終了する(humble以降)

Slide 143

Slide 143 text

Copyright © Fixstars Group test/launch/test_integration.py 143 @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()

Slide 144

Slide 144 text

Copyright © Fixstars Group test/launch/test_integration.py 144 @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) LaunchDescription を作成 • testdata と integration_launch は fixture のもの • 2つ前のページで定義した関数の結果が得られる ros2 bag play のプロセスを得られる。 await testdata.get_asyncio_future() でプロセス終了待ち、つまりテスト データ再生終了待ちができる

Slide 145

Slide 145 text

Copyright © Fixstars Group test/launch/test_integration.py 145 @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終了後の状態を得られる set up test_xxx tear down launch PUT shutdown PUT each test cases post test_xxx

Slide 146

Slide 146 text

Copyright © Fixstars Group その他TIPS

Slide 147

Slide 147 text

Copyright © Fixstars Group TIPS ⚫ ラベルとマーカー ⚫ テスト時のROS_DOMAIN_ID ⚫ CIでの自動テスト 147

Slide 148

Slide 148 text

Copyright © Fixstars Group ラベルとマーカー ⚫ ctest のラベルと pytest のマーカーを使うことで実行するテストを絞ること ができる ○ gtest だけ実行したい、lint だけ実行したい、といったケースに活用できる ⚫ ament_cmake では ament_add_test_label で ctest ラベルを付与 ○ テストフレームワーク毎に自動で設定されているものもある ⚫ pytest のマーカーの例 ⚫ コマンド例 148 @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

Slide 149

Slide 149 text

Copyright © Fixstars Group ROS_DOMAIN_ID ⚫ これまで紹介したテストは、1つ1つ独立して実行する場合問題ないが、同時 に実行すると問題が出るケースがある ○ launch integration test ではトピック通信を扱っていたため、トピック名が重複すると通信が 混線する ■ 確率で失敗する、という状態になる ⚫ この問題を回避するために ROS_DOMAIN_ID を使用したい ○ → しかしどのようにROS_DOMAIN_IDを設定するか? 149

Slide 150

Slide 150 text

Copyright © Fixstars Group ROS_DOMAIN_ID ⚫ domain_coordinatorを使用する ○ https://github.com/ros2/ament_cmake_ros/tree/rolling/domain_coordinator ○ 重複しないように ROS_DOMAIN_ID を選択する Python パッケージ ○ 実装としてはソケットによるポートロックを応用したもので、使われている ROS_DOMAIN_IDを検知しているわけではないことに注意 150

Slide 151

Slide 151 text

Copyright © Fixstars Group 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 スクリプトだけ拝借する形になる 151 # 要 ament_cmake_ros find_package(ament_cmake_ros REQUIRED) add_launch_test(test/test_twice_launch.py RUNNER "${ament_cmake_ros_DIR}/run_test_isolated.py" ) CMakeLists.txt $ colcon test --packages-select example_launch_testing_ament_cmake --event-handlers console_direct+ ... 1: Running with ROS_DOMAIN_ID 1 ... 実行例

Slide 152

Slide 152 text

Copyright © Fixstars Group ROS_DOMAIN_ID ⚫ ament_python の場合 ○ domain_coordinator をPythonライブラリとして使う形になりそう ○ 以下のようなコードを最初に書く 152 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) test/launch/test_twice_launch.py

Slide 153

Slide 153 text

Copyright © Fixstars Group CIでの自動テスト ⚫ コードをコミットし、リモートリポジトリにプッシュする毎に自動的にテス トが実行されるようになると、継続的な検証が可能になる ⚫ ここでは GitLab CI/CDでの一例を示す ⚫ CIで行うこと ○ ビルド/テスト環境の構築 ○ ビルド/テスト 153

Slide 154

Slide 154 text

Copyright © Fixstars Group CIでの自動テスト ⚫ ビルド/テスト環境の構築 ○ 使用しているDockerfileをビルドしてイメージを GitLab のレジストリに登録 154 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 .gitlab-ci.yml (1/2)

Slide 155

Slide 155 text

Copyright © Fixstars Group CIでの自動テスト ⚫ ビルド/テストの実行 ○ 作ったイメージを使ってビルドとテストを実行 ○ CIの結果として、buildとinstall、logディレクトリを artifacts に指定 155 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 .gitlab-ci.yml (2/2)

Slide 156

Slide 156 text

Copyright © Fixstars Group CIでの自動テスト ⚫ 実行例(抜粋) 156 GitLabの画面より

Slide 157

Slide 157 text

Copyright © Fixstars Group 参考文献

Slide 158

Slide 158 text

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/ 158

Slide 159

Slide 159 text

Copyright © Fixstars Group Thank you! お問い合わせ窓口 : [email protected]