Subscribe to Eventボックスをqi Frameworkで書きなおしてみた話

[Subscribe to Eventボックスにちゃんとunsubscribeさせる](http://qiita.com/yacchin1205/items/6a1b736c0b4b5b061aee)でボックスの修正の話を書いたわけですが、すでにアプリにコピーしてしまったボックスをどのように更新していくかはChoregrapheにおいてはとても悩ましい問題です。

すでに[Pepperボックスライブラリのテスト考](http://qiita.com/yacchin1205/items/a2d26ce8f60e8f66e32c)でもつらつら考えた通り、ボックスライブラリ中のあるボックスを更新しても、すでにアプリにコピーしてしまったボックスが自動的には更新されることはないようです。
つまり、現状では、ボックスライブラリのボックスを更新しても、すでにボックスライブラリからビヘイビアに配置してしまったボックスは、手作業でのボックスの配置し直しが必要になります。ボックス間の接続のやり直しなども考えると、接続が複雑になってしまったビヘイビアにおいては、ミスなくやり切るのはなかなか難しい作業です。

また、バグがなくても、プラットフォームは常に変化を続けます。たとえばChoregraphe 2.3のドキュメントを読むと、NAOqi Frameworkはqi Frameworkと統合、整理されていく流れが見えますし、これらのAPIを用いて実装されているボックスもあわせて変化していく必要があります。

そんなわけで、Subscribe to Eventボックスをqi Frameworkで書き直してみたという話を、ハマった点合わせて紹介しつつ、既存ボックスの置き換えをどう考えていくべきかの個人的トライアルをつらつらとメモしてみます。

# qi FrameworkでSubscribe to Eventボックスを書いてみる

以前、[Pepperくんで HTML5 TEST やってみた](http://qiita.com/yacchin1205/items/a81ea84e09843c1d3e1e)で作成したボックスライブラリに追加してみています。GitHubは以下、

- https://github.com/yacchin1205/pepper-web-boxes 

**web-boxes** ディレクトリがボックライブラリです。
[ボックスライブラリパネル](http://qiita.com/Atelier-Akihabara/items/db907ed319b8af0984db#%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%83%91%E3%83%8D%E3%83%AB)からこのディレクトリを読み込めばOKです。読み込む際は、ファイルの種類に `ボックスライブラリのディレクトリ(Directories)` を選択する必要があります。

ボックスライブラリの Memory/ ディレクトリにSubscribe to Eventボックスを格納しています。
入力、出力のタイプや名前は標準ボックスライブラリの Memory/Subscribe to Eventボックスにあわせています。

![SubscribeToEvent.png](https://qiita-image-store.s3.amazonaws.com/0/28770/da904e38-4bca-ad03-40d9-5423ff539672.png)

ボックスのコードは https://github.com/yacchin1205/pepper-web-boxes/blob/master/web-boxes/Memory/Subscribe%20to%20Event/box.xar を参照。以下のようなコードになっています。

```py
class MyClass(GeneratedClass):
    def __init__(self):
        GeneratedClass.__init__(self)
        import threading
        self.lock = threading.RLock()

    def onLoad(self):
        with self.lock:
            self.targetKey = None
            self.valueId = None

    def onUnload(self):
        with self.lock:
            if self.targetKey and self.valueId:
                self.logger.debug("Unsubscribe: %s, id=%s" % (self.targetKey, self.valueId))
                memory = self.session().service('ALMemory')
                signal = memory.subscriber(self.targetKey).signal
                signal.disconnect(self.valueId)
                self.targetKey = None
                self.valueId = None

    def onInput_onStart(self):
        with self.lock:
            if not self.valueId:
                self.logger.debug("Subscribe: %s" % self.targetKey)
                self.targetKey = self.getParameter('key')
                memory = self.session().service('ALMemory')
                signal = memory.subscriber(self.targetKey).signal
                self.valueId = signal.connect(self.onValue)

    def onValue(self, value):
        import qi
        if isinstance(value, long):
            value = int(value)
        qi.async(self.onEvent, value)

    def onInput_onStop(self):
        self.onUnload()
        self.onStopped()
```

## ハマった(1): 同時入力の禁止

Choregrapheはリソースの設定などしない限り、ボックスの排他制御に関して特に考えてくれなさそうです。Pythonスクリプトでメンバ変数の変更など状態を変える処理をおこなう際など、必要に応じて対策しておかないと、同時に複数スレッドから入力を受けると予期しない動作をしてしまう場合があります。
そのため今回は、`threading.RLock`を用いて排他制御をおこなっています。

Subscribe to EventボックスのPythonスクリプトのうち、コンストラクタで、

```py
        import threading
        self.lock = threading.RLock()
```

のようにして同期用オブジェクトを生成し、入力に対する処理は、

```py
    def onInput_onStart(self):
        with self.lock:
            # 処理
```

みたいな感じにすることで、同時に複数スレッドから入力を受けても、処理自体は同時実行されないないように考慮しています。
これを考えていなかったせいで、テスト中意図せず、同時発火する2つの出力から1つのボックスに入力してしまい、「なんじゃこりゃあ」みたいな動きをさせてしまっていたのでした。

個人的には、Choregrapheはボックスの配置、接続が非常に簡単にでき自由度が高い分、ボックス自体はどんな乱暴な入力のされ方をしても破綻しないよう、堅牢に作っておきたいところです。

## ハマった(2): 型の変換

慎重に作らないとハマるのが型の問題です。

今回わかったのは、`int` 値が渡されることを期待して、 `1` みたいな整数値をRaise Eventボックスに入力すると、qi Frameworkの `signal` から呼び出された関数の引数には `long` 型の値が渡されるようだ、ということ。

これ自体は問題のある挙動ではありませんが、数値入力を持つボックスに対して `long` 値を入力すると、ボックスの引数には `None` があらわれてしまうようです。qiとNAOqiとで型のポリシーが違うんですかねえ・・・まあ、[unicode型をボックスが受けつけてくれない](http://qiita.com/yacchin1205/items/95e98591abdc598d2bc4#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F-python%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9%E3%81%A7%E3%81%AEunicode%E3%81%AE%E6%89%B1%E3%81%84)という挙動もありましたしねえ・・・

```py
    def onValue(self, value):
        import qi
        if isinstance(value, long):
            value = int(value)
        qi.async(self.onEvent, value)
```

この辺の問題をすぐ見つけるためにも、テストをしっかり記述して定期的に実行するのはとても重要だなあ、という感じがします。

# テストの記述と実行

テスト用プロジェクトは、 **tests/test-memory** プロジェクトにあります。
ビヘイビアは、テスト用処理を格納したボックスを順次実行するような形式になっています。

![Test_root.png](https://qiita-image-store.s3.amazonaws.com/0/28770/348be2ba-9897-c253-25b1-860c2eafe8cb.png)

たとえば Event String 1 テストボックスならば、以下のように、2つの異なるキーを監視するSubscribe to Eventボックスを配置して、あるキーに対してRaise Eventされた場合に、適切なボックスのみがonEvent出力をおこなうこと、反応すべきonEvent出力からはどのような文字列が出力されるはずかどうかをテストしています。

![Test_EventString1.png](https://qiita-image-store.s3.amazonaws.com/0/28770/e554dfd9-ca78-3053-44a3-7155a4c0e325.png)

Assert Not Bang, Assert Stringボックスは、期待した値が入力されたか(あるいは入力されなかったか)どうかをテストし、期待しない状態になっていればエラーを示すようにしてあります。

また、 Stop Event テストボックスでは、[Subscribe to Eventボックスにちゃんとunsubscribeさせる](http://qiita.com/yacchin1205/items/6a1b736c0b4b5b061aee)のようにonStop入力によりonEvent出力が反応しなくなることを確認しています。

![Test_StopEvent.png](https://qiita-image-store.s3.amazonaws.com/0/28770/6c0f5d53-3d75-17d9-6ba6-f026ba900632.png)

このようにテストを記述することで、実行するだけで予期した挙動か否かを判定することができるわけです。例えばボックスにバグがあったときに修正したとき、このテストを実行することで、過去あったバグが復活していないかなど確認することができます。

# 既存ボックスの置き換え

このようにしてボックスを作成したら、自身のプログラムに反映してみます。これにはreplace-boxesツールを利用します。コードは https://github.com/yacchin1205/choregraphe-box-util で管理しています。(要pip)

```
$ pip install git+https://github.com/yacchin1205/choregraphe-box-util.git
```

インストールすると、 `replace-boxes` というコマンドが実行できるようになります。

たとえば、今回のように`web-boxes` というディレクトリにあるボックスライブラリのボックスを使って、test-memoryプロジェクト (.pmlファイルのパスは`tests/test-memory/test-memory.pml`とします) を作成していた場合に、test-memoryのビヘイビア中のボックスを、ボックスライブラリ中の最新のボックスに置き換えたい場合は、以下のように実行します。

```
$ replace-boxes --lib web-boxes/ tests/test-memory/test-memory.pml
Replaced: tests/test-memory/test-memory.pml (29 boxes)
```

すると、この[コミット差分](https://github.com/yacchin1205/pepper-web-boxes/commit/16bc18cf8c21964b5bd94a38d3c3d0cc2cd5607a)のように、web-boxes/Memory/Subscribe to Eventボックスにおこなわれた変更と同じ変更が、test-memoryプロジェクト中の同じ名前で、説明中に同じURLへの`@source`定義を持つボックスに反映されます。

なお、このツールの実行後、一度Choregrapheで開いて保存することで、XMLのインデントなどが調整されわかりやすい差分をとることができると思います。(Choregrapheと同じルールのXMLのフォーマットに挑戦しましたが、うまくいかなかった・・・)

# まとめ

今回はSubscribe to Eventボックスのqi Frameworkでの自作をする中で、そのテスト、変更したボックスの反映を、自作ツールを用いて自動化するチャレンジをしてみました。

すべてのバグを生まれる前に消し去るには、たぶん無限の因果を背負い込んで魔法少女になったりする必要がある気がするので、せめてバグを生んでしまったらすみやかに修正し反映する体制を整えておきたいものです。