ROS2クライアントライブラリのCIを構築した話

どうも。ROS/ROS2 Advent Calendar 2020 10日目の記事です。
自分は現在ElixirによるROS2クライアントライブラリ[Rclex](https://github.com/tlk-emb/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の設定は以下のようになっています。

``` ci.yml
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通信を行うことのテストを示します。

```talker.cpp
#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回目を受信しない場合があるためです。トピックに対して送信するだけでなく外部ファイルに同じ文字列を書き出しています。これは後でサブスクライバーノードが受け取った文字列と照会するためです。

```simple_sub.ex
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秒待った後強制的に終了するようにしています。また受け取った文字列を外部ファイルに出力するようにしています。

```run_test.sh
#!/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を行った様子を紹介しました。基礎は出来ているものの今後書き続けるためにはよりうまい仕組みを構築する必要があると思います。