aptpod Advent Calender 2020、 9日目の記事です。 本日の担当は、組込み開発チームでFW開発を担当している矢部です。
はじめに
組込み機器の開発に関わって1x年ですが、入出力の自動化が難しい機器も多く、結局手作業になって無駄にボタン押下やUI操作の速度が洗練されたりします。私の場合、ゲームをよくやっていたからか、効率よく操作させることができるとちょっと嬉しかったりもします。 とはいえリグレッションテストなどを考えると極力自動化したいところなので、過去いろいろと試行錯誤してきました。 今回は、その中でも比較的簡単な、USB通信をインターフェースに持つ機器のテストを自動化した際の手法について簡単に解説します。
前提知識
今回の記事で詳しくは解説しない部分をさらっと。
BDDとは
振る舞い駆動開発(Behavior Driven Development)と呼ばれるもので、TDDの一種。 システムに期待する挙動や制約などを自然言語に近い形式で記述し、テストを実行できるようにしたもの。自然言語で書けるため、非開発者であってもシステムの仕様がわかるように書ける(らしい)。 テストファーストな開発だと「テストコードは仕様である」という考え方になるので、それを突き詰めたものがBDDかな、と思っています。
BDDのフレームワーク
主要な言語であれば、たいていBDDを実現するためのフレームワークが存在します。
私が最初に触れたのはRSpecですが、最近はPythonを利用する機会が多いため、今回はBehaveで実装します。
Pythonの開発環境
色々ありますが、最近はpipenvを使っています。設定や実行方法など、バランスが取れていて使いやすい。
- 公式: https://pipenv-ja.readthedocs.io/ja/translate-ja/
- Pipenvを使ったPython開発まとめ: https://qiita.com/y-tsutsu/items/54c10e0b2c6b565c887a
USB機器をテストする
実行環境
- Ubuntu MATE 20.04
今回はLinuxのデバイスドライバを利用する都合上、テスト環境はLinuxになります。
Python環境構築
テスト用のディレクトリに、pipenvを利用してBehaveをインストールします。
$ mkdir usbtest $ cd usbtest $ pipenv --python 3 $ pipenv install behave
テスト対象
今回は私が開発に関わった、aptpodのCAN-USB Interface - AP-CT2A
1を対象にします。実際にAP-CT2A
ではこの方法で全機能の自動テストコードを書いています。なお、2020/12 時点ではデバイスドライバなどのコードを公開していないため、細かいところはぼかしたり改変しています。
PythonでUSBデバイスを操作する
方法はいくつかあると思いますが、今回はデバイスドライバがあるため、ioctl を利用して操作します。
基本的な手順は以下になります。
- デバイスファイルをopenする。
- ファイルディスクリプタを利用して、ioctlでIO制御する。
ioctlのリクエストは汎用的なものであればPython側にありますが、今回のように特殊なものは自前で設定する必要があります。 リクエストの種別としてIO、IOR、_IOWなど2があり、C/C++であれば以下のように実装されます。
struct fw_data_s { int value; }; typedef struct fw_data_s fw_data_t; #define CMD1 _IO('A', 0x1) #define CMD2 _IOR('A', 0x2, fw_data_t) #define CMD3 _IOW('A', 0x3, fw_data_t)
上記のコマンドがデバドラ側にあると仮定して、デバイス操作のためのクラスをPython上で実装すると以下のようになります。__make_xxx
のメソッドが、リクエストの生成部分になります。
import ctypes import fcntl import logging import os class FwData(ctypes.Structure): _fields_ = [("value", ctypes.c_int32)] class ApCt2a: def __init__(self, path): self.__path = path def open(self): self.__fd = os.open(self.__path, os.O_RDWR | os.O_NONBLOCK) if self.__fd == -1: logging.critical("[APTTRX] failed open path:{0}".format(self.__path)) def close(self): os.close(self.__fd) self.__fd = None def cmd1(self): fcntl.ioctl(self.__fd, self.__make_io_req(0x1)) @property def data(self): data = FwData() fcntl.ioctl(self.__fd, self.__make_ior_req(0x02, ctypes.sizeof(data)), data) return data.value @data.setter def data(self, value): data = FwData() data.value = value fcntl.ioctl(self.__fd, self.__make_iow_req(0x03, ctypes.sizeof(data)), data) def __make_io_req(self, nr): return ord("A") << 8 | nr def __make_iow_req(self, nr, size): return 1 << 30 | ord("A") << 8 | nr | size << 16 def __make_ior_req(self, nr, size): return 2 << 30 | ord("A") << 8 | nr | size << 16
テストする仕様
AP-CT2Aのメイン機能は、入力したCAN3データに時刻情報を付与してホストに渡すというものです。さらに複数台あった場合、同期用のケーブルで機器間を接続することで、付与する時刻を同期させることができます。
上の写真で、機器間を繋いでいるのが同期ケーブルです(開発用の機材なので汚いのはご容赦を)。今回は、この同期機能を確認するためのテストを例に取ります。
テストしたいシーケンス
- 2台のAP-CT2Aを接続する
- 2台のAP-CT2Aを時刻同期する
- CANデータを500kbpsで受信できるよう設定する
- CANデータを入力する
- 2台のAP-CT2A受信したCANデータの時間が一致していることを確認する
実際に書いてみる
ファイルは以下のような形で構成されます。.feature
にテストシナリオ、steps/
以下に実際にシナリオ内で実行される処理(step)を実装します4。
features/ features/everything.feature features/steps/ features/steps/steps.py
今回の例だと、まずテストシナリオをこのように書きます5。
Feature: AP-CT2A テスト Scenario: 複数台で時間同期する Given 2台のAP-CT2Aを用意する And AP-CT2Aを時刻同期する And AP-CT2Aを500kbps入力に設定する When AP-CT2AにCANデータを500kbpsで10個入力する Then AP-CT2AがCANデータを10個取得している And AP-CT2Aが受信したCANデータの時刻情報が一致している
そして、各stepの処理について、以下のように書きます。@~
のところで対応するstepを指定し、変数として利用したい箇所は{}
で記載します6。
from behave import given, step, then, when @given("AP-CT2Aを時刻同期する") def step_impl(context): for d in context.devices: d.sync() @when("AP-CT2AにCANデータを{baudrate:d}kbpsで{num:d}個入力する") def step_impl(context, baudrate, num): send_candata(baudrate, num) @then("AP-CT2AがCANデータを{num:d}個取得している") def step_impl(context, num): for d in context.devices: data = d.receive() assert len(data) == num
実行例
上のシナリオを実行した例。
$ pipenv shell $ behave Feature: AP-CT2A テスト # features/ap_ct2a.feature:1 Scenario: 複数台で時間同期する # features/ap_ct2a.feature:3 Given 2台のAP-CT2Aを用意する # features/steps/ap_ct2a_steps.py:6 0.001s And AP-CT2Aを時刻同期する # features/steps/ap_ct2a_steps.py:11 0.001s And AP-CT2Aを500kbps入力に設定する # features/steps/ap_ct2a_steps.py:16 0.000s When AP-CT2AにCANデータを500kbpsで10個入力する # features/steps/ap_ct2a_steps.py:21 0.000s Then AP-CT2AがCANデータを10個取得している # features/steps/ap_ct2a_steps.py:26 0.000s And AP-CT2Aが受信したCANデータの時刻情報が一致している # features/steps/ap_ct2a_steps.py:31 0.000s 1 feature passed, 0 failed, 0 skipped 1 scenario passed, 0 failed, 0 skipped 6 steps passed,
おわりに
今回はUSB機器を例に取りましたが、何らかのプロトコルで通信することで動作するものについてはほぼ自動テスト化できるものと思っています。 外部からの操作を必要とするものに関しては(例えばUSBの挿抜とか)なかなか自動化は難しいですが、aptpodには優秀なHWエンジニアも在籍しており、テスト用の治具も自作したりします。 組み込み機器であっても、既存のフレームワークをうまく使ったり、アイデア次第で煩雑なテスト業務を軽減させることができますので、トライしてみてはいかがでしょうか。
-
https://www.aptpod.co.jp/products/hardware/ のペリフェラルデバイスの項に載っています。↩
-
https://www.quora.com/What-is-IO-IOR-and-__IOW-in-ioctl↩
-
https://behave.readthedocs.io/en/latest/tutorial.html#features↩
-
普段は日本語では書きません。マルチバイト文字怖い。↩
-
https://behave.readthedocs.io/en/latest/tutorial.html#python-step-implementations↩