この記事は Akatsuki Games Advent Calendar 2023 10日目の記事です。
昨日は Yusuke Nakajima さんの UnityのProjectWindowに表示履歴機能を付けるエディタ拡張 でした。表示履歴機能があるだけで開発中のストレスが軽減されますね。導入もシンプルにしてくれているのがとても良いと思いました!
はじめに
QAエンジニアの山﨑@tomo_tk11です。私のチームでは「ゲーム開発を行うプロジェクトメンバーと連携しながら、QA コストや QA リードタイムの最適化」を行っています。 現在はマスターデータのチェッキング作業の効率化や、Airtest と AWS Device Farm(以下、Device Farm) を用いてリグレッションテストなどの自動化に取り組んでいます。
今回は、使用している Airtest のアップデートを行った際に Device Farm で動作しなくなったため、その原因と解決策をまとめました。 Airtest と Device Farm の自動テストシステムで過去に出した資料を合わせて読むことで理解が深まると思います。最後に載せておくのでよければご覧ください。
TL;DR
- デバイスを指定する uri に
serial_no
とadb_path
を入力しよう
# OK auto_setup(f"Android:///{serial_no}?adb_path={adb_path}") # NG auto_setup(f"Android:///")
目次
- はじめに
- TL;DR
- 目次
- Airtest アップデートのきっかけ
- Device Farm 上でテスト実行すると、 ADB Error が発生する
- 解決策
- v1.3.1 のアップデートで不要となった処理
- まとめ
- 参考資料
Airtest アップデートのきっかけ
今回は Airtest v1.3.0 から v1.3.1 へのアップデートを行いました。 最近のAirtest は2ヶ月くらいの間隔でバージョンアップされています。 アップデート内容もかなり積極的でとても助かっています。
- 最近のアップデートで嬉しかったもの
- デバイスの画面録画機能で cv2, ffmpeg サポート
- iOS の自動テストサポートの拡張(今までは一癖ありました)
そして、v1.3.1 では adb に関する気になるアップデートがありました。
adb will now give priority to using the current adb process, or the system variable is set to adb in ANDROID_HOME. If neither is found, adb in airtest will be used.
Device Farm では adb プロセスが既に立ち上がっており、その adb と Airtest に内蔵されている adb が競合するという問題が以前ありました。なので、私たちのシステムではひと工夫しています。
Airtestの FAQ によると、競合した際はpackage内のadbを任意のものに差し替えるとのことです
なのでtestspecのpre_testでinstallしたAirtest package内のadbをDevice Farmのものに差し替えます
v1.3.1 のアップデートでそのひと工夫が不要になるのではないかと思い、さっそく自動テストを動かしてみました。
Device Farm 上でテスト実行すると、 ADB Error が発生する
AdbError が発生し、テストスクリプトの実行にすら行きつけませんでした。
airtest.core.error.AdbError: stdout[] stderr[/bin/bash: devices: No such file or directory
原因
このエラーは、Airtest の adb に関する仕様変更による影響です。Device Farm では常に adb プロセスが動作しているという話をしましたが、実際には /bin/bash
と /opt/dev/android-sdk-linux/platform-tools/adb.orig
の2つの実行ファイルが adb を名乗って動いています。 Device Farm は adb の機能に一部制限を加えるために元々の adb バイナリファイルを adb.orig
とし、ラッパースクリプト(bash ファイル)を adb
として実行しています。今回のアップデートによって Airtest が Device Farm の adb ラッパーの仕組みを吸収できず、adb パスを認識しなくなったためエラーが発生しています。
ちょっとややこしいので深掘りします。
なぜエラーが起きるようになったのか?
Airtest の adb パス決定ロジックが追加されたから
adb will now give priority to using the current adb process, or the system variable is set to adb in ANDROID_HOME. If neither is found, adb in airtest will be used.It also supports directly specifying
adb_path
, for example:from airtest.core.android.android import Android, ADB adb = ADB(adb_path=r"D:\adb\adb.exe") dev = Android(serialno="5TSSMVBYUSEQNRY5", adb_path=r"D:\test\adb41\adb.exe")
v1.3.1 から以下の優先度で adb パスが自動的に決定されるようになりました。
- 現在動いている adb プロセス のパス
- 環境変数 ANDROID_HOME で指定されているディレクトリ内の adb のパス
- Airtest に内蔵されている adb のパス
また、手動で adb パスを与えることもできます。
どうやらこのロジックが Device Farm の adb ラッパーの仕組みとうまく噛み合わず、エラーが起きるようになってしまったようです。
Airtest の adb パス決定ロジックを詳しく調査する
まず、Device Farm の adb ラッパーの仕組みについて見てみます。Device Farm 上で which adb
を実行するとこのように返ってきます。
[DeviceFarm] which adb /opt/dev/android-sdk-linux/platform-tools/adb
ちなみに、Device Farm では $ANDROID_HOME は /opt/dev/android-sdk-linux
で定義されています。
また、Device Farm では adb に関して、以下の2プロセスが動いています。
{'exe': '/bin/bash', 'name': 'adb'} {'exe': '/opt/dev/android-sdk-linux/platform-tools/adb.orig', 'name': 'adb.orig'}
adb
は Device Farm 側で用意された adb のラッパースクリプト、 adb.orig
はオリジナルの adb バイナリファイルです。 adb
は /bin/bash
が実行していますが、実体は /opt/dev/android-sdk-linux/platform-tools/
に存在しています。
次に、Airtest の adb パスを探しに行くコードを見てみます。
@staticmethod def get_adb_path(): if platform.system() == "Windows": ADB_NAME = "adb.exe" else: ADB_NAME = "adb" # Check if adb process is already running for process in psutil.process_iter(['name', 'exe']): if process.info['name'] == ADB_NAME: return process.info['exe']
Airtest/airtest/core/android/adb.py at v1.3.1 · AirtestProject/Airtest · GitHub
このロジックだと name が adb のプロセスを探しに行きますが、Device Farm 上の adb スクリプトは bash のラッパースクリプトのため、psutil で取得される実行コマンドのパスは /bin/bash となってしまいます。
しかし、そのパスには adb の実体が存在しないので、Device Farm 上の Airtest では adb をうまく認識できない、という結果になっていました。
ADB_NAME = "adb.orig"
とすることで adb は実行できるようになると思いますが、得られる adb パスは オリジナルの adb.orig ファイルを示すのでラッパースクリプトを実行することができません。また、 Airtest のソースコードにパッチを当てることになり保守性が高くなってしまうため、それは選択しませんでした。
Airtest 上でエラーが起きている箇所はどこか?
エラーは、 airtest.core.api の init_device
を呼び出したときに発生します。
def init_device(platform="Android", uuid=None, **kwargs): """ Initialize device if not yet, and set as current device. :param platform: Android, IOS or Windows :param uuid: uuid for target device, e.g. serialno for Android, handle for Windows, uuid for iOS :param kwargs: Optional platform specific keyword args, e.g. `cap_method=JAVACAP` for Android :return: device instance :Example: >>> init_device(platform="Android",uuid="SJE5T17B17", cap_method="JAVACAP") >>> init_device(platform="Windows",uuid="123456") """ cls = import_device_cls(platform) dev = cls(uuid, **kwargs) # Add device instance in G and set as current device. G.add_device(dev) return dev
Airtest/airtest/core/api.py at v1.3.1 · AirtestProject/Airtest · GitHub
init_device は 自動テストの初期設定を行う auto_setup や デバイスの接続を行う connect_device に device の uri を指定したときに呼び出されます。
uri は "Android:///"
や "Android:///SJE5T17B17?cap_method=javacap&touch_method=adb"
のような形で表現されており、デバイスの OS と OS 固有のパラメータを入力するために指定します。
init_device
では、各 OS ごとのクラスからインスタンスが生成されます。
今回の場合、Android デバイスを使っているので、 Android クラスのインスタンスが生成されることになります。
次のコードは Android クラスの init
の一部です。
class Android(Device): """Android Device Class""" def __init__(self, serialno=None, host=None, cap_method=CAP_METHOD.MINICAP, touch_method=TOUCH_METHOD.MINITOUCH, ime_method=IME_METHOD.YOSEMITEIME, ori_method=ORI_METHOD.MINICAP, display_id=None, input_event=None, adb_path=None, name=None): super(Android, self).__init__() self.serialno = serialno or self.get_default_device() self._uuid = name or self.serialno self._cap_method = cap_method.upper() self._touch_method = touch_method.upper() self.ime_method = ime_method.upper() self.ori_method = ori_method.upper() self.display_id = display_id self.input_event = input_event # init adb self.adb = ADB(self.serialno, adb_path=adb_path, server_addr=host, display_id=self.display_id, input_event=self.input_event) self.adb.wait_for_device()
Airtest/airtest/core/android/android.py at v1.3.1 · AirtestProject/Airtest · GitHub
そして、エラーが発生している箇所は self.serialno = serialno or self.get_default_device()
の部分です。
今回、接続しているデバイスは1デバイスのみでしたので、初期設定に渡す uri は "Android:///"
だけ設定していました。
そのため、serialno は None
であり、デバイスを特定するために get_default_device
が呼ばれていました。
def get_default_device(self, adb_path=None): """ Get local default device when no serialno Returns: local device serialno """ if not ADB(adb_path=adb_path).devices(state="device"): raise IndexError("ADB devices not found") return ADB(adb_path=adb_path).devices(state="device")[0][0]
Airtest/airtest/core/android/android.py at v1.3.1 · AirtestProject/Airtest · GitHub
ここでは、要は adb devices
を実行し、マシンに接続されているデバイス一覧を取得し、その一番上のデバイスを接続先として指定しています。
ここで問題なのは、 get_default_device
の呼び出し元で adb_path
を指定しないまま実行していることです。
ADB クラスに adb_path = None を渡すとどうなるか?
このコードは ADB クラスの init
の一部です。
class ADB(object): """adb client object class""" _instances = [] status_device = "device" status_offline = "offline" SHELL_ENCODING = "utf-8" def __init__(self, serialno=None, adb_path=None, server_addr=None, display_id=None, input_event=None): self.serialno = serialno self.adb_path = adb_path or self.get_adb_path() self.display_id = display_id
Airtest/airtest/core/android/adb.py at v1.3.1 · AirtestProject/Airtest · GitHub
adb_pash = None
だった場合は、 self.get_adb_path()
が実行されます。
そして、この関数内で、「すでに動作している adb プロセスの実行パス」である /bin/bash
が取得されてしまいます。
ここで、冒頭の「Airtest の adb パス決定ロジック」に戻ります。
@staticmethod def get_adb_path(): if platform.system() == "Windows": ADB_NAME = "adb.exe" else: ADB_NAME = "adb" # Check if adb process is already running for process in psutil.process_iter(['name', 'exe']): if process.info['name'] == ADB_NAME: return process.info['exe']
今回のアップデートで adb_path
を外から入力し指定できるようになっていますが、デバイスの初期設定のタイミング(init_device
)で serialno = None
だった場合は、 adb_path
を指定することができません。
解決策
デバイス初期化時の uri を正確に書く
auto_setup
や connect_device
に対して、 "Android:///"
だけを渡していたことが今回の一番の問題でした。 serialno
さえあれば、接続するデバイスは一意に特定できるのです。更に、uri に adb_path
パラメータを含めれば、Android クラスに adb_path
を渡すことができるので、 Android クラスを介して呼び出される adb は指定した adb となります。つまり、以下のような uri にすることで Device Farm の adb_path
を指定して Android デバイスに接続することができます。
auto_setup(f"Android:///{serial_no}?adb_path={adb_path}")
肝心の serial_no
や adb_path
は テスト環境を構築するための testspec.yaml
(Device Farm の環境構築ファイル) で adb devices
や which adb
を使えば取得可能なので、それを環境変数かなにかで設定しておけばホストマシンからテストスクリプトである Python 側に渡すことができます。
v1.3.1 のアップデートで不要となった処理
Airtest に内蔵されている adb をホストマシンのものに差し替える対応
冒頭でも述べましたが、 v1.3.1 以前では、 Airtest に含まれている adb とシステム側の adb が競合した場合は差し替える対応を行う必要でした。
しかし、今回から任意の adb を用いる場合は adb_path
を指定する方式に変更されたので、差し替え対応が不要となりました。
従って、差し替えを行っているコードを削除しても正常に動作することが確認できました。
まとめ
adb 周りのアップデートがあった Airtest v1.3.1 へのアップデートを試みましたが、 Device Farm の adb ラッパーの仕組みと競合した結果、ADB Error が発生しました。Airtest のアップデートにより追加された adb パスを良い感じに探す仕組みでは、 Device Farm の adb.orig
を動かすプロセスを探しきれていませんでした。その問題は、Airtest のデバイス初期化時に Device Farm のデバイスの serial_no
と、adb プロセスの adb_path
を指定することで解決しました。そうすることで、Airtest は接続しているデバイスを始めから認識できるようになり、「デバイスを探す」動作と、そのための「adb パスを探す」動作を行う必要がなくなります。
今後も自動テストの取り組みは続けていくので、興味深い内容があれば記事にしたいと思います。この記事がゲームアプリケーションのテスト自動化に取り組まれる方の助けになれば嬉しいです。
明日は Koki Watanabe さんの記事です。お楽しみに!