| # mypy: allow-untyped-defs |
| |
| import os |
| import subprocess |
| import re |
| |
| from mozrunner import FennecEmulatorRunner, get_app_context |
| |
| from .base import (get_free_port, |
| cmd_arg, |
| browser_command) |
| from ..executors.executormarionette import (MarionetteTestharnessExecutor, # noqa: F401 |
| MarionetteRefTestExecutor, # noqa: F401 |
| MarionetteCrashtestExecutor, # noqa: F401 |
| MarionetteWdspecExecutor) # noqa: F401 |
| from .base import (Browser, |
| ExecutorBrowser) |
| from .firefox import (get_timeout_multiplier, # noqa: F401 |
| run_info_browser_version, |
| run_info_extras as fx_run_info_extras, |
| update_properties, # noqa: F401 |
| executor_kwargs as fx_executor_kwargs, # noqa: F401 |
| FirefoxWdSpecBrowser, |
| ProfileCreator as FirefoxProfileCreator) |
| |
| |
| __wptrunner__ = {"product": "firefox_android", |
| "check_args": "check_args", |
| "browser": {None: "FirefoxAndroidBrowser", |
| "wdspec": "FirefoxAndroidWdSpecBrowser"}, |
| "executor": {"testharness": "MarionetteTestharnessExecutor", |
| "reftest": "MarionetteRefTestExecutor", |
| "crashtest": "MarionetteCrashtestExecutor", |
| "wdspec": "MarionetteWdspecExecutor"}, |
| "browser_kwargs": "browser_kwargs", |
| "executor_kwargs": "executor_kwargs", |
| "env_extras": "env_extras", |
| "env_options": "env_options", |
| "run_info_extras": "run_info_extras", |
| "update_properties": "update_properties", |
| "timeout_multiplier": "get_timeout_multiplier"} |
| |
| |
| def check_args(**kwargs): |
| pass |
| |
| |
| def browser_kwargs(logger, test_type, run_info_data, config, **kwargs): |
| return {"adb_binary": kwargs["adb_binary"], |
| "webdriver_binary": kwargs["webdriver_binary"], |
| "webdriver_args": kwargs["webdriver_args"].copy(), |
| "binary": None, |
| "package_name": kwargs["package_name"], |
| "device_serial": kwargs["device_serial"], |
| "prefs_root": kwargs["prefs_root"], |
| "extra_prefs": kwargs["extra_prefs"].copy(), |
| "test_type": test_type, |
| "debug_info": kwargs["debug_info"], |
| "symbols_path": kwargs["symbols_path"], |
| "stackwalk_binary": kwargs["stackwalk_binary"], |
| "certutil_binary": kwargs["certutil_binary"], |
| "ca_certificate_path": config.ssl_config["ca_cert_path"], |
| "stackfix_dir": kwargs["stackfix_dir"], |
| "binary_args": kwargs["binary_args"].copy(), |
| "timeout_multiplier": get_timeout_multiplier(test_type, |
| run_info_data, |
| **kwargs), |
| "disable_fission": kwargs["disable_fission"], |
| # desktop only |
| "leak_check": False, |
| "chaos_mode_flags": kwargs["chaos_mode_flags"], |
| "config": config, |
| "install_fonts": kwargs["install_fonts"], |
| "tests_root": config.doc_root, |
| "specialpowers_path": kwargs["specialpowers_path"], |
| "debug_test": kwargs["debug_test"], |
| "env_extras": dict([x.split('=') for x in kwargs.get("env", [])])} |
| |
| |
| def executor_kwargs(logger, test_type, test_environment, run_info_data, |
| **kwargs): |
| rv = fx_executor_kwargs(logger, test_type, test_environment, run_info_data, |
| **kwargs) |
| if test_type == "wdspec": |
| rv["capabilities"]["moz:firefoxOptions"]["androidPackage"] = kwargs["package_name"] |
| return rv |
| |
| |
| def env_extras(**kwargs): |
| return [] |
| |
| |
| def run_info_extras(logger, **kwargs): |
| rv = fx_run_info_extras(logger, **kwargs) |
| rv.update({"headless": False}) |
| |
| if kwargs["browser_version"] is None: |
| rv.update(run_info_browser_version(**kwargs)) |
| |
| if rv.get("browser_version") is None: |
| # If we didn't get the browser version from the apk, try to get it from adb dumpsys |
| rv["browser_version"] = get_package_browser_version(logger, |
| kwargs["adb_binary"], |
| kwargs["package_name"]) |
| |
| return rv |
| |
| |
| def get_package_browser_version(logger, adb_binary, package_name): |
| if adb_binary is None: |
| logger.warning("Couldn't run adb to get Firefox Android version number") |
| return None |
| try: |
| completed = subprocess.run([adb_binary, "shell", "dumpsys", "package", package_name], |
| check=True, |
| capture_output=True, |
| encoding="utf8") |
| except subprocess.CalledProcessError as e: |
| logger.warning(f"adb failed with return code {e.returncode}\nCaptured stderr:\n{e.stderr}") |
| return None |
| |
| version_name_re = re.compile(r"^\s+versionName=(.*)") |
| for line in completed.stdout.splitlines(): |
| m = version_name_re.match(line) |
| if m is not None: |
| return m.group(1) |
| logger.warning("Failed to find versionName property in dumpsys output") |
| |
| |
| def env_options(): |
| return {"server_host": "127.0.0.1", |
| "supports_debugger": True} |
| |
| |
| def get_environ(chaos_mode_flags, env_extras=None): |
| env = {} |
| if env_extras is not None: |
| env.update(env_extras) |
| env["MOZ_CRASHREPORTER"] = "1" |
| env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1" |
| env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1" |
| if chaos_mode_flags is not None: |
| env["MOZ_CHAOSMODE"] = hex(chaos_mode_flags) |
| return env |
| |
| |
| class ProfileCreator(FirefoxProfileCreator): |
| def __init__(self, logger, prefs_root, config, test_type, extra_prefs, |
| disable_fission, debug_test, browser_channel, binary, |
| package_name, certutil_binary, ca_certificate_path): |
| |
| super().__init__(logger, prefs_root, config, test_type, extra_prefs, |
| disable_fission, debug_test, browser_channel, None, |
| package_name, certutil_binary, ca_certificate_path) |
| |
| def _set_required_prefs(self, profile): |
| profile.set_preferences({ |
| "network.dns.localDomains": ",".join(self.config.domains_set), |
| "dom.disable_open_during_load": False, |
| "places.history.enabled": False, |
| "dom.send_after_paint_to_content": True, |
| }) |
| |
| if self.package_name == "org.mozilla.geckoview.test_runner": |
| # Bug 1879324: The TestRunner doesn't support "beforeunload" prompts yet |
| profile.set_preferences({"dom.disable_beforeunload": True}) |
| |
| if self.test_type == "reftest": |
| self.logger.info("Setting android reftest preferences") |
| profile.set_preferences({ |
| "browser.viewport.desktopWidth": 800, |
| # Disable high DPI |
| "layout.css.devPixelsPerPx": "1.0", |
| # Ensure that the full browser element |
| # appears in the screenshot |
| "apz.allow_zooming": False, |
| "android.widget_paints_background": False, |
| # Ensure that scrollbars are always painted |
| "layout.testing.overlay-scrollbars.always-visible": True, |
| }) |
| |
| if self.test_type == "wdspec": |
| profile.set_preferences({"remote.prefs.recommended": True}) |
| |
| profile.set_preferences({"fission.autostart": True}) |
| if self.disable_fission: |
| profile.set_preferences({"fission.autostart": False}) |
| |
| |
| class FirefoxAndroidBrowser(Browser): |
| init_timeout = 300 |
| shutdown_timeout = 60 |
| |
| def __init__(self, logger, prefs_root, test_type, package_name="org.mozilla.geckoview.test_runner", |
| device_serial=None, extra_prefs=None, debug_info=None, |
| symbols_path=None, stackwalk_binary=None, certutil_binary=None, |
| ca_certificate_path=None, stackfix_dir=None, |
| binary_args=None, timeout_multiplier=None, leak_check=False, asan=False, |
| chaos_mode_flags=None, config=None, browser_channel="nightly", |
| install_fonts=False, tests_root=None, specialpowers_path=None, adb_binary=None, |
| debug_test=False, disable_fission=False, **kwargs): |
| |
| super().__init__(logger) |
| self.prefs_root = prefs_root |
| self.test_type = test_type |
| self.package_name = package_name |
| self.device_serial = device_serial |
| self.debug_info = debug_info |
| self.symbols_path = symbols_path |
| self.stackwalk_binary = stackwalk_binary |
| self.certutil_binary = certutil_binary |
| self.ca_certificate_path = ca_certificate_path |
| self.stackfix_dir = stackfix_dir |
| self.binary_args = binary_args |
| self.timeout_multiplier = timeout_multiplier |
| self.leak_check = leak_check |
| self.asan = asan |
| self.chaos_mode_flags = chaos_mode_flags |
| self.config = config |
| self.browser_channel = browser_channel |
| self.install_fonts = install_fonts |
| self.tests_root = tests_root |
| self.specialpowers_path = specialpowers_path |
| self.adb_binary = adb_binary |
| self.disable_fission = disable_fission |
| |
| self.profile_creator = ProfileCreator(logger, |
| prefs_root, |
| config, |
| test_type, |
| extra_prefs, |
| disable_fission, |
| debug_test, |
| browser_channel, |
| None, |
| package_name, |
| certutil_binary, |
| ca_certificate_path) |
| |
| self.marionette_port = None |
| self.profile = None |
| self.runner = None |
| self.env_extras = kwargs["env_extras"] |
| self._settings = {} |
| |
| def settings(self, test): |
| self._settings = {"check_leaks": self.leak_check and not test.leaks, |
| "lsan_allowed": test.lsan_allowed, |
| "lsan_max_stack_depth": test.lsan_max_stack_depth, |
| "mozleak_allowed": self.leak_check and test.mozleak_allowed, |
| "mozleak_thresholds": self.leak_check and test.mozleak_threshold, |
| "special_powers": self.specialpowers_path and test.url_base == "/_mozilla/"} |
| return self._settings |
| |
| def start(self, **kwargs): |
| if self.marionette_port is None: |
| self.marionette_port = get_free_port() |
| |
| addons = [self.specialpowers_path] if self._settings.get("special_powers") else None |
| self.profile = self.profile_creator.create(addons=addons) |
| self.profile.set_preferences({"marionette.port": self.marionette_port}) |
| |
| if self.install_fonts: |
| self.logger.debug("Copying Ahem font to profile") |
| font_dir = os.path.join(self.profile.profile, "fonts") |
| if not os.path.exists(font_dir): |
| os.makedirs(font_dir) |
| with open(os.path.join(self.tests_root, "fonts", "Ahem.ttf"), "rb") as src: |
| with open(os.path.join(font_dir, "Ahem.ttf"), "wb") as dest: |
| dest.write(src.read()) |
| |
| self.leak_report_file = None |
| |
| debug_args, cmd = browser_command(self.package_name, |
| self.binary_args if self.binary_args else [] + |
| [cmd_arg("marionette"), "about:blank"], |
| self.debug_info) |
| |
| env = get_environ(self.chaos_mode_flags, self.env_extras) |
| |
| self.runner = FennecEmulatorRunner(app=self.package_name, |
| profile=self.profile, |
| cmdargs=cmd[1:], |
| env=env, |
| symbols_path=self.symbols_path, |
| serial=self.device_serial, |
| # TODO - choose appropriate log dir |
| logdir=os.getcwd(), |
| adb_path=self.adb_binary, |
| explicit_cleanup=True) |
| |
| self.logger.debug("Starting %s" % self.package_name) |
| # connect to a running emulator |
| self.runner.device.connect() |
| |
| self.runner.stop() |
| self.runner.start(debug_args=debug_args, |
| interactive=self.debug_info and self.debug_info.interactive) |
| |
| self.runner.device.device.forward( |
| local=f"tcp:{self.marionette_port}", |
| remote=f"tcp:{self.marionette_port}") |
| |
| for ports in self.config.ports.values(): |
| for port in ports: |
| self.runner.device.device.reverse( |
| local=f"tcp:{port}", |
| remote=f"tcp:{port}") |
| |
| self.logger.debug("%s Started" % self.package_name) |
| |
| def stop(self, force=False): |
| if self.runner is not None: |
| if self.runner.device.connected: |
| try: |
| self.runner.device.device.remove_forwards() |
| self.runner.device.device.remove_reverses() |
| except Exception as e: |
| self.logger.warning("Failed to remove forwarded or reversed ports: %s" % e) |
| # We assume that stopping the runner prompts the |
| # browser to shut down. |
| self.runner.cleanup() |
| self.logger.debug("stopped") |
| |
| def pid(self): |
| if self.runner.process_handler is None: |
| return None |
| |
| try: |
| return self.runner.process_handler.pid |
| except AttributeError: |
| return None |
| |
| def is_alive(self): |
| if self.runner: |
| return self.runner.is_running() |
| return False |
| |
| def cleanup(self, force=False): |
| self.stop(force) |
| |
| def executor_browser(self): |
| return ExecutorBrowser, {"marionette_port": self.marionette_port, |
| # We never want marionette to install extensions because |
| # that doesn't work on Android; instead they are in the profile |
| "extensions": [], |
| "supports_devtools": False} |
| |
| def check_crash(self, process, test): |
| if not os.environ.get("MINIDUMP_STACKWALK", "") and self.stackwalk_binary: |
| os.environ["MINIDUMP_STACKWALK"] = self.stackwalk_binary |
| return bool(self.runner.check_for_crashes(test_name=test)) |
| |
| |
| class FirefoxAndroidWdSpecBrowser(FirefoxWdSpecBrowser): |
| def __init__(self, logger, prefs_root, webdriver_binary, webdriver_args, |
| extra_prefs=None, debug_info=None, symbols_path=None, stackwalk_binary=None, |
| certutil_binary=None, ca_certificate_path=None, |
| disable_fission=False, stackfix_dir=None, leak_check=False, |
| asan=False, chaos_mode_flags=None, config=None, |
| browser_channel="nightly", headless=None, debug_test=None, |
| binary=None, package_name="org.mozilla.geckoview.test_runner", device_serial=None, |
| adb_binary=None, profile_creator_cls=ProfileCreator, **kwargs): |
| |
| super().__init__(logger, None, package_name, prefs_root, webdriver_binary, webdriver_args, |
| extra_prefs=extra_prefs, debug_info=debug_info, symbols_path=symbols_path, |
| stackwalk_binary=stackwalk_binary, certutil_binary=certutil_binary, |
| ca_certificate_path=ca_certificate_path, |
| disable_fission=disable_fission, stackfix_dir=stackfix_dir, |
| leak_check=leak_check, asan=asan, |
| chaos_mode_flags=chaos_mode_flags, config=config, |
| browser_channel=browser_channel, headless=headless, |
| debug_test=debug_test, profile_creator_cls=profile_creator_cls, **kwargs) |
| |
| self.config = config |
| self.device_serial = device_serial |
| # This is just to support the same adb lookup as for other test types |
| context = get_app_context("fennec")(adb_path=adb_binary, device_serial=device_serial) |
| self.device = context.get_device(context.adb, self.device_serial) |
| |
| def start(self, group_metadata, **kwargs): |
| for ports in self.config.ports.values(): |
| for port in ports: |
| self.device.reverse( |
| local=f"tcp:{port}", |
| remote=f"tcp:{port}") |
| super().start(group_metadata, **kwargs) |
| |
| def stop(self, force=False): |
| try: |
| self.device.remove_reverses() |
| except Exception as e: |
| self.logger.warning("Failed to remove forwarded or reversed ports: %s" % e) |
| super().stop(force=force) |
| |
| def get_env(self, binary, debug_info, headless, chaos_mode_flags, e10s): |
| env = get_environ(chaos_mode_flags) |
| env["RUST_BACKTRACE"] = "1" |
| return env |
| |
| def executor_browser(self): |
| cls, args = super().executor_browser() |
| args["androidPackage"] = self.package_name |
| args["androidDeviceSerial"] = self.device_serial |
| args["env"] = self.env |
| args["supports_devtools"] = False |
| return cls, args |