diff options
Diffstat (limited to 'automated')
9 files changed, 661 insertions, 0 deletions
diff --git a/automated/android/multinode/connect-to-remote-adb-tcpip-devices.yaml b/automated/android/multinode/connect-to-remote-adb-tcpip-devices.yaml new file mode 100644 index 0000000..3dc2e42 --- /dev/null +++ b/automated/android/multinode/connect-to-remote-adb-tcpip-devices.yaml @@ -0,0 +1,24 @@ +metadata: + name: connect-to-remote-adb-tcpip-devices + format: "Lava-Test-Shell Test Definition 1.0" + description: "adb MultiNode setup: connect to remote devices made accessible via adb TCP/IP." + maintainer: + - karsten@fairphone.com + - softwareteam@fairphone.com + os: + - debian + - ubuntu + devices: + - lxc + scope: + - functional + +params: + ADB_CONNECT_TIMEOUT_SECS: "60" + DEVICE_WORKER_MAPPING_FILE: "/tmp/deviceWorkerMapping" + +run: + steps: + - . ./automated/lib/sh-test-lib + - . ./automated/lib/android-multinode-test-lib + - connect_to_remote_adb_tcpip_devices "${ADB_CONNECT_TIMEOUT_SECS}" "${DEVICE_WORKER_MAPPING_FILE}" diff --git a/automated/android/multinode/release-remote-adb-tcpip-devices.yaml b/automated/android/multinode/release-remote-adb-tcpip-devices.yaml new file mode 100644 index 0000000..c28c22e --- /dev/null +++ b/automated/android/multinode/release-remote-adb-tcpip-devices.yaml @@ -0,0 +1,20 @@ +metadata: + name: release-remote-adb-tcpip-devices + format: "Lava-Test-Shell Test Definition 1.0" + description: "Disconnect from remote adb devices and cleanup." + maintainer: + - karsten@fairphone.com + - softwareteam@fairphone.com + os: + - debian + - ubuntu + devices: + - lxc + scope: + - functional + +run: + steps: + - lava-sync release_dut + # Cleanup adb server: LAVA expects only one device connected to adb. + - adb kill-server diff --git a/automated/android/multinode/remote-adb-devices-smoke-test.yaml b/automated/android/multinode/remote-adb-devices-smoke-test.yaml new file mode 100644 index 0000000..838b866 --- /dev/null +++ b/automated/android/multinode/remote-adb-devices-smoke-test.yaml @@ -0,0 +1,27 @@ +metadata: + name: remote-adb-devices-smoke-test + format: "Lava-Test-Shell Test Definition 1.0" + description: "Smoke test demonstrating access to adb devices over TCP/IP." + maintainer: + - karsten@fairphone.com + - softwareteam@fairphone.com + os: + - debian + - ubuntu + devices: + - lxc + scope: + - functional + +params: + DEVICE_WORKER_MAPPING_FILE: "/tmp/deviceWorkerMapping" + +run: + steps: + - device_worker_mapping="$(cat "${DEVICE_WORKER_MAPPING_FILE}")" + - | + for device_to_worker in ${device_worker_mapping}; do + device="$(echo ${device_to_worker} | cut -d';' -f1)" + echo "${device}: $(adb -s "${device}" shell service call iphonesubinfo 1 | \ + grep -oE '(\.[0-9])|([0-9]\.)' | grep -oE '[0-9]' | tr -d '\n')" + done diff --git a/automated/android/multinode/share-local-device-over-adb-tcpip.yaml b/automated/android/multinode/share-local-device-over-adb-tcpip.yaml new file mode 100644 index 0000000..9ede1df --- /dev/null +++ b/automated/android/multinode/share-local-device-over-adb-tcpip.yaml @@ -0,0 +1,36 @@ +metadata: + name: share-local-device-over-adb-tcpip + format: "Lava-Test-Shell Test Definition 1.0" + description: "adb MultiNode setup: make local device remotely accessible via adb TCP/IP. + Handles the device over to a role that responds to the following synchronization steps: + - lava-sync start_handover + - lava-send dut_address dut_address=${dut_address} + - lava-sync finish_handover" + maintainer: + - karsten@fairphone.com + - softwareteam@fairphone.com + os: + - debian + - ubuntu + devices: + - lxc + scope: + - functional + +params: + ADB_PORT: "5555" + ADB_TCPIP_ATTEMPTS: "5" + TIMEOUT_SECS: "60" + RAISE_ON_FAILURE: "true" + +run: + steps: + - . ./automated/lib/sh-test-lib + - . ./automated/lib/android-test-lib + - . ./automated/lib/android-multinode-test-lib + - ret_val=0 + - share_local_device_over_adb_tcpip "${ADB_TCPIP_ATTEMPTS}" "${TIMEOUT_SECS}" "${ADB_PORT}" || ret_val=$? + - | + if [ "${ret_val}" -ne 0 -a "${RAISE_ON_FAILURE}" = "true" ]; then + lava-test-raise "Could not share device of adb tcpip." + fi diff --git a/automated/android/multinode/wait-and-keep-local-device-accessible.yaml b/automated/android/multinode/wait-and-keep-local-device-accessible.yaml new file mode 100644 index 0000000..35460d0 --- /dev/null +++ b/automated/android/multinode/wait-and-keep-local-device-accessible.yaml @@ -0,0 +1,98 @@ +metadata: + name: wait-and-keep-local-device-accessible + format: "Lava-Test-Shell Test Definition 1.0" + description: "Continuously wait for MultiNode messages from a remote role (master) and make the + locally connected device accessible again when it is lost for the remote role." + maintainer: + - karsten@fairphone.com + - softwareteam@fairphone.com + os: + - debian + - ubuntu + devices: + - lxc + scope: + - functional + +params: + ADB_PORT: "5555" + BOOT_TIMEOUT_SECS: "900" + NETWORK_TIMEOUT_SECS: "300" + ADB_TCPIP_ATTEMPTS: "5" + ADB_CONNECT_TEST_TIMEOUT_SECS: "60" + ANDROID_ENABLE_WIFI: "true" + +run: + steps: + - lava-install-packages --no-install-recommends python3-pip python3-setuptools python3-wheel + - pip3 install -q uiautomator + - . ./automated/lib/sh-test-lib + - . ./automated/lib/android-test-lib + - . ./automated/lib/android-multinode-test-lib + - lava-test-set start keepAlive + - | + reconnect_device() { + timeout 10 fastboot reboot || true + + local ret_val=0 + sh -c ". ./automated/lib/sh-test-lib && . ./automated/lib/android-test-lib \ + && wait_boot_completed \"${BOOT_TIMEOUT_SECS}\"" \ + || ret_val=$? + + if [ "${ret_val}" -ne 0 ]; then + result=false + echo "WARNING: Reconnect attempt failed: target did not boot up or is not accessible." + return + fi + + if [ "${ANDROID_ENABLE_WIFI}" = "true" ]; then + ./automated/lib/android_ui_wifi.py -a set_wifi_state on || ret_val=$? + if [ "${ret_val}" -ne 0 ]; then + echo "WARNING: Cannot ensure that Wi-Fi is enabled in the device settings; UI automation failed." + fi + fi + + ret_val=0 + sh -c ". ./automated/lib/sh-test-lib && . ./automated/lib/android-test-lib \ + && . ./automated/lib/android-multinode-test-lib \ + && wait_network_connected \"${NETWORK_TIMEOUT_SECS}\" \ + && open_adb_tcpip_on_local_device \ + \"${ADB_TCPIP_ATTEMPTS}\" \"${ADB_CONNECT_TEST_TIMEOUT_SECS}\" \"${ADB_PORT}\"" \ + || ret_val=$? + + if [ "${ret_val}" -ne 0 ]; then + result=false + echo "WARNING: Reconnect attempt failed." + fi + } + - iteration=1 + - | + while true; do + lava-wait master-sync-$(lava-self)-${iteration} + + command="$(cat /tmp/lava_multi_node_cache.txt | grep "command" | sed 's/.*command=//' | grep -v '^$')" + + result="pass" + + case "${command}" in + continue) + ;; + release) + break + ;; + reconnect) + echo "Reconnect requested by master." + adb kill-server || true + adb devices || true + reconnect_device + ;; + *) + lava-test-raise "Script error. Unexpected message from master to worker, command=${command}" + esac + + lava-send worker-sync-$(lava-self)-${iteration} result=$result + + iteration="$(expr ${iteration} + 1)" + done + - echo "master released the device." + - lava-test-set stop diff --git a/automated/android/multinode/wait-for-release-and-reset.yaml b/automated/android/multinode/wait-for-release-and-reset.yaml new file mode 100644 index 0000000..78a52b4 --- /dev/null +++ b/automated/android/multinode/wait-for-release-and-reset.yaml @@ -0,0 +1,21 @@ +metadata: + name: wait-for-release-and-reset + format: "Lava-Test-Shell Test Definition 1.0" + description: "Wait until a remote MultiNode role (master) requests to release the device. + Then, bring the device back into adb USB state." + maintainer: + - karsten@fairphone.com + - softwareteam@fairphone.com + os: + - debian + - ubuntu + devices: + - lxc + scope: + - functional + +run: + steps: + - lava-sync release_dut + - adb kill-server || true + - adb usb diff --git a/automated/lib/android-multinode-test-lib b/automated/lib/android-multinode-test-lib new file mode 100644 index 0000000..d5fe843 --- /dev/null +++ b/automated/lib/android-multinode-test-lib @@ -0,0 +1,213 @@ +#!/bin/sh + +# Configure adb to accept adb connections via TCP/IP and make sure that the device is actually +# accessible. +# This function assumes that the device has a network address. Guards around `adb tcpip` and test +# connection setups using `adb connect` are used to check if the device is reachable after this +# call. +# Globals: +# dut_address Set to "ip_address:adb_port" by this function +# Arguments: +# adb_tcpip_attempts Number of tries for enabling adb TCP/IP mode on the device +# timeout_secs Timeout for waiting for getting the IP address from the device +# adb_port Network port to use for adb TCP/IP +# Returns: +# 0 only if the device is accessible via adb TCP/IP, 1 otherwise. +open_adb_tcpip_on_local_device() { + [ "$#" -lt 2 -o "$#" -gt 3 ] && \ + error_fatal "Usage: open_adb_tcpip_on_local_device adb_tcpip_attempts timeout_secs [adb_port]" + local adb_tcpip_attempts="$1" + local timeout_secs="$2" + local adb_port="$3" + if [ -z "${adb_port}" ]; then + local adb_port=5555 # default port assumed by adb connect + fi + + local end=$(( $(date +%s) + timeout_secs )) + + local ret_val=0 + local ip_address + ip_address="$(get_ip_address ${timeout_secs})" || ret_val=$? + if [ "${ret_val}" -ne 0 ]; then + warn_msg "get_ip_address failed unexpectedly." + return 1 + fi + if [ -z "${ip_address}" ]; then + warn_msg "Device has no ip address (network not connected?)" + return 1 + fi + dut_address="${ip_address}:${adb_port}" + + # adb tcpip may fail with different reasons + # (e.g., "error: protocol fault (couldn't read status): Connection reset by peer"). + # Just hope that it works after a few retries. + local adb_tcpip_retry_wait_secs=10 + local attempt=0 + while [ "${attempt}" -lt "${adb_tcpip_attempts}" -a "$(date +%s)" -lt "$end" ]; do + ret_val=0 + adb tcpip "${adb_port}" || ret_val=$? + if [ "${ret_val}" -eq 0 ]; then + break + fi + info_msg "adb tcpip apparently failed. Retrying in a moment..." + sleep "${adb_tcpip_retry_wait_secs}" + adb usb || true # In between, make sure to have some default state. + done + + if [ "${ret_val}" -ne 0 ]; then + warn_msg "Could not prepare the device for adb TCP/IP connections: adb tcpip failed." + return 1 + fi + + # `adb tcpip` sometimes takes some time + # (on some builds, up to 10 seconds were observed) + local success=false + while [ "$(date +%s)" -lt "$end" ]; do + if [ $(adb connect "${dut_address}" | grep -c '^connected to ') -eq 1 ]; then + success=true + break + fi + sleep 1 + done + + # Make sure the device is not reserved to the local adb server. + adb disconnect "${dut_address}" >/dev/null 2>&1 || true + + if [ "${success}" = false ]; then + warn_msg "Could not prepare the device for adb TCP/IP connections: device is not reachable via network." + return 1 + fi +} + +# Make this device accessible via adb TCP/IP and send its address via handshake to a waiting role. +# NOTE: This function must only be called once per role per test submission, as LAVA does not allow +# to use the same MultiNode message ID multiple times. +# One job instance must call connect_to_remote_adb_tcpip_devices to receive the send addresses and +# complete the handshake. +# See open_adb_tcpip_on_local_device and connect_to_remote_adb_tcpip_devices +# Globals: +# dut_address Set to "ip_address:adb_port" by this function +# Arguments: +# adb_tcpip_attempts Number of tries for establishing enabling adb TCP/IP mode on the device +# timeout_secs Timeout for waiting for getting the IP address from the device +# adb_port Network port to use for adb TCP/IP +# Returns: +# 0 only if the device is accessible via adb TCP/IP, 1 otherwise. +share_local_device_over_adb_tcpip() { + [ "$#" -lt 2 -o "$#" -gt 3 ] && \ + error_fatal "Usage: share_local_device_over_adb_tcpip adb_tcpip_attempts timeout_secs [adb_port]" + local adb_tcpip_attempts="$1" + local timeout_secs="$2" + local adb_port="$3" + + local ret_val=0 + open_adb_tcpip_on_local_device "${adb_tcpip_attempts}" "${timeout_secs}" "${adb_port}" || ret_val=$? + if [ "${ret_val}" -ne 0 ]; then + return "${ret_val}" + fi + + lava-sync start_handover + lava-send dut_address dut_address="${dut_address}" + lava-sync finish_handover +} + +# Counterpart to share_local_device_over_adb_tcpip +# Wait for other job instances to send their device address, guarded by a handshake for +# synchronization. +# Globals: +# None +# Arguments: +# adb_connect_timeout_secs Timeout for waiting for getting the IP address from the device +# device_worker_mapping_file File to store a mapping between devices and their LAVA worker host +# in the format 'serial_or_address;worker_host_id'. This file is relevant for following +# functions to communicate with the devices or workers. +# Optional: This file will not be created if no path is specified. +# Returns: +# 0 on success 1 otherwise. +connect_to_remote_adb_tcpip_devices() { + [ "$#" -lt 1 -o "$#" -gt 2 ] && \ + error_fatal "Usage: connect_to_remote_adb_tcpip_devices adb_connect_timeout_secs [device_worker_mapping_file]" + + local adb_connect_timeout_secs="$1" + local device_worker_mapping_file="$2" + + lava-sync start_handover + # For lava-wait-all, all involved nodes must invoke lava-send with the same message id, + # otherwise lava-wait-all would lead to a dead lock. + # However, only the nodes that make their device accessible (workers) add the value + # dut_address="address:port". + lava-send dut_address + lava-wait-all dut_address + + # The MultiNode cache file might not exist if there is no other worker. + local cache_lines + local device_worker_mapping="" + if [ -f "/tmp/lava_multi_node_cache.txt" ]; then + cache_lines="$(cat "/tmp/lava_multi_node_cache.txt" | grep "dut_address" | grep -v '^$' || true)" + + for line in ${cache_lines}; do + # <worker_job_id>:dut_address=<dut_address> + local dut_address="$(echo "$line"| sed 's/.*dut_address=//')" + local worker_host="$(echo "$line" | cut -d: -f1)" + device_worker_mapping="${device_worker_mapping}${dut_address};${worker_host}\n" + done + device_worker_mapping="$(printf "${device_worker_mapping}" | grep -v '^$' || true)" + fi + + lava-sync finish_handover + # adb is not super reliable, it too often sees connected and authorized devices as "offline" + adb kill-server || true + + # Connect to remote devices and wait until they appear online + + for device_to_worker in ${device_worker_mapping}; do + local device="$(echo ${device_to_worker} | cut -d';' -f1)" + for i in $(seq 5); do + local ret_val=0 + adb connect "${device}" || ret_val="$?" + if [ "${ret_val}" -eq 0 ]; then + break + fi + adb disconnect "${device}" || true + warn_msg "adb connect failed. Retrying in a minute..." + sleep 1m + done + done + + for device_to_worker in ${device_worker_mapping}; do + local device="$(echo ${device_to_worker} | cut -d';' -f1)" + if ! timeout "${adb_connect_timeout_secs}" adb -s "${device}" wait-for-device; then + warn_msg "adb wait-for-device for ${device} timed out after ${adb_connect_timeout_secs} seconds." + return 1 + fi + done + + local num_remote_devices="$(echo "${device_worker_mapping}" | wc -l)" + info_msg "All ${num_remote_devices} remote devices are connected and online." + + info_msg "Now adding devices locally connected via USB." + local connected_devices + local ret_val=0 + connected_devices="$(adb devices | grep -E '^([:\.[:alnum:]]+)\s+device$' | cut -f1)" || ret_val=$? + if [ "${ret_val}" -ne 0 ]; then + warn_msg "\"adb devices\" did not exit cleanly. Cannot reliably determine the list of connected devices." + fi + local remote_only_mapping="${device_worker_mapping}" + device_worker_mapping="${device_worker_mapping}\n" + for device in ${connected_devices}; do + if [ "$(echo "${remote_only_mapping}" | cut -d';' -f1 | grep -xc "${device}")" -eq 0 ]; then + device_worker_mapping="${device_worker_mapping}${device};\n" + info_msg "Local device: ${device}" + fi + done + device_worker_mapping="$(printf "${device_worker_mapping}" | grep -v '^$')" + + if [ "${device_worker_mapping_file}" ]; then + # Make mapping between attached DUTs and worker job ids and accessible to subsequent tests: + echo "${device_worker_mapping}" > "${device_worker_mapping_file}" + info_msg "Mapping between devices and to worker job ids stored in ${device_worker_mapping_file}:" + info_msg "${device_worker_mapping}" + else + info_msg "NOT storing device to worker job id mapping, empty filename specified." + fi +} diff --git a/automated/lib/android_adb_wrapper.py b/automated/lib/android_adb_wrapper.py new file mode 100644 index 0000000..64fcbb1 --- /dev/null +++ b/automated/lib/android_adb_wrapper.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +import re +import subprocess +import time + + +ADB_DEVICES_PATTERN = re.compile(r"^([a-z0-9-]+)\s+device$", flags=re.M) + + +class DeviceCommandError(BaseException): + """An error happened while sending a command to a device.""" + + def __init__(self, serial, command, error_message): + self.serial = serial + self.command = command + self.error_message = error_message + message = "Command `{}` failed on {}: {}".format( + command, serial, error_message + ) + super(DeviceCommandError, self).__init__(message) + + +def adb(*args, serial=None, raise_on_error=True): + """Run ADB command attached to serial. + + Example: + >>> process = adb('shell', 'getprop', 'ro.build.fingerprint', serial='aserialnumber') + >>> process.returncode + 0 + >>> process.stdout.strip() + 'ExampleVendor/Device/version/tags' + + :param *args: + List of options to ADB (including command). + :param str serial: + Identifier for ADB connection to device. + :param raise_on_error bool: + Whether to raise a DeviceCommandError exception if the return code is + less than 0. + :returns subprocess.CompletedProcess: + Completed process. + :raises DeviceCommandError: + If the command failed. + """ + + # Make sure the adb server is started to avoid the infamous "out of date" + # message that pollutes stdout. + ret = subprocess.run( + ["adb", "start-server"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + if ret.returncode < 0: + if raise_on_error: + raise DeviceCommandError( + serial if serial else "??", str(args), ret.stderr + ) + else: + return None + + command = ["adb"] + if serial: + command += ["-s", serial] + if args: + command += list(args) + ret = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + if raise_on_error and ret.returncode < 0: + raise DeviceCommandError( + serial if serial else "??", str(args), ret.stderr + ) + + return ret + + +def list_devices(): + """List serial numbers of devices attached to adb. + + Raises: + DeviceCommandError: If the underlying adb command failed. + """ + process = adb("devices") + return ADB_DEVICES_PATTERN.findall(process.stdout) + + +def unlock(dut): + """Wake-up the device and unlock it. + + Raises: + DeviceCommandError: If the underlying adb commands failed. + """ + if not dut.info["screenOn"]: + adb("shell", "input keyevent KEYCODE_POWER", serial=dut.serial) + time.sleep(1) + + # Make sure we are on the home screen. + adb("shell", "input keyevent KEYCODE_HOME", serial=dut.serial) + # The KEYCODE_MENU input is enough to unlock a "swipe up to unlock" + # lockscreen on Android 6, but unfortunately not Android 7. So we use a + # swipe up (that depends on the screen resolution) instead. + adb("shell", "input touchscreen swipe 930 880 930 380", serial=dut.serial) + time.sleep(1) + adb("shell", "input keyevent KEYCODE_HOME", serial=dut.serial) diff --git a/automated/lib/android_ui_wifi.py b/automated/lib/android_ui_wifi.py new file mode 100755 index 0000000..dd32166 --- /dev/null +++ b/automated/lib/android_ui_wifi.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +from android_adb_wrapper import * +import argparse +import sys +from uiautomator import Device + + +def set_wifi_state(dut, turn_on): + """Turn WiFi on or off. + + This checks the current WiFi settings and turns it on or off. It does + nothing if the settings are already in the desired state. + + Parameters: + dut (Device): The device object. + enabled: Boolean, true for on, false for off + Raises: + DeviceCommandError: If the UI automation fails. + """ + # Open the Wi-Fi settings + adb( + "shell", + ("am start -a android.settings.WIFI_SETTINGS " "--activity-clear-task"), + serial=dut.serial, + ) + + # Check if there is an option to turn WiFi on or off + wifi_enabler = dut( + text="OFF", resourceId="com.android.settings:id/switch_widget" + ) + wifi_disabler = dut( + text="ON", resourceId="com.android.settings:id/switch_widget" + ) + + if not wifi_enabler.exists and not wifi_disabler.exists: + raise DeviceCommandError( + dut, + "UI: set Wi-Fi state", + "Neither switch for turning Wi-Fi on nor for turning it off are present.", + ) + if wifi_enabler.exists and wifi_disabler.exists: + raise DeviceCommandError( + dut, + "UI: set Wi-Fi state", + "Unexpected UI: Both, a switch for turning Wi-Fi on and for turning it off are present.", + ) + + if turn_on: + if wifi_enabler.exists: + wifi_enabler.click() + else: + print("Wi-Fi is already enabled.") + else: + if wifi_disabler.exists: + wifi_disabler.click() + else: + print("Wi-Fi is already disabled.") + + # Leave the settings + dut.press.back() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-a", + dest="ACTION", + required=True, + nargs="+", + help="Action to perform. Following action is currently implemented: \ + set_wifi_state <on|off>", + ) + parser.add_argument( + "-s", + dest="SERIALS", + nargs="+", + help="Serial numbers of devices to configure. \ + If not present, all available devices will be configured.", + ) + args = parser.parse_args() + + if args.ACTION[0] != "set_wifi_state" or args.ACTION[1] not in ( + "on", + "off", + ): + print( + "ERROR: Specified ACTION is not supported: {}".format(args.ACTION), + file=sys.stderr, + ) + sys.exit(1) + + serials = args.SERIALS if args.SERIALS is not None else list_devices() + + for serial in serials: + print("Configuring device {}…".format(serial)) + + dut = Device(serial) + # Work around the not-so-easy Device class + dut.serial = serial + + try: + unlock(dut) + + set_wifi_state(dut, args.ACTION[1] == "on") + + except DeviceCommandError as e: + print("ERROR {}".format(e), file=sys.stderr) + + +if __name__ == "__main__": + main() |