1import json
2import os
3import socket
4import traceback
5
6from .base import (Protocol,
7                   BaseProtocolPart,
8                   RefTestExecutor,
9                   RefTestImplementation,
10                   TestharnessExecutor,
11                   TimedRunner,
12                   strip_server)
13from ..webdriver_server import wait_for_service
14
15webdriver = None
16ServoCommandExtensions = None
17
18here = os.path.dirname(__file__)
19
20
21def do_delayed_imports():
22    global webdriver
23    import webdriver
24
25    global ServoCommandExtensions
26
27    class ServoCommandExtensions(object):
28        def __init__(self, session):
29            self.session = session
30
31        @webdriver.client.command
32        def get_prefs(self, *prefs):
33            body = {"prefs": list(prefs)}
34            return self.session.send_session_command("POST", "servo/prefs/get", body)
35
36        @webdriver.client.command
37        def set_prefs(self, prefs):
38            body = {"prefs": prefs}
39            return self.session.send_session_command("POST", "servo/prefs/set", body)
40
41        @webdriver.client.command
42        def reset_prefs(self, *prefs):
43            body = {"prefs": list(prefs)}
44            return self.session.send_session_command("POST", "servo/prefs/reset", body)
45
46        def change_prefs(self, old_prefs, new_prefs):
47            # Servo interprets reset with an empty list as reset everything
48            if old_prefs:
49                self.reset_prefs(*old_prefs.keys())
50            self.set_prefs({k: parse_pref_value(v) for k, v in new_prefs.items()})
51
52
53# See parse_pref_from_command_line() in components/config/opts.rs
54def parse_pref_value(value):
55    if value == "true":
56        return True
57    if value == "false":
58        return False
59    try:
60        return float(value)
61    except ValueError:
62        return value
63
64
65class ServoBaseProtocolPart(BaseProtocolPart):
66    def execute_script(self, script, asynchronous=False):
67        pass
68
69    def set_timeout(self, timeout):
70        pass
71
72    def wait(self):
73        pass
74
75    def set_window(self, handle):
76        pass
77
78    def window_handles(self):
79        return []
80
81    def load(self, url):
82        pass
83
84
85class ServoWebDriverProtocol(Protocol):
86    implements = [ServoBaseProtocolPart]
87
88    def __init__(self, executor, browser, capabilities, **kwargs):
89        do_delayed_imports()
90        Protocol.__init__(self, executor, browser)
91        self.capabilities = capabilities
92        self.host = browser.webdriver_host
93        self.port = browser.webdriver_port
94        self.init_timeout = browser.init_timeout
95        self.session = None
96
97    def connect(self):
98        """Connect to browser via WebDriver."""
99        wait_for_service((self.host, self.port), timeout=self.init_timeout)
100
101        self.session = webdriver.Session(self.host, self.port, extension=ServoCommandExtensions)
102        self.session.start()
103
104    def after_connect(self):
105        pass
106
107    def teardown(self):
108        self.logger.debug("Hanging up on WebDriver session")
109        try:
110            self.session.end()
111        except Exception:
112            pass
113
114    def is_alive(self):
115        try:
116            # Get a simple property over the connection
117            self.session.window_handle
118        # TODO what exception?
119        except Exception:
120            return False
121        return True
122
123    def wait(self):
124        while True:
125            try:
126                self.session.execute_async_script("")
127            except webdriver.TimeoutException:
128                pass
129            except (socket.timeout, IOError):
130                break
131            except Exception:
132                self.logger.error(traceback.format_exc())
133                break
134
135
136class ServoWebDriverRun(TimedRunner):
137    def set_timeout(self):
138        pass
139
140    def run_func(self):
141        try:
142            self.result = True, self.func(self.protocol.session, self.url, self.timeout)
143        except webdriver.TimeoutException:
144            self.result = False, ("EXTERNAL-TIMEOUT", None)
145        except (socket.timeout, IOError):
146            self.result = False, ("CRASH", None)
147        except Exception as e:
148            message = getattr(e, "message", "")
149            if message:
150                message += "\n"
151            message += traceback.format_exc()
152            self.result = False, ("INTERNAL-ERROR", e)
153        finally:
154            self.result_flag.set()
155
156
157class ServoWebDriverTestharnessExecutor(TestharnessExecutor):
158    supports_testdriver = True
159
160    def __init__(self, logger, browser, server_config, timeout_multiplier=1,
161                 close_after_done=True, capabilities=None, debug_info=None,
162                 **kwargs):
163        TestharnessExecutor.__init__(self, logger, browser, server_config, timeout_multiplier=1,
164                                     debug_info=None)
165        self.protocol = ServoWebDriverProtocol(self, browser, capabilities=capabilities)
166        with open(os.path.join(here, "testharness_servodriver.js")) as f:
167            self.script = f.read()
168        self.timeout = None
169
170    def on_protocol_change(self, new_protocol):
171        pass
172
173    def is_alive(self):
174        return self.protocol.is_alive()
175
176    def do_test(self, test):
177        url = self.test_url(test)
178
179        timeout = test.timeout * self.timeout_multiplier + self.extra_timeout
180
181        if timeout != self.timeout:
182            try:
183                self.protocol.session.timeouts.script = timeout
184                self.timeout = timeout
185            except IOError:
186                msg = "Lost WebDriver connection"
187                self.logger.error(msg)
188                return ("INTERNAL-ERROR", msg)
189
190        success, data = ServoWebDriverRun(self.logger,
191                                          self.do_testharness,
192                                          self.protocol,
193                                          url,
194                                          timeout,
195                                          self.extra_timeout).run()
196
197        if success:
198            return self.convert_result(test, data)
199
200        return (test.result_cls(*data), [])
201
202    def do_testharness(self, session, url, timeout):
203        session.url = url
204        result = json.loads(
205            session.execute_async_script(
206                self.script % {"abs_url": url,
207                               "url": strip_server(url),
208                               "timeout_multiplier": self.timeout_multiplier,
209                               "timeout": timeout * 1000}))
210        # Prevent leaking every page in history until Servo develops a more sane
211        # page cache
212        session.back()
213        return result
214
215    def on_environment_change(self, new_environment):
216        self.protocol.session.extension.change_prefs(
217            self.last_environment.get("prefs", {}),
218            new_environment.get("prefs", {})
219        )
220
221
222class TimeoutError(Exception):
223    pass
224
225
226class ServoWebDriverRefTestExecutor(RefTestExecutor):
227    def __init__(self, logger, browser, server_config, timeout_multiplier=1,
228                 screenshot_cache=None, capabilities=None, debug_info=None,
229                 **kwargs):
230        """Selenium WebDriver-based executor for reftests"""
231        RefTestExecutor.__init__(self,
232                                 logger,
233                                 browser,
234                                 server_config,
235                                 screenshot_cache=screenshot_cache,
236                                 timeout_multiplier=timeout_multiplier,
237                                 debug_info=debug_info)
238        self.protocol = ServoWebDriverProtocol(self, browser,
239                                               capabilities=capabilities)
240        self.implementation = RefTestImplementation(self)
241        self.timeout = None
242        with open(os.path.join(here, "test-wait.js")) as f:
243            self.wait_script = f.read() % {"classname": "reftest-wait"}
244
245    def reset(self):
246        self.implementation.reset()
247
248    def is_alive(self):
249        return self.protocol.is_alive()
250
251    def do_test(self, test):
252        try:
253            result = self.implementation.run_test(test)
254            return self.convert_result(test, result)
255        except IOError:
256            return test.result_cls("CRASH", None), []
257        except TimeoutError:
258            return test.result_cls("TIMEOUT", None), []
259        except Exception as e:
260            message = getattr(e, "message", "")
261            if message:
262                message += "\n"
263            message += traceback.format_exc()
264            return test.result_cls("INTERNAL-ERROR", message), []
265
266    def screenshot(self, test, viewport_size, dpi, page_ranges):
267        # https://github.com/web-platform-tests/wpt/issues/7135
268        assert viewport_size is None
269        assert dpi is None
270
271        timeout = (test.timeout * self.timeout_multiplier + self.extra_timeout
272                   if self.debug_info is None else None)
273
274        if self.timeout != timeout:
275            try:
276                self.protocol.session.timeouts.script = timeout
277                self.timeout = timeout
278            except IOError:
279                msg = "Lost webdriver connection"
280                self.logger.error(msg)
281                return ("INTERNAL-ERROR", msg)
282
283        return ServoWebDriverRun(self.logger,
284                                 self._screenshot,
285                                 self.protocol,
286                                 self.test_url(test),
287                                 timeout,
288                                 self.extra_timeout).run()
289
290    def _screenshot(self, session, url, timeout):
291        session.url = url
292        session.execute_async_script(self.wait_script)
293        return session.screenshot()
294
295    def on_environment_change(self, new_environment):
296        self.protocol.session.extension.change_prefs(
297            self.last_environment.get("prefs", {}),
298            new_environment.get("prefs", {})
299        )
300