diff options
Diffstat (limited to 'testing/mozbase/mozrunner/mozrunner/devices')
7 files changed, 1722 insertions, 0 deletions
diff --git a/testing/mozbase/mozrunner/mozrunner/devices/__init__.py b/testing/mozbase/mozrunner/mozrunner/devices/__init__.py new file mode 100644 index 0000000000..7a4ec02e19 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/__init__.py @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from mozrunner.devices import emulator_battery, emulator_geo, emulator_screen + +from .base import Device +from .emulator import BaseEmulator, EmulatorAVD + +__all__ = [ + "BaseEmulator", + "EmulatorAVD", + "Device", + "emulator_battery", + "emulator_geo", + "emulator_screen", +] diff --git a/testing/mozbase/mozrunner/mozrunner/devices/android_device.py b/testing/mozbase/mozrunner/mozrunner/devices/android_device.py new file mode 100644 index 0000000000..45565349f5 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/android_device.py @@ -0,0 +1,1062 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import glob +import os +import platform +import posixpath +import re +import shutil +import signal +import subprocess +import sys +import telnetlib +import time +from distutils.spawn import find_executable +from enum import Enum + +import six +from mozdevice import ADBDeviceFactory, ADBHost +from six.moves import input, urllib + +MOZBUILD_PATH = os.environ.get( + "MOZBUILD_STATE_PATH", os.path.expanduser(os.path.join("~", ".mozbuild")) +) + +EMULATOR_HOME_DIR = os.path.join(MOZBUILD_PATH, "android-device") + +EMULATOR_AUTH_FILE = os.path.join( + os.path.expanduser("~"), ".emulator_console_auth_token" +) + +TOOLTOOL_PATH = "python/mozbuild/mozbuild/action/tooltool.py" + +TRY_URL = "https://hg.mozilla.org/try/raw-file/default" + +MANIFEST_PATH = "testing/config/tooltool-manifests" + +SHORT_TIMEOUT = 10 + +verbose_logging = False + +LLDB_SERVER_INSTALL_COMMANDS_SCRIPT = """ +umask 0002 + +mkdir -p {lldb_bin_dir} + +cp /data/local/tmp/lldb-server {lldb_bin_dir} +chmod +x {lldb_bin_dir}/lldb-server + +chmod 0775 {lldb_dir} +""".lstrip() + +LLDB_SERVER_START_COMMANDS_SCRIPT = """ +umask 0002 + +export LLDB_DEBUGSERVER_LOG_FILE={lldb_log_file} +export LLDB_SERVER_LOG_CHANNELS="{lldb_log_channels}" +export LLDB_DEBUGSERVER_DOMAINSOCKET_DIR={socket_dir} + +rm -rf {lldb_tmp_dir} +mkdir {lldb_tmp_dir} +export TMPDIR={lldb_tmp_dir} + +rm -rf {lldb_log_dir} +mkdir {lldb_log_dir} + +touch {lldb_log_file} +touch {platform_log_file} + +cd {lldb_tmp_dir} +{lldb_bin_dir}/lldb-server platform --server --listen {listener_scheme}://{socket_file} \\ + --log-file "{platform_log_file}" --log-channels "{lldb_log_channels}" \\ + < /dev/null > {platform_stdout_log_file} 2>&1 & +""".lstrip() + + +class InstallIntent(Enum): + YES = 1 + NO = 2 + + +class AvdInfo(object): + """ + Simple class to contain an AVD description. + """ + + def __init__(self, description, name, extra_args, x86): + self.description = description + self.name = name + self.extra_args = extra_args + self.x86 = x86 + + +""" + A dictionary to map an AVD type to a description of that type of AVD. + + There is one entry for each type of AVD used in Mozilla automated tests + and the parameters for each reflect those used in mozharness. +""" +AVD_DICT = { + "arm": AvdInfo( + "Android arm", + "mozemulator-armeabi-v7a", + [ + "-skip-adb-auth", + "-verbose", + "-show-kernel", + "-ranchu", + "-selinux", + "permissive", + "-memory", + "3072", + "-cores", + "4", + "-skin", + "800x1280", + "-gpu", + "on", + "-no-snapstorage", + "-no-snapshot", + "-prop", + "ro.test_harness=true", + ], + False, + ), + "arm64": AvdInfo( + "Android arm64", + "mozemulator-arm64", + [ + "-skip-adb-auth", + "-verbose", + "-show-kernel", + "-ranchu", + "-selinux", + "permissive", + "-memory", + "3072", + "-cores", + "4", + "-skin", + "800x1280", + "-gpu", + "on", + "-no-snapstorage", + "-no-snapshot", + "-prop", + "ro.test_harness=true", + ], + False, + ), + "x86_64": AvdInfo( + "Android x86_64", + "mozemulator-x86_64", + [ + "-skip-adb-auth", + "-verbose", + "-show-kernel", + "-ranchu", + "-selinux", + "permissive", + "-memory", + "3072", + "-cores", + "4", + "-skin", + "800x1280", + "-prop", + "ro.test_harness=true", + "-no-snapstorage", + "-no-snapshot", + ], + True, + ), +} + + +def _get_device(substs, device_serial=None): + adb_path = _find_sdk_exe(substs, "adb", False) + if not adb_path: + adb_path = "adb" + device = ADBDeviceFactory( + adb=adb_path, verbose=verbose_logging, device=device_serial + ) + return device + + +def _install_host_utils(build_obj): + _log_info("Installing host utilities...") + installed = False + host_platform = _get_host_platform() + if host_platform: + path = os.path.join(build_obj.topsrcdir, MANIFEST_PATH) + path = os.path.join(path, host_platform, "hostutils.manifest") + _get_tooltool_manifest( + build_obj.substs, path, EMULATOR_HOME_DIR, "releng.manifest" + ) + _tooltool_fetch(build_obj.substs) + xre_path = glob.glob(os.path.join(EMULATOR_HOME_DIR, "host-utils*")) + for path in xre_path: + if os.path.isdir(path) and os.path.isfile( + os.path.join(path, _get_xpcshell_name()) + ): + os.environ["MOZ_HOST_BIN"] = path + installed = True + elif os.path.isfile(path): + os.remove(path) + if not installed: + _log_warning("Unable to install host utilities.") + else: + _log_warning( + "Unable to install host utilities -- your platform is not supported!" + ) + + +def _get_xpcshell_name(): + """ + Returns the xpcshell binary's name as a string (dependent on operating system). + """ + xpcshell_binary = "xpcshell" + if os.name == "nt": + xpcshell_binary = "xpcshell.exe" + return xpcshell_binary + + +def _maybe_update_host_utils(build_obj): + """ + Compare the installed host-utils to the version name in the manifest; + if the installed version is older, offer to update. + """ + + # Determine existing/installed version + existing_path = None + xre_paths = glob.glob(os.path.join(EMULATOR_HOME_DIR, "host-utils*")) + for path in xre_paths: + if os.path.isdir(path) and os.path.isfile( + os.path.join(path, _get_xpcshell_name()) + ): + existing_path = path + break + if existing_path is None: + # if not installed, no need to upgrade (new version will be installed) + return + existing_version = os.path.basename(existing_path) + + # Determine manifest version + manifest_version = None + host_platform = _get_host_platform() + if host_platform: + # Extract tooltool file name from manifest, something like: + # "filename": "host-utils-58.0a1.en-US-linux-x86_64.tar.gz", + path = os.path.join(build_obj.topsrcdir, MANIFEST_PATH) + manifest_path = os.path.join(path, host_platform, "hostutils.manifest") + with open(manifest_path, "r") as f: + for line in f.readlines(): + m = re.search('.*"(host-utils-.*)"', line) + if m: + manifest_version = m.group(1) + break + + # Compare, prompt, update + if existing_version and manifest_version: + hu_version_regex = "host-utils-([\d\.]*)" + manifest_version = float(re.search(hu_version_regex, manifest_version).group(1)) + existing_version = float(re.search(hu_version_regex, existing_version).group(1)) + if existing_version < manifest_version: + _log_info("Your host utilities are out of date!") + _log_info( + "You have %s installed, but %s is available" + % (existing_version, manifest_version) + ) + response = input("Update host utilities? (Y/n) ").strip() + if response.lower().startswith("y") or response == "": + parts = os.path.split(existing_path) + backup_dir = "_backup-" + parts[1] + backup_path = os.path.join(parts[0], backup_dir) + shutil.move(existing_path, backup_path) + _install_host_utils(build_obj) + + +def verify_android_device( + build_obj, + install=InstallIntent.NO, + xre=False, + debugger=False, + network=False, + verbose=False, + app=None, + device_serial=None, + aab=False, +): + """ + Determine if any Android device is connected via adb. + If no device is found, prompt to start an emulator. + If a device is found or an emulator started and 'install' is + specified, also check whether Firefox is installed on the + device; if not, prompt to install Firefox. + If 'xre' is specified, also check with MOZ_HOST_BIN is set + to a valid xre/host-utils directory; if not, prompt to set + one up. + If 'debugger' is specified, also check that lldb-server is installed; + if it is not found, set it up. + If 'network' is specified, also check that the device has basic + network connectivity. + Returns True if the emulator was started or another device was + already connected. + """ + if "MOZ_DISABLE_ADB_INSTALL" in os.environ: + install = InstallIntent.NO + _log_info( + "Found MOZ_DISABLE_ADB_INSTALL in environment, skipping android app" + "installation" + ) + device_verified = False + emulator = AndroidEmulator("*", substs=build_obj.substs, verbose=verbose) + adb_path = _find_sdk_exe(build_obj.substs, "adb", False) + if not adb_path: + adb_path = "adb" + adbhost = ADBHost(adb=adb_path, verbose=verbose, timeout=SHORT_TIMEOUT) + devices = adbhost.devices(timeout=SHORT_TIMEOUT) + if "device" in [d["state"] for d in devices]: + device_verified = True + elif emulator.is_available(): + response = input( + "No Android devices connected. Start an emulator? (Y/n) " + ).strip() + if response.lower().startswith("y") or response == "": + if not emulator.check_avd(): + _log_info("Android AVD not found, please run |mach bootstrap|") + return + _log_info( + "Starting emulator running %s..." % emulator.get_avd_description() + ) + emulator.start() + emulator.wait_for_start() + device_verified = True + + if device_verified and "DEVICE_SERIAL" not in os.environ: + devices = adbhost.devices(timeout=SHORT_TIMEOUT) + for d in devices: + if d["state"] == "device": + os.environ["DEVICE_SERIAL"] = d["device_serial"] + break + + if device_verified and install != InstallIntent.NO: + # Determine if test app is installed on the device; if not, + # prompt to install. This feature allows a test command to + # launch an emulator, install the test app, and proceed with testing + # in one operation. It is also a basic safeguard against other + # cases where testing is requested but test app installation has + # been forgotten. + # If a test app is installed, there is no way to determine whether + # the current build is installed, and certainly no way to + # determine if the installed build is the desired build. + # Installing every time (without prompting) is problematic because: + # - it prevents testing against other builds (downloaded apk) + # - installation may take a couple of minutes. + if not app: + app = "org.mozilla.geckoview.test_runner" + device = _get_device(build_obj.substs, device_serial) + response = "" + installed = device.is_app_installed(app) + + if not installed: + _log_info("It looks like %s is not installed on this device." % app) + if "fennec" in app or "firefox" in app: + if installed: + device.uninstall_app(app) + _log_info("Installing Firefox...") + build_obj._run_make(directory=".", target="install", ensure_exit_code=False) + elif app == "org.mozilla.geckoview.test": + if installed: + device.uninstall_app(app) + _log_info("Installing geckoview AndroidTest...") + build_obj._mach_context.commands.dispatch( + "android", + build_obj._mach_context, + subcommand="install-geckoview-test", + args=[], + ) + elif app == "org.mozilla.geckoview.test_runner": + if installed: + device.uninstall_app(app) + _log_info("Installing geckoview test_runner...") + sub = ( + "install-geckoview-test_runner-aab" + if aab + else "install-geckoview-test_runner" + ) + build_obj._mach_context.commands.dispatch( + "android", build_obj._mach_context, subcommand=sub, args=[] + ) + elif app == "org.mozilla.geckoview_example": + if installed: + device.uninstall_app(app) + _log_info("Installing geckoview_example...") + sub = ( + "install-geckoview_example-aab" if aab else "install-geckoview_example" + ) + build_obj._mach_context.commands.dispatch( + "android", build_obj._mach_context, subcommand=sub, args=[] + ) + elif not installed: + response = input( + "It looks like %s is not installed on this device,\n" + "but I don't know how to install it.\n" + "Install it now, then hit Enter " % app + ) + + device.run_as_package = app + + if device_verified and xre: + # Check whether MOZ_HOST_BIN has been set to a valid xre; if not, + # prompt to install one. + xre_path = os.environ.get("MOZ_HOST_BIN") + err = None + if not xre_path: + err = ( + "environment variable MOZ_HOST_BIN is not set to a directory " + "containing host xpcshell" + ) + elif not os.path.isdir(xre_path): + err = "$MOZ_HOST_BIN does not specify a directory" + elif not os.path.isfile(os.path.join(xre_path, _get_xpcshell_name())): + err = "$MOZ_HOST_BIN/xpcshell does not exist" + if err: + _maybe_update_host_utils(build_obj) + xre_path = glob.glob(os.path.join(EMULATOR_HOME_DIR, "host-utils*")) + for path in xre_path: + if os.path.isdir(path) and os.path.isfile( + os.path.join(path, _get_xpcshell_name()) + ): + os.environ["MOZ_HOST_BIN"] = path + err = None + break + if err: + _log_info("Host utilities not found: %s" % err) + response = input("Download and setup your host utilities? (Y/n) ").strip() + if response.lower().startswith("y") or response == "": + _install_host_utils(build_obj) + + if device_verified and network: + # Optionally check the network: If on a device that does not look like + # an emulator, verify that the device IP address can be obtained + # and check that this host can ping the device. + serial = device_serial or os.environ.get("DEVICE_SERIAL") + if not serial or ("emulator" not in serial): + device = _get_device(build_obj.substs, serial) + device.run_as_package = app + try: + addr = device.get_ip_address() + if not addr: + _log_warning("unable to get Android device's IP address!") + _log_warning( + "tests may fail without network connectivity to the device!" + ) + else: + _log_info("Android device's IP address: %s" % addr) + response = subprocess.check_output(["ping", "-c", "1", addr]) + _log_debug(response) + except Exception as e: + _log_warning( + "unable to verify network connection to device: %s" % str(e) + ) + _log_warning( + "tests may fail without network connectivity to the device!" + ) + else: + _log_debug("network check skipped on emulator") + + if debugger: + _setup_or_run_lldb_server(app, build_obj.substs, device_serial, setup=True) + + return device_verified + + +def run_lldb_server(app, substs, device_serial): + return _setup_or_run_lldb_server(app, substs, device_serial, setup=False) + + +def _setup_or_run_lldb_server(app, substs, device_serial, setup=True): + device = _get_device(substs, device_serial) + + # Don't use enable_run_as here, as this will not give you what you + # want if we have root access on the device. + pkg_dir = device.shell_output("run-as %s pwd" % app) + if not pkg_dir or pkg_dir == "/": + pkg_dir = "/data/data/%s" % app + _log_warning( + "Unable to resolve data directory for package %s, falling back to hardcoded path" + % app + ) + + pkg_lldb_dir = posixpath.join(pkg_dir, "lldb") + pkg_lldb_bin_dir = posixpath.join(pkg_lldb_dir, "bin") + pkg_lldb_server = posixpath.join(pkg_lldb_bin_dir, "lldb-server") + + if setup: + # Check whether lldb-server is already there + if device.shell_bool("test -x %s" % pkg_lldb_server, enable_run_as=True): + _log_info( + "Found lldb-server binary, terminating any running server processes..." + ) + # lldb-server is already present. Kill any running server. + device.shell("pkill -f lldb-server", enable_run_as=True) + else: + _log_info("lldb-server not found, installing...") + + # We need to do an install + try: + server_path_local = substs["ANDROID_LLDB_SERVER"] + except KeyError: + _log_info( + "ANDROID_LLDB_SERVER is not configured correctly; " + "please re-configure your build." + ) + return + + device.push(server_path_local, "/data/local/tmp") + + install_cmds = LLDB_SERVER_INSTALL_COMMANDS_SCRIPT.format( + lldb_bin_dir=pkg_lldb_bin_dir, lldb_dir=pkg_lldb_dir + ) + + install_cmds = [l for l in install_cmds.splitlines() if l] + + _log_debug( + "Running the following installation commands:\n%r" % (install_cmds,) + ) + + device.batch_execute(install_cmds, enable_run_as=True) + return + + pkg_lldb_sock_file = posixpath.join(pkg_dir, "platform-%d.sock" % int(time.time())) + + pkg_lldb_log_dir = posixpath.join(pkg_lldb_dir, "log") + pkg_lldb_tmp_dir = posixpath.join(pkg_lldb_dir, "tmp") + + pkg_lldb_log_file = posixpath.join(pkg_lldb_log_dir, "lldb-server.log") + pkg_platform_log_file = posixpath.join(pkg_lldb_log_dir, "platform.log") + pkg_platform_stdout_log_file = posixpath.join( + pkg_lldb_log_dir, "platform-stdout.log" + ) + + listener_scheme = "unix-abstract" + log_channels = "lldb process:gdb-remote packets" + + start_cmds = LLDB_SERVER_START_COMMANDS_SCRIPT.format( + lldb_bin_dir=pkg_lldb_bin_dir, + lldb_log_file=pkg_lldb_log_file, + lldb_log_channels=log_channels, + socket_dir=pkg_dir, + lldb_tmp_dir=pkg_lldb_tmp_dir, + lldb_log_dir=pkg_lldb_log_dir, + platform_log_file=pkg_platform_log_file, + listener_scheme=listener_scheme, + platform_stdout_log_file=pkg_platform_stdout_log_file, + socket_file=pkg_lldb_sock_file, + ) + + start_cmds = [l for l in start_cmds.splitlines() if l] + + _log_debug("Running the following start commands:\n%r" % (start_cmds,)) + + device.batch_execute(start_cmds, enable_run_as=True) + + return pkg_lldb_sock_file + + +def get_adb_path(build_obj): + return _find_sdk_exe(build_obj.substs, "adb", False) + + +def grant_runtime_permissions(build_obj, app, device_serial=None): + """ + Grant required runtime permissions to the specified app + (eg. org.mozilla.geckoview.test_runner). + """ + device = _get_device(build_obj.substs, device_serial) + device.run_as_package = app + device.grant_runtime_permissions(app) + + +class AndroidEmulator(object): + """ + Support running the Android emulator with an AVD from Mozilla + test automation. + + Example usage: + emulator = AndroidEmulator() + if not emulator.is_running() and emulator.is_available(): + if not emulator.check_avd(): + print("Android Emulator AVD not found, please run |mach bootstrap|") + emulator.start() + emulator.wait_for_start() + emulator.wait() + """ + + def __init__(self, avd_type=None, verbose=False, substs=None, device_serial=None): + global verbose_logging + self.emulator_log = None + self.emulator_path = "emulator" + verbose_logging = verbose + self.substs = substs + self.avd_type = self._get_avd_type(avd_type) + self.avd_info = AVD_DICT[self.avd_type] + self.gpu = True + self.restarted = False + self.device_serial = device_serial + self.avd_path = os.path.join( + EMULATOR_HOME_DIR, "avd", "%s.avd" % self.avd_info.name + ) + _log_debug("Running on %s" % platform.platform()) + _log_debug("Emulator created with type %s" % self.avd_type) + + def __del__(self): + if self.emulator_log: + self.emulator_log.close() + + def is_running(self): + """ + Returns True if the Android emulator is running. + """ + adb_path = _find_sdk_exe(self.substs, "adb", False) + if not adb_path: + adb_path = "adb" + adbhost = ADBHost(adb=adb_path, verbose=verbose_logging, timeout=SHORT_TIMEOUT) + devs = adbhost.devices(timeout=SHORT_TIMEOUT) + devs = [(d["device_serial"], d["state"]) for d in devs] + _log_debug(devs) + if ("emulator-5554", "device") in devs: + return True + return False + + def is_available(self): + """ + Returns True if an emulator executable is found. + """ + found = False + emulator_path = _find_sdk_exe(self.substs, "emulator", True) + if emulator_path: + self.emulator_path = emulator_path + found = True + return found + + def check_avd(self): + """ + Determine if the AVD is already installed locally. + + Returns True if the AVD is installed. + """ + if os.path.exists(self.avd_path): + _log_debug("AVD found at %s" % self.avd_path) + return True + _log_warning("Could not find AVD at %s" % self.avd_path) + return False + + def start(self, gpu_arg=None): + """ + Launch the emulator. + """ + if self.avd_info.x86 and "linux" in _get_host_platform(): + _verify_kvm(self.substs) + if os.path.exists(EMULATOR_AUTH_FILE): + os.remove(EMULATOR_AUTH_FILE) + _log_debug("deleted %s" % EMULATOR_AUTH_FILE) + self._update_avd_paths() + # create an empty auth file to disable emulator authentication + auth_file = open(EMULATOR_AUTH_FILE, "w") + auth_file.close() + + env = os.environ + env["ANDROID_EMULATOR_HOME"] = EMULATOR_HOME_DIR + env["ANDROID_AVD_HOME"] = os.path.join(EMULATOR_HOME_DIR, "avd") + command = [self.emulator_path, "-avd", self.avd_info.name] + override = os.environ.get("MOZ_EMULATOR_COMMAND_ARGS") + if override: + command += override.split() + _log_debug("Found MOZ_EMULATOR_COMMAND_ARGS in env: %s" % override) + else: + if gpu_arg: + command += ["-gpu", gpu_arg] + # Clear self.gpu to avoid our restart-without-gpu feature: if a specific + # gpu setting is requested, try to use that, and nothing else. + self.gpu = False + elif self.gpu: + command += ["-gpu", "on"] + if self.avd_info.extra_args: + # -enable-kvm option is not valid on OSX and Windows + if ( + _get_host_platform() in ("macosx64", "win32") + and "-enable-kvm" in self.avd_info.extra_args + ): + self.avd_info.extra_args.remove("-enable-kvm") + command += self.avd_info.extra_args + log_path = os.path.join(EMULATOR_HOME_DIR, "emulator.log") + self.emulator_log = open(log_path, "w+") + _log_debug("Starting the emulator with this command: %s" % " ".join(command)) + _log_debug("Emulator output will be written to '%s'" % log_path) + self.proc = subprocess.Popen( + command, + env=env, + stdin=subprocess.PIPE, + stdout=self.emulator_log, + stderr=self.emulator_log, + ) + _log_debug("Emulator started with pid %d" % int(self.proc.pid)) + + def wait_for_start(self): + """ + Verify that the emulator is running, the emulator device is visible + to adb, and Android has booted. + """ + if not self.proc: + _log_warning("Emulator not started!") + return False + if self.check_completed(): + return False + _log_debug("Waiting for device status...") + adb_path = _find_sdk_exe(self.substs, "adb", False) + if not adb_path: + adb_path = "adb" + adbhost = ADBHost(adb=adb_path, verbose=verbose_logging, timeout=SHORT_TIMEOUT) + devs = adbhost.devices(timeout=SHORT_TIMEOUT) + devs = [(d["device_serial"], d["state"]) for d in devs] + while ("emulator-5554", "device") not in devs: + time.sleep(10) + if self.check_completed(): + return False + devs = adbhost.devices(timeout=SHORT_TIMEOUT) + devs = [(d["device_serial"], d["state"]) for d in devs] + _log_debug("Device status verified.") + + _log_debug("Checking that Android has booted...") + device = _get_device(self.substs, self.device_serial) + complete = False + while not complete: + output = "" + try: + output = device.get_prop("sys.boot_completed", timeout=5) + except Exception: + # adb not yet responding...keep trying + pass + if output.strip() == "1": + complete = True + else: + time.sleep(10) + if self.check_completed(): + return False + _log_debug("Android boot status verified.") + + if not self._verify_emulator(): + return False + if self.avd_info.x86: + _log_info( + "Running the x86/x86_64 emulator; be sure to install an x86 or x86_64 APK!" + ) + else: + _log_info("Running the arm emulator; be sure to install an arm APK!") + return True + + def check_completed(self): + if self.proc.poll() is not None: + if self.gpu: + try: + for line in self.emulator_log.readlines(): + if ( + "Invalid value for -gpu" in line + or "Invalid GPU mode" in line + ): + self.gpu = False + break + except Exception as e: + _log_warning(str(e)) + + if not self.gpu and not self.restarted: + _log_warning( + "Emulator failed to start. Your emulator may be out of date." + ) + _log_warning("Trying to restart the emulator without -gpu argument.") + self.restarted = True + self.start() + return False + _log_warning("Emulator has already completed!") + log_path = os.path.join(EMULATOR_HOME_DIR, "emulator.log") + _log_warning( + "See log at %s and/or use --verbose for more information." % log_path + ) + return True + return False + + def wait(self): + """ + Wait for the emulator to close. If interrupted, close the emulator. + """ + try: + self.proc.wait() + except Exception: + if self.proc.poll() is None: + self.cleanup() + return self.proc.poll() + + def cleanup(self): + """ + Close the emulator. + """ + self.proc.kill(signal.SIGTERM) + + def get_avd_description(self): + """ + Return the human-friendly description of this AVD. + """ + return self.avd_info.description + + def _update_avd_paths(self): + ini_path = os.path.join(EMULATOR_HOME_DIR, "avd", "%s.ini" % self.avd_info.name) + with open(ini_path, "r") as f: + lines = f.readlines() + with open(ini_path, "w") as f: + for line in lines: + if line.startswith("path="): + f.write("path=%s\n" % self.avd_path) + elif line.startswith("path.rel="): + f.write("path.rel=avd/%s.avd\n" % self.avd_info.name) + else: + f.write(line) + + def _telnet_read_until(self, telnet, expected, timeout): + if six.PY3 and isinstance(expected, six.text_type): + expected = expected.encode("ascii") + return telnet.read_until(expected, timeout) + + def _telnet_write(self, telnet, command): + if six.PY3 and isinstance(command, six.text_type): + command = command.encode("ascii") + telnet.write(command) + + def _telnet_cmd(self, telnet, command): + _log_debug(">>> %s" % command) + self._telnet_write(telnet, "%s\n" % command) + result = self._telnet_read_until(telnet, "OK", 10) + _log_debug("<<< %s" % result) + return result + + def _verify_emulator(self): + telnet_ok = False + tn = None + while not telnet_ok: + try: + tn = telnetlib.Telnet("localhost", 5554, 10) + if tn is not None: + self._telnet_read_until(tn, "OK", 10) + self._telnet_cmd(tn, "avd status") + self._telnet_cmd(tn, "redir list") + self._telnet_cmd(tn, "network status") + self._telnet_write(tn, "quit\n") + tn.read_all() + telnet_ok = True + else: + _log_warning("Unable to connect to port 5554") + except Exception: + _log_warning("Trying again after unexpected exception") + finally: + if tn is not None: + tn.close() + if not telnet_ok: + time.sleep(10) + if self.proc.poll() is not None: + _log_warning("Emulator has already completed!") + return False + return telnet_ok + + def _get_avd_type(self, requested): + if requested in AVD_DICT.keys(): + return requested + if self.substs: + target_cpu = self.substs["TARGET_CPU"] + if target_cpu == "aarch64": + return "arm64" + elif target_cpu.startswith("arm"): + return "arm" + return "x86_64" + + +def _find_sdk_exe(substs, exe, tools): + if tools: + subdirs = ["emulator", "tools"] + else: + subdirs = ["platform-tools"] + + found = False + if not found and substs: + # It's best to use the tool specified by the build, rather + # than something we find on the PATH or crawl for. + try: + exe_path = substs[exe.upper()] + if os.path.exists(exe_path): + found = True + else: + _log_debug("Unable to find executable at %s" % exe_path) + except KeyError: + _log_debug("%s not set" % exe.upper()) + + # Append '.exe' to the name on Windows if it's not present, + # so that the executable can be found. + if os.name == "nt" and not exe.lower().endswith(".exe"): + exe += ".exe" + + if not found: + # Can exe be found in the Android SDK? + try: + android_sdk_root = os.environ["ANDROID_SDK_ROOT"] + for subdir in subdirs: + exe_path = os.path.join(android_sdk_root, subdir, exe) + if os.path.exists(exe_path): + found = True + break + else: + _log_debug("Unable to find executable at %s" % exe_path) + except KeyError: + _log_debug("ANDROID_SDK_ROOT not set") + + if not found: + # Can exe be found in the default bootstrap location? + for subdir in subdirs: + exe_path = os.path.join(MOZBUILD_PATH, "android-sdk-linux", subdir, exe) + if os.path.exists(exe_path): + found = True + break + else: + _log_debug("Unable to find executable at %s" % exe_path) + + if not found: + # Is exe on PATH? + exe_path = find_executable(exe) + if exe_path: + found = True + else: + _log_debug("Unable to find executable on PATH") + + if found: + _log_debug("%s found at %s" % (exe, exe_path)) + try: + creation_time = os.path.getctime(exe_path) + _log_debug(" ...with creation time %s" % time.ctime(creation_time)) + except Exception: + _log_warning("Could not get creation time for %s" % exe_path) + + prop_path = os.path.join(os.path.dirname(exe_path), "source.properties") + if os.path.exists(prop_path): + with open(prop_path, "r") as f: + for line in f.readlines(): + if line.startswith("Pkg.Revision"): + line = line.strip() + _log_debug( + " ...with SDK version in %s: %s" % (prop_path, line) + ) + break + else: + exe_path = None + return exe_path + + +def _log_debug(text): + if verbose_logging: + print("DEBUG: %s" % text) + + +def _log_warning(text): + print("WARNING: %s" % text) + + +def _log_info(text): + print("%s" % text) + + +def _download_file(url, filename, path): + _log_debug("Download %s to %s/%s..." % (url, path, filename)) + f = urllib.request.urlopen(url) + if not os.path.isdir(path): + try: + os.makedirs(path) + except Exception as e: + _log_warning(str(e)) + return False + local_file = open(os.path.join(path, filename), "wb") + local_file.write(f.read()) + local_file.close() + _log_debug("Downloaded %s to %s/%s" % (url, path, filename)) + return True + + +def _get_tooltool_manifest(substs, src_path, dst_path, filename): + if not os.path.isdir(dst_path): + try: + os.makedirs(dst_path) + except Exception as e: + _log_warning(str(e)) + copied = False + if substs and "top_srcdir" in substs: + src = os.path.join(substs["top_srcdir"], src_path) + if os.path.exists(src): + dst = os.path.join(dst_path, filename) + shutil.copy(src, dst) + copied = True + _log_debug("Copied tooltool manifest %s to %s" % (src, dst)) + if not copied: + url = os.path.join(TRY_URL, src_path) + _download_file(url, filename, dst_path) + + +def _tooltool_fetch(substs): + tooltool_full_path = os.path.join(substs["top_srcdir"], TOOLTOOL_PATH) + command = [ + sys.executable, + tooltool_full_path, + "fetch", + "-o", + "-m", + "releng.manifest", + ] + try: + response = subprocess.check_output(command, cwd=EMULATOR_HOME_DIR) + _log_debug(response) + except Exception as e: + _log_warning(str(e)) + + +def _get_host_platform(): + plat = None + if "darwin" in str(sys.platform).lower(): + plat = "macosx64" + elif "win32" in str(sys.platform).lower(): + plat = "win32" + elif "linux" in str(sys.platform).lower(): + if "64" in platform.architecture()[0]: + plat = "linux64" + else: + plat = "linux32" + return plat + + +def _verify_kvm(substs): + # 'emulator -accel-check' should produce output like: + # accel: + # 0 + # KVM (version 12) is installed and usable + # accel + emulator_path = _find_sdk_exe(substs, "emulator", True) + if not emulator_path: + emulator_path = "emulator" + command = [emulator_path, "-accel-check"] + try: + out = subprocess.check_output(command) + if six.PY3 and not isinstance(out, six.text_type): + out = out.decode("utf-8") + if "is installed and usable" in "".join(out): + return + except Exception as e: + _log_warning(str(e)) + _log_warning("Unable to verify kvm acceleration!") + _log_warning("The x86/x86_64 emulator may fail to start without kvm.") diff --git a/testing/mozbase/mozrunner/mozrunner/devices/base.py b/testing/mozbase/mozrunner/mozrunner/devices/base.py new file mode 100644 index 0000000000..6197b0d6ce --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/base.py @@ -0,0 +1,259 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import datetime +import os +import posixpath +import shutil +import tempfile +import time + +from mozdevice import ADBError, ADBHost +from six.moves.configparser import ConfigParser, RawConfigParser + + +class Device(object): + connected = False + + def __init__(self, app_ctx, logdir=None, serial=None, restore=True): + self.app_ctx = app_ctx + self.device = self.app_ctx.device + self.restore = restore + self.serial = serial + self.logdir = os.path.abspath(os.path.expanduser(logdir)) + self.added_files = set() + self.backup_files = set() + + @property + def remote_profiles(self): + """ + A list of remote profiles on the device. + """ + remote_ini = self.app_ctx.remote_profiles_ini + if not self.device.is_file(remote_ini): + raise IOError("Remote file '%s' not found" % remote_ini) + + local_ini = tempfile.NamedTemporaryFile() + self.device.pull(remote_ini, local_ini.name) + cfg = ConfigParser() + cfg.read(local_ini.name) + + profiles = [] + for section in cfg.sections(): + if cfg.has_option(section, "Path"): + if cfg.has_option(section, "IsRelative") and cfg.getint( + section, "IsRelative" + ): + profiles.append( + posixpath.join( + posixpath.dirname(remote_ini), cfg.get(section, "Path") + ) + ) + else: + profiles.append(cfg.get(section, "Path")) + return profiles + + def pull_minidumps(self): + """ + Saves any minidumps found in the remote profile on the local filesystem. + + :returns: Path to directory containing the dumps. + """ + remote_dump_dir = posixpath.join(self.app_ctx.remote_profile, "minidumps") + local_dump_dir = tempfile.mkdtemp() + try: + self.device.pull(remote_dump_dir, local_dump_dir) + except ADBError as e: + # OK if directory not present -- sometimes called before browser start + if "does not exist" not in str(e): + try: + shutil.rmtree(local_dump_dir) + except Exception: + pass + finally: + raise e + else: + print("WARNING: {}".format(e)) + if os.listdir(local_dump_dir): + self.device.rm(remote_dump_dir, recursive=True) + self.device.mkdir(remote_dump_dir, parents=True) + return local_dump_dir + + def setup_profile(self, profile): + """ + Copy profile to the device and update the remote profiles.ini + to point to the new profile. + + :param profile: mozprofile object to copy over. + """ + if self.device.is_dir(self.app_ctx.remote_profile): + self.device.rm(self.app_ctx.remote_profile, recursive=True) + + self.device.push(profile.profile, self.app_ctx.remote_profile) + + timeout = 5 # seconds + starttime = datetime.datetime.now() + while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout): + if self.device.is_file(self.app_ctx.remote_profiles_ini): + break + time.sleep(1) + local_profiles_ini = tempfile.NamedTemporaryFile() + if not self.device.is_file(self.app_ctx.remote_profiles_ini): + # Unless fennec is already running, and/or remote_profiles_ini is + # not inside the remote_profile (deleted above), this is entirely + # normal. + print("timed out waiting for profiles.ini") + else: + self.device.pull(self.app_ctx.remote_profiles_ini, local_profiles_ini.name) + + config = ProfileConfigParser() + config.read(local_profiles_ini.name) + for section in config.sections(): + if "Profile" in section: + config.set(section, "IsRelative", 0) + config.set(section, "Path", self.app_ctx.remote_profile) + + # delete=False to allow opening the same file from ADB on Windows. + # The file will still be deleted at the end of the `with` block. + # See the "Opening the temporary file again" paragraph in: + # https://docs.python.org/3/library/tempfile.html#tempfile.NamedTemporaryFile + with tempfile.NamedTemporaryFile(delete=False) as new_profiles_ini: + config.write(new_profiles_ini) + self.backup_file(self.app_ctx.remote_profiles_ini) + self.device.push(new_profiles_ini.name, self.app_ctx.remote_profiles_ini) + + # Ideally all applications would read the profile the same way, but in practice + # this isn't true. Perform application specific profile-related setup if necessary. + if hasattr(self.app_ctx, "setup_profile"): + for remote_path in self.app_ctx.remote_backup_files: + self.backup_file(remote_path) + self.app_ctx.setup_profile(profile) + + def _get_online_devices(self): + adbhost = ADBHost(adb=self.app_ctx.adb) + devices = adbhost.devices() + return [ + d["device_serial"] + for d in devices + if d["state"] != "offline" + if not d["device_serial"].startswith("emulator") + ] + + def connect(self): + """ + Connects to a running device. If no serial was specified in the + constructor, defaults to the first entry in `adb devices`. + """ + if self.connected: + return + + online_devices = self._get_online_devices() + if not online_devices: + raise IOError( + "No devices connected. Ensure the device is on and " + "remote debugging via adb is enabled in the settings." + ) + self.serial = online_devices[0] + + self.connected = True + + def reboot(self): + """ + Reboots the device via adb. + """ + self.device.reboot() + + def wait_for_net(self): + active = False + time_out = 0 + while not active and time_out < 40: + if self.device.get_ip_address() is not None: + active = True + time_out += 1 + time.sleep(1) + return active + + def backup_file(self, remote_path): + if not self.restore: + return + + if self.device.exists(remote_path): + self.device.cp(remote_path, "%s.orig" % remote_path, recursive=True) + self.backup_files.add(remote_path) + else: + self.added_files.add(remote_path) + + def cleanup(self): + """ + Cleanup the device. + """ + if not self.restore: + return + + try: + self.device.remount() + # Restore the original profile + for added_file in self.added_files: + self.device.rm(added_file) + + for backup_file in self.backup_files: + if self.device.exists("%s.orig" % backup_file): + self.device.mv("%s.orig" % backup_file, backup_file) + + # Perform application specific profile cleanup if necessary + if hasattr(self.app_ctx, "cleanup_profile"): + self.app_ctx.cleanup_profile() + + # Remove the test profile + self.device.rm(self.app_ctx.remote_profile, force=True, recursive=True) + except Exception as e: + print("cleanup aborted: %s" % str(e)) + + def _rotate_log(self, srclog, index=1): + """ + Rotate a logfile, by recursively rotating logs further in the sequence, + deleting the last file if necessary. + """ + basename = os.path.basename(srclog) + basename = basename[: -len(".log")] + if index > 1: + basename = basename[: -len(".1")] + basename = "%s.%d.log" % (basename, index) + + destlog = os.path.join(self.logdir, basename) + if os.path.isfile(destlog): + if index == 3: + os.remove(destlog) + else: + self._rotate_log(destlog, index + 1) + shutil.move(srclog, destlog) + + +class ProfileConfigParser(RawConfigParser): + """ + Class to create profiles.ini config files + + Subclass of RawConfigParser that outputs .ini files in the exact + format expected for profiles.ini, which is slightly different + than the default format. + """ + + def optionxform(self, optionstr): + return optionstr + + def write(self, fp): + if self._defaults: + fp.write("[%s]\n" % ConfigParser.DEFAULTSECT) + for key, value in self._defaults.items(): + fp.write("%s=%s\n" % (key, str(value).replace("\n", "\n\t"))) + fp.write("\n") + for section in self._sections: + fp.write("[%s]\n" % section) + for key, value in self._sections[section].items(): + if key == "__name__": + continue + if (value is not None) or (self._optcre == self.OPTCRE): + key = "=".join((key, str(value).replace("\n", "\n\t"))) + fp.write("%s\n" % (key)) + fp.write("\n") diff --git a/testing/mozbase/mozrunner/mozrunner/devices/emulator.py b/testing/mozbase/mozrunner/mozrunner/devices/emulator.py new file mode 100644 index 0000000000..4a2aa81733 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator.py @@ -0,0 +1,224 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import datetime +import os +import shutil +import subprocess +import tempfile +import time +from telnetlib import Telnet + +from mozdevice import ADBHost +from mozprocess import ProcessHandler + +from ..errors import TimeoutException +from .base import Device +from .emulator_battery import EmulatorBattery +from .emulator_geo import EmulatorGeo +from .emulator_screen import EmulatorScreen + + +class ArchContext(object): + def __init__(self, arch, context, binary=None, avd=None, extra_args=None): + homedir = getattr(context, "homedir", "") + kernel = os.path.join(homedir, "prebuilts", "qemu-kernel", "%s", "%s") + sysdir = os.path.join(homedir, "out", "target", "product", "%s") + self.extra_args = [] + self.binary = os.path.join(context.bindir or "", "emulator") + if arch == "x86": + self.binary = os.path.join(context.bindir or "", "emulator-x86") + self.kernel = kernel % ("x86", "kernel-qemu") + self.sysdir = sysdir % "generic_x86" + elif avd: + self.avd = avd + self.extra_args = [ + "-show-kernel", + "-debug", + "init,console,gles,memcheck,adbserver,adbclient,adb,avd_config,socket", + ] + else: + self.kernel = kernel % ("arm", "kernel-qemu-armv7") + self.sysdir = sysdir % "generic" + self.extra_args = ["-cpu", "cortex-a8"] + + if binary: + self.binary = binary + + if extra_args: + self.extra_args.extend(extra_args) + + +class SDCard(object): + def __init__(self, emulator, size): + self.emulator = emulator + self.path = self.create_sdcard(size) + + def create_sdcard(self, sdcard_size): + """ + Creates an sdcard partition in the emulator. + + :param sdcard_size: Size of partition to create, e.g '10MB'. + """ + mksdcard = self.emulator.app_ctx.which("mksdcard") + path = tempfile.mktemp(prefix="sdcard", dir=self.emulator.tmpdir) + sdargs = [mksdcard, "-l", "mySdCard", sdcard_size, path] + sd = subprocess.Popen(sdargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + retcode = sd.wait() + if retcode: + raise Exception( + "unable to create sdcard: exit code %d: %s" + % (retcode, sd.stdout.read()) + ) + return path + + +class BaseEmulator(Device): + port = None + proc = None + telnet = None + + def __init__(self, app_ctx, **kwargs): + self.arch = ArchContext( + kwargs.pop("arch", "arm"), + app_ctx, + binary=kwargs.pop("binary", None), + avd=kwargs.pop("avd", None), + ) + super(BaseEmulator, self).__init__(app_ctx, **kwargs) + self.tmpdir = tempfile.mkdtemp() + # These rely on telnet + self.battery = EmulatorBattery(self) + self.geo = EmulatorGeo(self) + self.screen = EmulatorScreen(self) + + @property + def args(self): + """ + Arguments to pass into the emulator binary. + """ + return [self.arch.binary] + + def start(self): + """ + Starts a new emulator. + """ + if self.proc: + return + + original_devices = set(self._get_online_devices()) + + # QEMU relies on atexit() to remove temporary files, which does not + # work since mozprocess uses SIGKILL to kill the emulator process. + # Use a customized temporary directory so we can clean it up. + os.environ["ANDROID_TMP"] = self.tmpdir + + qemu_log = None + qemu_proc_args = {} + if self.logdir: + # save output from qemu to logfile + qemu_log = os.path.join(self.logdir, "qemu.log") + if os.path.isfile(qemu_log): + self._rotate_log(qemu_log) + qemu_proc_args["logfile"] = qemu_log + else: + qemu_proc_args["processOutputLine"] = lambda line: None + self.proc = ProcessHandler(self.args, **qemu_proc_args) + self.proc.run() + + devices = set(self._get_online_devices()) + now = datetime.datetime.now() + while (devices - original_devices) == set([]): + time.sleep(1) + # Sometimes it takes more than 60s to launch emulator, so we + # increase timeout value to 180s. Please see bug 1143380. + if datetime.datetime.now() - now > datetime.timedelta(seconds=180): + raise TimeoutException("timed out waiting for emulator to start") + devices = set(self._get_online_devices()) + devices = devices - original_devices + self.serial = devices.pop() + self.connect() + + def _get_online_devices(self): + adbhost = ADBHost(adb=self.app_ctx.adb) + return [ + d["device_serial"] + for d in adbhost.devices() + if d["state"] != "offline" + if d["device_serial"].startswith("emulator") + ] + + def connect(self): + """ + Connects to a running device. If no serial was specified in the + constructor, defaults to the first entry in `adb devices`. + """ + if self.connected: + return + + super(BaseEmulator, self).connect() + self.port = int(self.serial[self.serial.rindex("-") + 1 :]) + + def cleanup(self): + """ + Cleans up and kills the emulator, if it was started by mozrunner. + """ + super(BaseEmulator, self).cleanup() + if self.proc: + self.proc.kill() + self.proc = None + self.connected = False + + # Remove temporary files + if os.path.isdir(self.tmpdir): + shutil.rmtree(self.tmpdir) + + def _get_telnet_response(self, command=None): + output = [] + assert self.telnet + if command is not None: + self.telnet.write("%s\n" % command) + while True: + line = self.telnet.read_until("\n") + output.append(line.rstrip()) + if line.startswith("OK"): + return output + elif line.startswith("KO:"): + raise Exception("bad telnet response: %s" % line) + + def _run_telnet(self, command): + if not self.telnet: + self.telnet = Telnet("localhost", self.port) + self._get_telnet_response() + return self._get_telnet_response(command) + + def __del__(self): + if self.telnet: + self.telnet.write("exit\n") + self.telnet.read_all() + + +class EmulatorAVD(BaseEmulator): + def __init__(self, app_ctx, binary, avd, port=5554, **kwargs): + super(EmulatorAVD, self).__init__(app_ctx, binary=binary, avd=avd, **kwargs) + self.port = port + + @property + def args(self): + """ + Arguments to pass into the emulator binary. + """ + qemu_args = super(EmulatorAVD, self).args + qemu_args.extend(["-avd", self.arch.avd, "-port", str(self.port)]) + qemu_args.extend(self.arch.extra_args) + return qemu_args + + def start(self): + if self.proc: + return + + env = os.environ + env["ANDROID_AVD_HOME"] = self.app_ctx.avd_home + + super(EmulatorAVD, self).start() diff --git a/testing/mozbase/mozrunner/mozrunner/devices/emulator_battery.py b/testing/mozbase/mozrunner/mozrunner/devices/emulator_battery.py new file mode 100644 index 0000000000..58d42b0a0e --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator_battery.py @@ -0,0 +1,53 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +class EmulatorBattery(object): + def __init__(self, emulator): + self.emulator = emulator + + def get_state(self): + status = {} + state = {} + + response = self.emulator._run_telnet("power display") + for line in response: + if ":" in line: + field, value = line.split(":") + value = value.strip() + if value == "true": + value = True + elif value == "false": + value = False + elif field == "capacity": + value = float(value) + status[field] = value + + # pylint --py3k W1619 + state["level"] = status.get("capacity", 0.0) / 100 + if status.get("AC") == "online": + state["charging"] = True + else: + state["charging"] = False + + return state + + def get_charging(self): + return self.get_state()["charging"] + + def get_level(self): + return self.get_state()["level"] + + def set_level(self, level): + self.emulator._run_telnet("power capacity %d" % (level * 100)) + + def set_charging(self, charging): + if charging: + cmd = "power ac on" + else: + cmd = "power ac off" + self.emulator._run_telnet(cmd) + + charging = property(get_charging, set_charging) + level = property(get_level, set_level) diff --git a/testing/mozbase/mozrunner/mozrunner/devices/emulator_geo.py b/testing/mozbase/mozrunner/mozrunner/devices/emulator_geo.py new file mode 100644 index 0000000000..a1fd6fc8b2 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator_geo.py @@ -0,0 +1,16 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +class EmulatorGeo(object): + def __init__(self, emulator): + self.emulator = emulator + + def set_default_location(self): + self.lon = -122.08769 + self.lat = 37.41857 + self.set_location(self.lon, self.lat) + + def set_location(self, lon, lat): + self.emulator._run_telnet("geo fix %0.5f %0.5f" % (self.lon, self.lat)) diff --git a/testing/mozbase/mozrunner/mozrunner/devices/emulator_screen.py b/testing/mozbase/mozrunner/mozrunner/devices/emulator_screen.py new file mode 100644 index 0000000000..8f261c3610 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator_screen.py @@ -0,0 +1,91 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +class EmulatorScreen(object): + """Class for screen related emulator commands.""" + + SO_PORTRAIT_PRIMARY = "portrait-primary" + SO_PORTRAIT_SECONDARY = "portrait-secondary" + SO_LANDSCAPE_PRIMARY = "landscape-primary" + SO_LANDSCAPE_SECONDARY = "landscape-secondary" + + def __init__(self, emulator): + self.emulator = emulator + + def initialize(self): + self.orientation = self.SO_PORTRAIT_PRIMARY + + def _get_raw_orientation(self): + """Get the raw value of the current device orientation.""" + response = self.emulator._run_telnet("sensor get orientation") + + return response[0].split("=")[1].strip() + + def _set_raw_orientation(self, data): + """Set the raw value of the specified device orientation.""" + self.emulator._run_telnet("sensor set orientation %s" % data) + + def get_orientation(self): + """Get the current device orientation. + + Returns; + orientation -- Orientation of the device. One of: + SO_PORTRAIT_PRIMARY - system buttons at the bottom + SO_PORTRIAT_SECONDARY - system buttons at the top + SO_LANDSCAPE_PRIMARY - system buttons at the right + SO_LANDSCAPE_SECONDARY - system buttons at the left + + """ + data = self._get_raw_orientation() + + if data == "0:-90:0": + orientation = self.SO_PORTRAIT_PRIMARY + elif data == "0:90:0": + orientation = self.SO_PORTRAIT_SECONDARY + elif data == "0:0:90": + orientation = self.SO_LANDSCAPE_PRIMARY + elif data == "0:0:-90": + orientation = self.SO_LANDSCAPE_SECONDARY + else: + raise ValueError("Unknown orientation sensor value: %s." % data) + + return orientation + + def set_orientation(self, orientation): + """Set the specified device orientation. + + Args + orientation -- Orientation of the device. One of: + SO_PORTRAIT_PRIMARY - system buttons at the bottom + SO_PORTRIAT_SECONDARY - system buttons at the top + SO_LANDSCAPE_PRIMARY - system buttons at the right + SO_LANDSCAPE_SECONDARY - system buttons at the left + """ + orientation = SCREEN_ORIENTATIONS[orientation] + + if orientation == self.SO_PORTRAIT_PRIMARY: + data = "0:-90:0" + elif orientation == self.SO_PORTRAIT_SECONDARY: + data = "0:90:0" + elif orientation == self.SO_LANDSCAPE_PRIMARY: + data = "0:0:90" + elif orientation == self.SO_LANDSCAPE_SECONDARY: + data = "0:0:-90" + else: + raise ValueError("Invalid orientation: %s" % orientation) + + self._set_raw_orientation(data) + + orientation = property(get_orientation, set_orientation) + + +SCREEN_ORIENTATIONS = { + "portrait": EmulatorScreen.SO_PORTRAIT_PRIMARY, + "landscape": EmulatorScreen.SO_LANDSCAPE_PRIMARY, + "portrait-primary": EmulatorScreen.SO_PORTRAIT_PRIMARY, + "landscape-primary": EmulatorScreen.SO_LANDSCAPE_PRIMARY, + "portrait-secondary": EmulatorScreen.SO_PORTRAIT_SECONDARY, + "landscape-secondary": EmulatorScreen.SO_LANDSCAPE_SECONDARY, +} |