どうも。ROS/ROS2 Advent Calendar 2020 10日目の記事です。
自分は現在ElixirによるROS2クライアントライブラリRclexに携わっているのですが、やはりモダンな開発なら自動テストは欠かせないと思います。そこで今回はGitHubActions上でROS2を動かしCIを行う話をしたいと思います。
Rclex
RclexとはElixirと呼ばれる言語で書かれたROS2のライブラリです。ElixirとはErlangVM上で動く動的型付けな言語で、Elixirのコードはプロセスと呼ばれる隔離された軽量な実行スレッドの中で動作し、メッセージを通じて情報のやり取りを行います。プロセスが隔離されているため各プロセスは個別にガベージコレクションを行うことが可能で、計算資源を有効に活用できます。
目標
やりたいことは主に以下のことになります。
- GitHubリポジトリにpushされたときに自動テストが行われること
- Rclexの各メソッドをテストできること
- Rclex同士やRclexと他のROS2ライブラリが通信可能なことをテストできること
1つ目の「GitHubリポジトリにpushされたときに自動テストが行われること」に関しては様々な方法がありますが今回はGitHubActionsを用いることで達成しました。他にもCIを行う方法としてJenkinsやCircleCI等がありますが、自分でサーバーを建てる必要がなく新たにアカウントを作る(そして管理する)必要もないことからGitHubaActionsを選択しました。
2つ目の「Rclexの各メソッドをテストできること」ですがElixirにはテスト機能が備わっており、mix test
を行うことで指定したテストを行うことが出来ます。テストを書いてしまえばこれをGitHubActions内で実行することでRclexで各メソッドのテストを行うことが可能です。
3つ目の「Rclex同士やRclexと他のROS2ライブラリが通信可能なことをテストできること」ですがこれが一番難しいです。というのも通信のテストを行う以上異なるプロセスでそれぞれのノードを実行させたいのですがmix test
だと同一のプロセスで通信を行うことやそもそも別のクライアントライブラリを用いることからmix test
を用いてテストを行うことが出来ず、自分でなんらかのテスト機構を考える必要があります。今回はシェルスクリプトを用いてRclexとrclcppのノードを起動し通信を行いました。
構成
GitHubActionsの設定は以下のようになっています。
on: [push, pull_request]
defaults:
run:
shell: bash
jobs:
rclex_test:
runs-on: ubuntu-latest
container: ros:dashing
steps:
# clone latest branch
- name: clone
uses: actions/checkout@v2
- name: setup elixir
run: |
apt-get update
apt-get install -y wget
wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb && sudo dpkg -i erlang-solutions_2.0_all.deb
apt-get update
apt-cache showpkg elixir | grep 1.9.1
apt-get install -y esl-erlang=1:22.0.7-1
apt-get install -y elixir=1.9.1-1
- name: compile
run: |
echo 'mix local.hex --force'
MIX_ENV=test mix local.hex --force
echo 'mix deps.get'
MIX_ENV=test mix deps.get
echo 'MIX_ENV=test mix compile'
MIX_ENV=test mix compile
- name: mix test
run: |
source /opt/ros/dashing/setup.bash
echo 'mix test'
mix test
- name: ros2 connection test
run: |
source /opt/ros/dashing/setup.bash
cd ros2_test
./entrypoint.sh
基本的には必要な環境を持ってきて動かせるようにした後にテストを行っているだけです。
clone
では変更されたブランチを持ってきています。setup elixir
ではelixirをインストールし、compile
で変更されたブランチをコンパイル。mix test
とros2 connection test
ではそれぞれRclexのメソッドテストと通信のテストを行います。
Elixirの環境は持ってきているけどrosの環境は特に設定を行っていないように見えるのはcontainer: ros:dashing
のおかげで、これによってdashingの環境がすでに整理されたコンテナを使ってテストを行えるようになっています。なので後はsetup.bash
を読み込めばros2を実行することが出来るのですが、注意しないといけないのはGitHubActionsではrun
ごとにシェルが変わるためrun
のたびにsetup.bash
を読み込む必要があります。これに気づかずにかなり時間を使った...。
通信テスト
ros2 connection test
内のentrypoint.sh
を実行することで各サブフォルダのテストを実行します。テストが通らない場合は異常終了し、テストが失敗したことを表します。例としてrclcppからRclexに対してpubsub通信を行うことのテストを示します。
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include <fstream>
#include <random>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class Test_String
{
public:
static std::string get_random_string(const int len){
static const char alphanum[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
srand (time(NULL));
std::string random_string;
for (int i = 0; i < len; ++i) {
random_string += alphanum[rand() % sizeof(alphanum) - 1];
}
return random_string;
}
};
int main(int argc, char *argv[]) {
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("talker");
auto publisher = node->create_publisher<std_msgs::msg::String>("testtopic", 10);
std_msgs::msg::String message;
message.data = Test_String::get_random_string(10);
rclcpp::WallRate rate(1s);
for (int i = 0; i < 2; i++) {
publisher->publish(message);
std::ofstream ofs("cpp_pub.txt");
ofs << message.data;
rclcpp::spin_some(node);
rate.sleep();
}
rclcpp::shutdown();
return 0;
}
talker.cppはrclcppで作られたノードでシンプルなパブリッシャーノードです。トピックに対してランダムな文字列を送信します。1回ではなく2回送信しているのは現在Rclexが1回目を受信しない場合があるためです。トピックに対して送信するだけでなく外部ファイルに同じ文字列を書き出しています。これは後でサブスクライバーノードが受け取った文字列と照会するためです。
defmodule Test.App.SimpleSub do
@moduledoc """
The sample which makes any number of publishers.
"""
def sub_main(num_node) do
# Create as many nodes as you specify in num_node
context = Rclex.rclexinit
node_list = Rclex.create_nodes(context,'test_sub_node',num_node)
subscriber_list = Rclex.create_subscribers(node_list, 'testtopic', :single)
{sv, child} = Rclex.Subscriber.subscribe_start(subscriber_list, context, &sub_callback/1)
Process.sleep(4000)
Rclex.Timer.terminate_timer(sv, child)
Rclex.subscriber_finish(subscriber_list, node_list)
Rclex.node_finish(node_list)
Rclex.shutdown(context)
end
# Describe callback function.
def sub_callback(msg) do
# IO.puts("sub time:#{:os.system_time(:microsecond)}")
received_msg = Rclex.readdata_string(msg)
IO.puts("received msg:#{received_msg}")
File.write("ex_sub.txt", received_msg)
end
end
simple_sub.exはRclexのサブスクライバーノードの実装です。受け取らずに待ち続けると困るので4秒待った後強制的に終了するようにしています。また受け取った文字列を外部ファイルに出力するようにしています。
#!/bin/bash
testDir=`pwd`
root=$1
cd rclcpp
colcon build
source install/setup.bash
cd $root
echo $root
mix compile
mix run ros2_test/simple_pubsub_with_cpp/rclex/sub_test.exs &
sleep 1
cd $testDir
ros2 run cpp_pubsub talker &
wait
cppPub=`cat cpp_pub.txt`
exSub=`cat $root/ex_sub.txt`
test $cppPub = $exSub
result=$?
echo "published message : $cppPub"
echo "subscribed message : $exSub"
echo "result : $result"
if [ $result -ne 0 ]; then
exit 1
fi
実際のテスト部分はrun_test.shで行います。
先にサブスクライバーノードを起動し、その後パブリッシャーノードを起動します。うまく通信がいけば2つのノードから出力された文字列が一致します。逆に一致しなかった場合はexit 1
することで異常終了します。
今後の課題
現在ではRclex自体の機能が足りず、ノード情報やトピック情報を使いやすい形で取得することが出来ず、Rclexのメソッドテストにおいてテストしたい情報が得られないことがありテストを必ず書けるような状態ではありません。これは今後の機能拡張によって行っていく予定です。
また、Rclexとその他のライブラリとの通信テストが煩雑であることはテストの書きにくさに繋がるため改善の余地があります。今の所あまりうまくいっておらず試行錯誤している状態です。
まとめ
今回はROS2クライアントライブラリのCIを行った様子を紹介しました。基礎は出来ているものの今後書き続けるためにはよりうまい仕組みを構築する必要があると思います。