aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKarsten Tausche <karsten@fairphone.com>2018-06-19 15:34:01 +0200
committerKarsten Tausche <karsten@fairphone.com>2019-02-12 17:15:27 +0100
commit81ffea2f5d0a6cb72f0709f1943a2fd3933f5798 (patch)
treed47eb352e0cdf7c71bbf042ad13e9b88d4cb9f30
parentad557e01524d4b06f61e31072c2127fdd7077aa9 (diff)
Add MultiNode scripts for running tests on multiple adb TCP/IP devices
With these test shells and functions, test implementations can make use of multiple devices by connecting remote devices via adb TCP/IP connections. On the MultiNode side, one master role would execute the actual test shell, whereas multiple instances of a worker role allow for remote access to their device adb TCP/IP. The following steps are required: * Start with completely booted and network attached devices. * workers: share-local-device-over-adb-tcpip.yaml - run `adb tcpip` on their local devices and share their IP address with the master * master: connect-to-remote-adb-tcpip-devices.yaml - Connects to all devices shared by the workers via `adb connect` (while also using its own local USB attached device) * workers: wait-and-keep-local-device-accessible.yaml - Wait for commands from the master -- mostly wait and do nothing else, but may need to reset their devices/network connections to make them accessible again if they got lost. * master: remote-adb-devices-smoke-test.yaml - Dummy action for the actual test job. A proper test job would execute some potentially long running tests on all available devices, instruct the worker to reconnect on connection lost etc. * master: release-remote-adb-tcpip-devices.yaml - Test executions end with the release command, so that workers exit their event loop. * workers: wait-for-release-and-reset.yaml - Final synchronization point between worker and the master, so that the master is in control when the workers are shutting down. Brings worker devices back into adb USB mode, so that they are usable for regular local test jobs. Change-Id: I23f22344b9bd758d3898d4345204157cecd7d624 Depends-On: Icd68e9de5a349880c52ec06229cd3f8bcb8eeecc Signed-off-by: Karsten Tausche <karsten@fairphone.com>
-rw-r--r--automated/android/multinode/connect-to-remote-adb-tcpip-devices.yaml24
-rw-r--r--automated/android/multinode/release-remote-adb-tcpip-devices.yaml20
-rw-r--r--automated/android/multinode/remote-adb-devices-smoke-test.yaml27
-rw-r--r--automated/android/multinode/share-local-device-over-adb-tcpip.yaml36
-rw-r--r--automated/android/multinode/wait-and-keep-local-device-accessible.yaml98
-rw-r--r--automated/android/multinode/wait-for-release-and-reset.yaml21
-rw-r--r--automated/lib/android-multinode-test-lib213
-rw-r--r--automated/lib/android_adb_wrapper.py110
-rwxr-xr-xautomated/lib/android_ui_wifi.py112
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()