diff options
Diffstat (limited to 'testing/condprofile/condprof/android.py')
-rw-r--r-- | testing/condprofile/condprof/android.py | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/testing/condprofile/condprof/android.py b/testing/condprofile/condprof/android.py new file mode 100644 index 0000000000..b56d0b906c --- /dev/null +++ b/testing/condprofile/condprof/android.py @@ -0,0 +1,294 @@ +# 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/. +""" Drives an android device. +""" +import os +import posixpath +import tempfile +import contextlib +import time +import logging + +import attr +from arsenic.services import Geckodriver, free_port, subprocess_based_service +from mozdevice import ADBDeviceFactory, ADBError + +from condprof.util import write_yml_file, logger, DEFAULT_PREFS, BaseEnv + + +# XXX most of this code should migrate into mozdevice - see Bug 1574849 +class AndroidDevice: + def __init__( + self, + app_name, + marionette_port=2828, + verbose=False, + remote_test_root="/sdcard/test_root/", + ): + self.app_name = app_name + + # XXX make that an option + if "fenix" in app_name: + self.activity = "org.mozilla.fenix.IntentReceiverActivity" + else: + self.activity = "org.mozilla.geckoview_example.GeckoViewActivity" + self.verbose = verbose + self.device = None + self.marionette_port = marionette_port + self.profile = None + self.remote_profile = None + self.log_file = None + self.remote_test_root = remote_test_root + self._adb_fh = None + + def _set_adb_logger(self, log_file): + self.log_file = log_file + if self.log_file is None: + return + logger.info("Setting ADB log file to %s" % self.log_file) + adb_logger = logging.getLogger("adb") + adb_logger.setLevel(logging.DEBUG) + self._adb_fh = logging.FileHandler(self.log_file) + self._adb_fh.setLevel(logging.DEBUG) + adb_logger.addHandler(self._adb_fh) + + def _unset_adb_logger(self): + if self._adb_fh is None: + return + logging.getLogger("adb").removeHandler(self._adb_fh) + self._adb_fh = None + + def clear_logcat(self, timeout=None, buffers=[]): + if not self.device: + return + self.device.clear_logcat(timeout, buffers) + + def get_logcat(self): + if not self.device: + return None + # we don't want to have ADBCommand dump the command + # in the debug stream so we reduce its verbosity here + # temporarely + old_verbose = self.device._verbose + self.device._verbose = False + try: + return self.device.get_logcat() + finally: + self.device._verbose = old_verbose + + def prepare(self, profile, logfile): + self._set_adb_logger(logfile) + try: + # See android_emulator_pgo.py run_tests for more + # details on why test_root must be /sdcard/test_root + # for android pgo due to Android 4.3. + self.device = ADBDeviceFactory( + verbose=self.verbose, logger_name="adb", test_root=self.remote_test_root + ) + except Exception: + logger.error("Cannot initialize device") + raise + device = self.device + self.profile = profile + + # checking that the app is installed + if not device.is_app_installed(self.app_name): + raise Exception("%s is not installed" % self.app_name) + + # debug flag + logger.info("Setting %s as the debug app on the phone" % self.app_name) + device.shell( + "am set-debug-app --persistent %s" % self.app_name, + stdout_callback=logger.info, + ) + + # creating the profile on the device + logger.info("Creating the profile on the device") + + remote_profile = posixpath.join(self.remote_test_root, "profile") + logger.info("The profile on the phone will be at %s" % remote_profile) + + device.rm(remote_profile, force=True, recursive=True) + logger.info("Pushing %s on the phone" % self.profile) + device.push(profile, remote_profile) + device.chmod(remote_profile, recursive=True) + self.profile = profile + self.remote_profile = remote_profile + + # creating the yml file + yml_data = { + "args": ["-marionette", "-profile", self.remote_profile], + "prefs": DEFAULT_PREFS, + "env": {"LOG_VERBOSE": 1, "R_LOG_LEVEL": 6, "MOZ_LOG": ""}, + } + + yml_name = "%s-geckoview-config.yaml" % self.app_name + yml_on_host = posixpath.join(tempfile.mkdtemp(), yml_name) + write_yml_file(yml_on_host, yml_data) + tmp_on_device = posixpath.join("/data", "local", "tmp") + if not device.exists(tmp_on_device): + raise IOError("%s does not exists on the device" % tmp_on_device) + yml_on_device = posixpath.join(tmp_on_device, yml_name) + try: + device.rm(yml_on_device, force=True, recursive=True) + device.push(yml_on_host, yml_on_device) + device.chmod(yml_on_device, recursive=True) + except Exception: + logger.info("could not create the yaml file on device. Permission issue?") + raise + + # command line 'extra' args not used with geckoview apps; instead we use + # an on-device config.yml file + intent = "android.intent.action.VIEW" + device.stop_application(self.app_name) + device.launch_application( + self.app_name, self.activity, intent, extras=None, url="about:blank" + ) + if not device.process_exist(self.app_name): + raise Exception("Could not start %s" % self.app_name) + + logger.info("Creating socket forwarding on port %d" % self.marionette_port) + device.forward( + local="tcp:%d" % self.marionette_port, + remote="tcp:%d" % self.marionette_port, + ) + + # we don't have a clean way for now to check that GV or Fenix + # is ready to handle our tests. So here we just wait 30s + logger.info("Sleeping for 30s") + time.sleep(30) + + def stop_browser(self): + logger.info("Stopping %s" % self.app_name) + try: + self.device.stop_application(self.app_name) + except ADBError: + logger.info("Could not stop the application using force-stop") + + time.sleep(5) + if self.device.process_exist(self.app_name): + logger.info("%s still running, trying SIGKILL" % self.app_name) + num_tries = 0 + while self.device.process_exist(self.app_name) and num_tries < 5: + try: + self.device.pkill(self.app_name) + except ADBError: + pass + num_tries += 1 + time.sleep(1) + logger.info("%s stopped" % self.app_name) + + def collect_profile(self): + logger.info("Collecting profile from %s" % self.remote_profile) + self.device.pull(self.remote_profile, self.profile) + + def close(self): + self._unset_adb_logger() + if self.device is None: + return + try: + self.device.remove_forwards("tcp:%d" % self.marionette_port) + except ADBError: + logger.warning("Could not remove forward port") + + +# XXX redundant, remove +@contextlib.contextmanager +def device( + app_name, marionette_port=2828, verbose=True, remote_test_root="/sdcard/test_root/" +): + device_ = AndroidDevice( + app_name, marionette_port, verbose, remote_test_root=remote_test_root + ) + try: + yield device_ + finally: + device_.close() + + +@attr.s +class AndroidGeckodriver(Geckodriver): + async def start(self): + port = free_port() + await self._check_version() + logger.info("Running Webdriver on port %d" % port) + logger.info("Running Marionette on port 2828") + pargs = [ + self.binary, + "--log", + "trace", + "--port", + str(port), + "--marionette-port", + "2828", + ] + logger.info("Connecting on Android device") + pargs.append("--connect-existing") + return await subprocess_based_service( + pargs, f"http://localhost:{port}", self.log_file + ) + + +class AndroidEnv(BaseEnv): + @contextlib.contextmanager + def get_device(self, *args, **kw): + with device(self.firefox, *args, **kw) as d: + self.device = d + yield self.device + + def get_target_platform(self): + app = self.firefox.split("org.mozilla.")[-1] + if self.device_name is None: + return app + return "%s-%s" % (self.device_name, app) + + def dump_logs(self): + logger.info("Dumping Android logs") + try: + logcat = self.device.get_logcat() + if logcat: + # local path, not using posixpath + logfile = os.path.join(self.archive, "logcat.log") + logger.info("Writing logcat at %s" % logfile) + with open(logfile, "wb") as f: + for line in logcat: + f.write(line.encode("utf8", errors="replace") + b"\n") + else: + logger.info("logcat came back empty") + except Exception: + logger.error("Could not extract the logcat", exc_info=True) + + @contextlib.contextmanager + def get_browser(self): + yield + + def get_browser_args(self, headless, prefs=None): + # XXX merge with DesktopEnv.get_browser_args + options = ["--allow-downgrade"] + if headless: + options.append("-headless") + if prefs is None: + prefs = {} + return {"moz:firefoxOptions": {"args": options, "prefs": prefs}} + + def prepare(self, logfile): + self.device.prepare(self.profile, logfile) + + def get_browser_version(self): + return self.target_platform + "-XXXneedtograbversion" + + def get_geckodriver(self, log_file): + return AndroidGeckodriver(binary=self.geckodriver, log_file=log_file) + + def check_session(self, session): + async def fake_close(*args): + pass + + session.close = fake_close + + def collect_profile(self): + self.device.collect_profile() + + def stop_browser(self): + self.device.stop_browser() |