1import os
2
3import moznetwork
4from mozrunner import FennecEmulatorRunner
5
6from .base import (get_free_port,
7                   cmd_arg,
8                   browser_command)
9from ..executors.executormarionette import (MarionetteTestharnessExecutor,  # noqa: F401
10                                            MarionetteRefTestExecutor,  # noqa: F401
11                                            MarionetteCrashtestExecutor)  # noqa: F401
12from .base import (Browser,
13                   ExecutorBrowser)
14from .firefox import (get_timeout_multiplier,  # noqa: F401
15                      run_info_extras as fx_run_info_extras,
16                      update_properties,  # noqa: F401
17                      executor_kwargs,  # noqa: F401
18                      ProfileCreator as FirefoxProfileCreator)
19
20
21__wptrunner__ = {"product": "firefox_android",
22                 "check_args": "check_args",
23                 "browser": "FirefoxAndroidBrowser",
24                 "executor": {"testharness": "MarionetteTestharnessExecutor",
25                              "reftest": "MarionetteRefTestExecutor",
26                              "crashtest": "MarionetteCrashtestExecutor"},
27                 "browser_kwargs": "browser_kwargs",
28                 "executor_kwargs": "executor_kwargs",
29                 "env_extras": "env_extras",
30                 "env_options": "env_options",
31                 "run_info_extras": "run_info_extras",
32                 "update_properties": "update_properties",
33                 "timeout_multiplier": "get_timeout_multiplier"}
34
35
36def check_args(**kwargs):
37    pass
38
39
40def browser_kwargs(logger, test_type, run_info_data, config, **kwargs):
41    return {"adb_binary": kwargs["adb_binary"],
42            "package_name": kwargs["package_name"],
43            "device_serial": kwargs["device_serial"],
44            "prefs_root": kwargs["prefs_root"],
45            "extra_prefs": kwargs["extra_prefs"],
46            "test_type": test_type,
47            "debug_info": kwargs["debug_info"],
48            "symbols_path": kwargs["symbols_path"],
49            "stackwalk_binary": kwargs["stackwalk_binary"],
50            "certutil_binary": kwargs["certutil_binary"],
51            "ca_certificate_path": config.ssl_config["ca_cert_path"],
52            "enable_webrender": kwargs["enable_webrender"],
53            "stackfix_dir": kwargs["stackfix_dir"],
54            "binary_args": kwargs["binary_args"],
55            "timeout_multiplier": get_timeout_multiplier(test_type,
56                                                         run_info_data,
57                                                         **kwargs),
58            "e10s": run_info_data["e10s"],
59            # desktop only
60            "leak_check": False,
61            "stylo_threads": kwargs["stylo_threads"],
62            "chaos_mode_flags": kwargs["chaos_mode_flags"],
63            "config": config,
64            "install_fonts": kwargs["install_fonts"],
65            "tests_root": config.doc_root,
66            "specialpowers_path": kwargs["specialpowers_path"]}
67
68
69def env_extras(**kwargs):
70    return []
71
72
73def run_info_extras(**kwargs):
74    rv = fx_run_info_extras(**kwargs)
75    package = kwargs["package_name"]
76    rv.update({"e10s": True if package is not None and "geckoview" in package else False,
77               "headless": False})
78    return rv
79
80
81def env_options():
82    # The server host is set to public localhost IP so that resources can be accessed
83    # from Android emulator
84    return {"server_host": moznetwork.get_ip(),
85            "bind_address": False,
86            "supports_debugger": True}
87
88
89class ProfileCreator(FirefoxProfileCreator):
90    def __init__(self, logger, prefs_root, config, test_type, extra_prefs,
91                 enable_fission, browser_channel, certutil_binary, ca_certificate_path):
92        super(ProfileCreator, self).__init__(logger, prefs_root, config, test_type, extra_prefs,
93                                             True, enable_fission, browser_channel, None,
94                                             certutil_binary, ca_certificate_path)
95
96    def _set_required_prefs(self, profile):
97        profile.set_preferences({
98            "network.dns.localDomains": ",".join(self.config.domains_set),
99            "dom.disable_open_during_load": False,
100            "places.history.enabled": False,
101            "dom.send_after_paint_to_content": True,
102            "network.preload": True,
103            "browser.tabs.remote.autostart": True,
104        })
105
106        if self.test_type == "reftest":
107            self.logger.info("Setting android reftest preferences")
108            profile.set_preferences({
109                "browser.viewport.desktopWidth": 800,
110                # Disable high DPI
111                "layout.css.devPixelsPerPx": "1.0",
112                # Ensure that the full browser element
113                # appears in the screenshot
114                "apz.allow_zooming": False,
115                "android.widget_paints_background": False,
116                # Ensure that scrollbars are always painted
117                "layout.testing.overlay-scrollbars.always-visible": True,
118            })
119
120
121class FirefoxAndroidBrowser(Browser):
122    init_timeout = 300
123    shutdown_timeout = 60
124
125    def __init__(self, logger, prefs_root, test_type, package_name="org.mozilla.geckoview.test",
126                 device_serial="emulator-5444", extra_prefs=None, debug_info=None,
127                 symbols_path=None, stackwalk_binary=None, certutil_binary=None,
128                 ca_certificate_path=None, e10s=False, enable_webrender=False, stackfix_dir=None,
129                 binary_args=None, timeout_multiplier=None, leak_check=False, asan=False,
130                 stylo_threads=1, chaos_mode_flags=None, config=None, browser_channel="nightly",
131                 install_fonts=False, tests_root=None, specialpowers_path=None, adb_binary=None,
132                 **kwargs):
133
134        super(FirefoxAndroidBrowser, self).__init__(logger)
135        self.prefs_root = prefs_root
136        self.test_type = test_type
137        self.package_name = package_name
138        self.device_serial = device_serial
139        self.debug_info = debug_info
140        self.symbols_path = symbols_path
141        self.stackwalk_binary = stackwalk_binary
142        self.certutil_binary = certutil_binary
143        self.ca_certificate_path = ca_certificate_path
144        self.e10s = True
145        self.enable_webrender = enable_webrender
146        self.stackfix_dir = stackfix_dir
147        self.binary_args = binary_args
148        self.timeout_multiplier = timeout_multiplier
149        self.leak_check = leak_check
150        self.asan = asan
151        self.stylo_threads = stylo_threads
152        self.chaos_mode_flags = chaos_mode_flags
153        self.config = config
154        self.browser_channel = browser_channel
155        self.install_fonts = install_fonts
156        self.tests_root = tests_root
157        self.specialpowers_path = specialpowers_path
158        self.adb_binary = adb_binary
159
160        self.profile_creator = ProfileCreator(logger,
161                                              prefs_root,
162                                              config,
163                                              test_type,
164                                              extra_prefs,
165                                              False,
166                                              browser_channel,
167                                              certutil_binary,
168                                              ca_certificate_path)
169
170        self.marionette_port = None
171        self.profile = None
172        self.runner = None
173        self._settings = {}
174
175    def settings(self, test):
176        self._settings = {"check_leaks": self.leak_check and not test.leaks,
177                          "lsan_allowed": test.lsan_allowed,
178                          "lsan_max_stack_depth": test.lsan_max_stack_depth,
179                          "mozleak_allowed": self.leak_check and test.mozleak_allowed,
180                          "mozleak_thresholds": self.leak_check and test.mozleak_threshold,
181                          "special_powers": self.specialpowers_path and test.url_base == "/_mozilla/"}
182        return self._settings
183
184    def start(self, **kwargs):
185        if self.marionette_port is None:
186            self.marionette_port = get_free_port()
187
188        addons = [self.specialpowers_path] if self._settings.get("special_powers") else None
189        self.profile = self.profile_creator.create(addons=addons)
190        self.profile.set_preferences({"marionette.port": self.marionette_port})
191
192        if self.install_fonts:
193            self.logger.debug("Copying Ahem font to profile")
194            font_dir = os.path.join(self.profile.profile, "fonts")
195            if not os.path.exists(font_dir):
196                os.makedirs(font_dir)
197            with open(os.path.join(self.tests_root, "fonts", "Ahem.ttf"), "rb") as src:
198                with open(os.path.join(font_dir, "Ahem.ttf"), "wb") as dest:
199                    dest.write(src.read())
200
201        self.leak_report_file = None
202
203        debug_args, cmd = browser_command(self.package_name,
204                                          self.binary_args if self.binary_args else [] +
205                                          [cmd_arg("marionette"), "about:blank"],
206                                          self.debug_info)
207
208        env = {}
209        env["MOZ_CRASHREPORTER"] = "1"
210        env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
211        env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
212        env["STYLO_THREADS"] = str(self.stylo_threads)
213        if self.chaos_mode_flags is not None:
214            env["MOZ_CHAOSMODE"] = str(self.chaos_mode_flags)
215        if self.enable_webrender:
216            env["MOZ_WEBRENDER"] = "1"
217        else:
218            env["MOZ_WEBRENDER"] = "0"
219
220        self.runner = FennecEmulatorRunner(app=self.package_name,
221                                           profile=self.profile,
222                                           cmdargs=cmd[1:],
223                                           env=env,
224                                           symbols_path=self.symbols_path,
225                                           serial=self.device_serial,
226                                           # TODO - choose appropriate log dir
227                                           logdir=os.getcwd(),
228                                           adb_path=self.adb_binary,
229                                           explicit_cleanup=True)
230
231        self.logger.debug("Starting %s" % self.package_name)
232        # connect to a running emulator
233        self.runner.device.connect()
234
235        self.runner.stop()
236        self.runner.start(debug_args=debug_args,
237                          interactive=self.debug_info and self.debug_info.interactive)
238
239        self.runner.device.device.forward(
240            local="tcp:{}".format(self.marionette_port),
241            remote="tcp:{}".format(self.marionette_port))
242
243        for ports in self.config.ports.values():
244            for port in ports:
245                self.runner.device.device.reverse(
246                    local="tcp:{}".format(port),
247                    remote="tcp:{}".format(port))
248
249        self.logger.debug("%s Started" % self.package_name)
250
251    def stop(self, force=False):
252        if self.runner is not None:
253            if self.runner.device.connected:
254                try:
255                    self.runner.device.device.remove_forwards()
256                    self.runner.device.device.remove_reverses()
257                except Exception as e:
258                    self.logger.warning("Failed to remove forwarded or reversed ports: %s" % e)
259            # We assume that stopping the runner prompts the
260            # browser to shut down.
261            self.runner.cleanup()
262        self.logger.debug("stopped")
263
264    def pid(self):
265        if self.runner.process_handler is None:
266            return None
267
268        try:
269            return self.runner.process_handler.pid
270        except AttributeError:
271            return None
272
273    def is_alive(self):
274        if self.runner:
275            return self.runner.is_running()
276        return False
277
278    def cleanup(self, force=False):
279        self.stop(force)
280
281    def executor_browser(self):
282        return ExecutorBrowser, {"marionette_port": self.marionette_port,
283                                 # We never want marionette to install extensions because
284                                 # that doesn't work on Android; instead they are in the profile
285                                 "extensions": []}
286
287    def check_crash(self, process, test):
288        if not os.environ.get("MINIDUMP_STACKWALK", "") and self.stackwalk_binary:
289            os.environ["MINIDUMP_STACKWALK"] = self.stackwalk_binary
290        return bool(self.runner.check_for_crashes(test_name=test))
291