Meta Quest開発者向けの、デバイス上で自動化されるエンドツーエンド(E2E)テストには課題があります。ユーザー重視のデバイス機能が、テストの障害になることがあるためです。例えば、モーダルシステムダイアログによってテストの起動が妨げられたり、長時間にわたるパフォーマンステストの最中にテストデバイスが自動スリープに入ったりする可能性があります。Meta Questスクリプト化対応テストサービスを使用すると、これらの機能を無効化し、自動化されたテストを確実に実行できます。さらに、デバイスをテスト用に設定したり反復テスト用にリセットしたりするために必要な手動のステップの数が大幅に削減され、完全に撤廃されるケースも多数あります。このサービスの目的は、開発者がより効率的かつ大規模にテストを実施できるようにするため、ハードウェア構成やテストプロセスを合理化することです。
ユースケース
Meta Quest OS v44には、認定されたMeta開発者が、自動化テストの実行などの開発者タスクを円滑に実施することを目的として、自身のデバイスをリセットしたり特定のシステム機能を無効にすることを可能にしたりするテストサービスが組み込まれています。特に、新しいサービスでは、開発者はヘッドセットを装着しなくても、ヘッドセット上でシンプルなADBコマンドを使って以下を行うことができます。
プロパティを設定/取得
[Allow USB Debugging (USBデバッグを許可する)]ダイアログや、[Allow connected device to access files (接続されているデバイスによるファイルへのアクセスを許可する)]ダイアログなど、特定のモーダル(ブロッキング)システムダイアログを有効/無効にする。
開発者アカウントまたは作成したいずれかのテストユーザーアカウントでログインし、デバイスを初回の使用に備えて準備します。これは、ヘッドセットを装着して標準ユーザーの設定フローに従うか、Meta Quest開発者ハブの「Setup New Device (新デバイスの設定)」機能を使って行うことができます。
// Disable three system features that would otherwise interfere with an E2E test.
// Note that a developer (or test user) must be logged on and you must specify
// the Store PIN associated with that logged in account.
> adb shell content call --uri content://com.oculus.rc --method SET_PROPERTY \
--extra 'disable_guardian:b:true' \
--extra 'disable_dialogs:b:true' \
--extra 'disable_autosleep:b:true' \
--extra 'PIN:s:1234'
Result: Bundle[{Success=true}]
// Query the currently set property values.
> adb shell content call --uri content://com.oculus.rc --method GET_PROPERTY
Result: Bundle[{disable_guardian=true, disable_dialogs=true, disable_autosleep=true}]
// Simulate putting on the headset by triggering the proximity sensor.
> adb shell am broadcast -a com.oculus.vrpowermanager.prox_close
// Install your APK and execute your test here. E.g.,
> adb install /path/to/my_app.apk
> adb shell am start -S com.my.app.packagename/.MainTestActivity
// Re-enable the system features.
> adb shell content call --uri content://com.oculus.rc --method SET_PROPERTY \
--extra 'disable_guardian:b:false' \
--extra 'disable_dialogs:b:false' \
--extra 'disable_autosleep:b:false' \
--extra 'PIN:s:1234'
// Simulate taking the headset off by triggering the proximity sensor.
> adb shell am broadcast -a com.oculus.vrpowermanager.prox_far
// Factory reset the device. Note that if a user is logged on, it must be a
// developer (or test user) and you must specify the Store PIN associated with
// that account.
> adb shell content call --uri content://com.oculus.rc --method WIPE_DEVICE \
--extra 'PIN:s:1234'
Result: Bundle[{Message=WIPE_DATA Pending, Success=true}]
// ...wait for the device to re-start…
// Login your test user (it must be a test user) and connect to local wifi.
> adb shell content call --uri content://com.oculus.rc --method SETUP_FOR_TEST \
--extra 'WIFI_SSID:s:my_wifi_ssid' \
--extra 'WIFI_PWD:s:my_wifi_password' \
--extra 'WIFI_AUTH:s:WPA' \
--extra 'EMAIL:s:my_test_user@tfbnw.net' \
--extra 'PWD:s:my_test_user_password'
// ...wait for the device to re-start...
// [Optional] Disable auto-sleep. Note that disable_guardian and disable_dialogs
// was already completed as part of the SETUP_FOR_TEST call above.
> adb shell content call --uri content://com.oculus.rc --method SET_PROPERTY \
--extra 'disable_autosleep:b:true' \
--extra 'PIN:s:1234'
Result: Bundle[{Success=true}]
// Install your APK and execute your test here. E.g.,
> adb install /path/to/my_app.apk
> adb shell am start -S com.my.app.packagename/.MainTestActivity
// Repeat the process again for your next test, starting with WIPE_DEVICE.
# Ready the connected device to run a test by factory resetting the device,
# connecting to wifi and logging in a test user.
#
# testUserEmail/testUserPassword: specify the credentials of an existing Oculus test
# user (https://developers.meta.com/horizon/resources/test-users). This is the
# user that will be logged into the device after it's been reset. Generally, these
# email addresses are in the @tfbnw.net domain.
#
# testUserPin: if the device is in a logged in state, then it must be a developer
# or test user that's logged in AND (for security purposes) this parameter must
# specify that user's Store PIN. If calling this method when the device
# is in a non-logged in state, this parameter is ignored.
# Note that the logged in user may be different from the test user specified above.
#
# wifiSSID/wifiPassowrd: specify valid credentials for a wifi network that is in range..
#
# deviceId: specify the device's serial number--only required if multiple headsets
# are connected to the host machine.
def setupDeviceForTest(
testUserEmail,
testUserPassword,
testUserPin,
wifiSSID,
wifiPassword,
deviceId=None,
):
# Factory reset the device to get a clean test environment. This is a requirement
# of the subsequent SETUP_FOR_TEST call. WIPE_DEVICE will reset the device but also
# preserve ADB access after the subsequent reboot.
result = __runAdbShell(
f"content call --uri content://com.oculus.rc --method WIPE_DEVICE "
f"--extra 'PIN:s:{testUserPin}'",
deviceId,
)
if result.returncode == 0 and "Success=true" in result.stdout:
__runAdbCommand("wait-for-disconnect", deviceId)
__waitForDeviceBootCompleted(40, deviceId)
else:
print(
f"WIPE_DEVICE call failed: returncode={result.returncode}; {result.stdout}; {result.stderr};"
)
return -1
# Connect to wifi and log in the test user.
result = __runAdbShell(
f"content call --uri content://com.oculus.rc --method SETUP_FOR_TEST "
f"--extra 'WIFI_SSID:s:{wifiSSID}' --extra 'WIFI_PWD:s:{wifiPassword}' --extra 'WIFI_AUTH:s:WPA' "
f"--extra 'EMAIL:s:{testUserEmail}' --extra 'PWD:s:{testUserPassword}'",
deviceId,
)
if result.returncode == 0 and "Success=true" in result.stdout:
__runAdbCommand("wait-for-disconnect", deviceId)
__waitForDeviceBootCompleted(40, deviceId)
__waitForDumpSys("Horizon logged in: true", 10, deviceId)
else:
print(
f"SETUP_FOR_TEST call failed: returncode={result.returncode}; {result.stdout}; {result.stderr};"
)
return -1
# Install the specified APK and launch the app.
def installAndStartApp(apkPath, packageName, activityName, deviceId=None):
__runAdbCommand(f"install {apkPath}", deviceId)
__runAdbShell(f"am start -S {packageName}/{activityName}", deviceId)
def sleepHeadset(deviceId=None):
__runAdbShell("input keyevent POWER", deviceId)
def __runShellCommand(command):
print(f"SHELL: {command}")
split = command.split()
result = subprocess.run(split, capture_output=True, text=True)
return result
def __getDeviceArg(deviceId):
if deviceId is None:
return " "
else:
return f" -s {deviceId} "
def __runAdbShell(command, deviceId):
return __runShellCommand("adb" + __getDeviceArg(deviceId) + "shell " + command)
def __runAdbCommand(command, deviceId):
return __runShellCommand("adb" + __getDeviceArg(deviceId) + command)
def __waitForProperty(property, maxSeconds, deviceId):
print(f"Waiting for {property} to turn true")
start = time.time()
while "1" not in __runAdbShell(f"getprop {property}", deviceId).stdout:
if time.time() - start > maxSeconds:
raise RuntimeError(f"timed out while waiting for {property} to turn true")
__sleep(2)
print(f"{property} is true")
def __waitForDeviceBootCompleted(maxSeconds, deviceId):
__runAdbCommand("wait-for-device", deviceId)
__waitForProperty("sys.boot_completed", maxSeconds, deviceId)
__sleep(2)
def __waitForCommand(command, targetString, maxSeconds):
print(f"Waiting for command '{command}' to return '{targetString}'")
start = time.time()
while True:
result = __runShellCommand(command)
# Break the loop if we find the target string in stdout or stderr.
if (
result.stderr.find(targetString) >= 0
or result.stdout.find(targetString) >= 0
):
break
# Raise an exception if we don't find the target in time.
if time.time() - start > maxSeconds:
raise RuntimeError(
f"timed out while waiting for command '{command}' to return '{targetString}'. \n"
+ f"STDOUT: {result.stdout} \n"
+ f"STDERR: {result.stderr} \n"
)
__sleep(2)
print("Found return string: " + targetString)
def __waitForDumpSys(targetString, maxSeconds, deviceId):
__waitForCommand(
"adb" + __getDeviceArg(deviceId) + "shell dumpsys CompanionService",
targetString,
maxSeconds,
)
def __sleep(seconds):
print(f"SLEEP {seconds}s")
time.sleep(seconds)