开发

使用 Meta Quest 可脚本化测试服务来启用 E2E 测试

对 Meta Quest 开发者来说,设备上的自动化端到端(E2E)测试存在挑战,因为以用户为中心的设备功能可能会妨碍测试。例如,模态系统对话框可能会阻止启动测试,或者测试设备可能会在长时间的性能测试中进入自动睡眠模式。可以借助 Meta Quest 可脚本化测试服务禁用这些功能,可靠地运行自动化测试。此外,配置测试设备及在重复测试间重置设备所需的手动步骤已大幅减少,多数情况下甚至无需手动操作。该服务的目标是简化硬件配置和测试流程,让开发者能够更高效、更大规模地进行测试。

用例

Meta Quest OS v44 及更高版本包含测试服务,可让已认证的 Meta 开发者重置设备并禁用某些系统功能,以便开发者执行运行自动测试等任务。具体来说,新服务允许开发者使用简单的 ADB 命令在头戴设备上执行以下操作,却无需佩戴头戴设备:
  • 设置/获取属性:
    • 启用/禁用某些模态(阻止)系统对话框,包括允许进行 USB 调试允许连接的设备访问文件对话框。
    • 启用/禁用边界。
    • 启用/禁用自动睡眠。
  • 重置设备:
    • 将设备恢复出厂设置。
    • 配置 Wi-Fi。
    • 登录测试用户账户。
DEVELOPER USE ONLY
不支持非开发者使用这些功能,或将其用于 Meta Quest 软件开发以外的目的,因为这样做可能会缩短头戴设备的使用寿命。

前提条件

  • 如果未在环境设置期间安装 ADB 和快速启动,请进行安装。这可以通过安装相关的 Android SDK Platform Tools 程序包来完成。
  • 将头戴设备更新至 Meta Quest OS v44 或更高版本。
  • 如果尚无测试用户账户,请创建一个。记下测试用户的电子邮件、密码和 PIN 码。
  • 使用开发者账户或某个模拟用户账户登录,为首次使用做好准备。您可以通过戴上头戴设备并遵循标准用户设置流程,或使用 Meta Quest 开发者中心内的“设置新设备”功能来执行此操作。

使用方法

可以使用下面描述的 ADB 命令在脚本中访问这些功能,无需佩戴头戴设备。这些功能非常适合用于在本地运行自动化测试或管理设备实验室进行大规模测试。
测试服务有两个主要功能:
  • GET_PROPERTYSET_PROPERTY:这两个属性允许控制可能干扰自动化测试的服务。
  • WIPE_DEVICESETUP_FOR_TEST:这两条命令可以将设备重置为已知状态,确保连续测试不会互相干扰。
以下各节详细介绍了有关使用这些功能的更多信息。

控制干扰自动化的服务(SET/GET_PROPERTY)

SET_PROPERTY 和 GET_PROPERTY 命令同步执行并提供适当的返回值。SET_PROPERTY 返回命令是否成功,如果不成功,则返回相应的错误消息。GET_PROPERTY 仅返回一个包含所有受支持属性的当前设置值的包。
// 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
为了避免出现意料之外的负面影响,例如烧屏和电池快速耗尽,请在执行测试代码后重新启用任何已禁用的属性。将设备恢复出厂设置也会将这些属性都恢复为默认值。不过,重新启动并不会重置这些属性。

擦除设备来准备测试 (WIPE_DEVICE/SETUP_FOR_TEST)

为了隔离 E2E 测试运行并提高可重复性,最佳做法是擦除设备,以便从已知状态开始测试。这会降低本次测试运行受到前一次运行影响的可能性。本节旨在介绍如何通过链接对 WIPE_DEVICE 和 SETUP_FOR_TEST 的调用来实现这一点。示例流程如下所示:
// 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.

用于设备擦除和设置的 Python 帮手脚本

为了简化之前的流程,以下 Python 帮手脚本将所有 ADB 命令抽象为单个方法调用。使用适当的参数调用 setupDeviceForTest(),即会执行以下操作:
  1. 调用 WIPE_DEVICE。
  2. 等待设备重新启动。
  3. 调用 SETUP_FOR_TEST。
  4. 等待设备重新启动。
该方法返回时,设备就会准备好安装 APK 并运行测试。
import subprocess
import time

# 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 Meta 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/wifiPassword: 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", 25, 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)

常见问题

如果设备在 WIPE_DEVICE/SETUP_FOR_TEST 流程之后启动时出现一些操作系统对话框,这是否代表出现了问题?
只有这些对话框会阻止您以编程方式启动测试时(运行 adb installadb shell am start 时),才表示出现了问题。这些功能只是为了消除阻止对话框。可能仍会出现其他非阻止对话框。
SETUP_FOR_TEST 返回成功,但设备未重新启动。出了什么问题?
如果 SETUP_FOR_TEST 调用了返回 {Message=Login/Wifi Pending, Success=true} 但设备未重新启动,则通常意味着提供的 Wi-Fi 凭证或测试用户登录凭证存在问题。如需了解更多信息,在设备的 logcat 中搜索 Skip NUX 即可。例如,可能会看到如下错误消息,表明问题出在哪里:
  • [Skip NUX] Failed to connect to WiFi.
  • [Skip NUX] Failed to login.
注意,空格(例如 Wi-Fi SSID 中的空格)必须用反斜杠来转义。
我每天会调用 SETUP_FOR_TEST 数百次,但测试用户登录偶尔会失败,并且会显示含糊的失败消息。我该怎么办?
如果使用同一个测试用户每天登录几十次或几百次,身份验证服务可能会限制测试用户的登录。解决此限制的最佳方法是创建更多测试用户来轮流使用。
这些功能可以在未配置的设备上运行吗?
不可以。我们尚未为未配置的设备提供支持。
如果有多台设备连接到主机,可以正常运行吗?
可以。可以使用 -s 参数指定目标设备的序列号,将 ADB 命令路由到正确的设备。adb devices 会显示所有已连接设备的序列号。
为什么 ADB 无法识别设备已连接?
如“前提条件”小节所述,必须首先通过传统流程登录设备来启用开发者模式。这样才能让 ADB 识别设备已连接。如果使用传统方式将设备恢复出厂设置(例如 fastboot erase userdata),则会丢失此访问权限,必须重新登录。仅在必要时使用 WIPE_DEVICE 命令重置设备并立即进行有效的 SETUP_FOR_TEST 调用,即可避免这种情况。

其他测试资源

Quest 可脚本化测试服务可让设备准备好运行 E2E 测试,但服务本身并不会为您运行和验证测试。所以,您需要一个测试驱动程序或测试框架。有多个选项可用,包括:

另请参阅