27be0c54b7854a014d78fb96f76bc662e183d972
[WebKit-https.git] / WebDriverTests / imported / w3c / tools / wptrunner / wptrunner / executors / base.py
1 import hashlib
2 import httplib
3 import os
4 import threading
5 import traceback
6 import socket
7 import urlparse
8 from abc import ABCMeta, abstractmethod
9
10 from ..testrunner import Stop
11
12 here = os.path.split(__file__)[0]
13
14 # Extra timeout to use after internal test timeout at which the harness
15 # should force a timeout
16 extra_timeout = 5 # seconds
17
18
19 def executor_kwargs(test_type, server_config, cache_manager, **kwargs):
20     timeout_multiplier = kwargs["timeout_multiplier"]
21     if timeout_multiplier is None:
22         timeout_multiplier = 1
23
24     executor_kwargs = {"server_config": server_config,
25                        "timeout_multiplier": timeout_multiplier,
26                        "debug_info": kwargs["debug_info"]}
27
28     if test_type == "reftest":
29         executor_kwargs["screenshot_cache"] = cache_manager.dict()
30
31     if test_type == "wdspec":
32         executor_kwargs["binary"] = kwargs.get("binary")
33         executor_kwargs["webdriver_binary"] = kwargs.get("webdriver_binary")
34         executor_kwargs["webdriver_args"] = kwargs.get("webdriver_args")
35
36     return executor_kwargs
37
38
39 def strip_server(url):
40     """Remove the scheme and netloc from a url, leaving only the path and any query
41     or fragment.
42
43     url - the url to strip
44
45     e.g. http://example.org:8000/tests?id=1#2 becomes /tests?id=1#2"""
46
47     url_parts = list(urlparse.urlsplit(url))
48     url_parts[0] = ""
49     url_parts[1] = ""
50     return urlparse.urlunsplit(url_parts)
51
52
53 class TestharnessResultConverter(object):
54     harness_codes = {0: "OK",
55                      1: "ERROR",
56                      2: "TIMEOUT"}
57
58     test_codes = {0: "PASS",
59                   1: "FAIL",
60                   2: "TIMEOUT",
61                   3: "NOTRUN"}
62
63     def __call__(self, test, result):
64         """Convert a JSON result into a (TestResult, [SubtestResult]) tuple"""
65         result_url, status, message, stack, subtest_results = result
66         assert result_url == test.url, ("Got results from %s, expected %s" %
67                                       (result_url, test.url))
68         harness_result = test.result_cls(self.harness_codes[status], message)
69         return (harness_result,
70                 [test.subtest_result_cls(name, self.test_codes[status], message, stack)
71                  for name, status, message, stack in subtest_results])
72
73
74 testharness_result_converter = TestharnessResultConverter()
75
76
77 def reftest_result_converter(self, test, result):
78     return (test.result_cls(result["status"], result["message"],
79                             extra=result.get("extra")), [])
80
81
82 def pytest_result_converter(self, test, data):
83     harness_data, subtest_data = data
84
85     if subtest_data is None:
86         subtest_data = []
87
88     harness_result = test.result_cls(*harness_data)
89     subtest_results = [test.subtest_result_cls(*item) for item in subtest_data]
90
91     return (harness_result, subtest_results)
92
93
94 class ExecutorException(Exception):
95     def __init__(self, status, message):
96         self.status = status
97         self.message = message
98
99
100 class TestExecutor(object):
101     __metaclass__ = ABCMeta
102
103     test_type = None
104     convert_result = None
105
106     def __init__(self, browser, server_config, timeout_multiplier=1,
107                  debug_info=None, **kwargs):
108         """Abstract Base class for object that actually executes the tests in a
109         specific browser. Typically there will be a different TestExecutor
110         subclass for each test type and method of executing tests.
111
112         :param browser: ExecutorBrowser instance providing properties of the
113                         browser that will be tested.
114         :param server_config: Dictionary of wptserve server configuration of the
115                               form stored in TestEnvironment.external_config
116         :param timeout_multiplier: Multiplier relative to base timeout to use
117                                    when setting test timeout.
118         """
119         self.runner = None
120         self.browser = browser
121         self.server_config = server_config
122         self.timeout_multiplier = timeout_multiplier
123         self.debug_info = debug_info
124         self.last_environment = {"protocol": "http",
125                                  "prefs": {}}
126         self.protocol = None # This must be set in subclasses
127
128     @property
129     def logger(self):
130         """StructuredLogger for this executor"""
131         if self.runner is not None:
132             return self.runner.logger
133
134     def setup(self, runner):
135         """Run steps needed before tests can be started e.g. connecting to
136         browser instance
137
138         :param runner: TestRunner instance that is going to run the tests"""
139         self.runner = runner
140         if self.protocol is not None:
141             self.protocol.setup(runner)
142
143     def teardown(self):
144         """Run cleanup steps after tests have finished"""
145         if self.protocol is not None:
146             self.protocol.teardown()
147
148     def run_test(self, test):
149         """Run a particular test.
150
151         :param test: The test to run"""
152         if test.environment != self.last_environment:
153             self.on_environment_change(test.environment)
154
155         try:
156             result = self.do_test(test)
157         except Exception as e:
158             result = self.result_from_exception(test, e)
159
160         if result is Stop:
161             return result
162
163         # log result of parent test
164         if result[0].status == "ERROR":
165             self.logger.debug(result[0].message)
166
167         self.last_environment = test.environment
168
169         self.runner.send_message("test_ended", test, result)
170
171     def server_url(self, protocol):
172         return "%s://%s:%s" % (protocol,
173                                self.server_config["host"],
174                                self.server_config["ports"][protocol][0])
175
176     def test_url(self, test):
177         return urlparse.urljoin(self.server_url(test.environment["protocol"]), test.url)
178
179     @abstractmethod
180     def do_test(self, test):
181         """Test-type and protocol specific implementation of running a
182         specific test.
183
184         :param test: The test to run."""
185         pass
186
187     def on_environment_change(self, new_environment):
188         pass
189
190     def result_from_exception(self, test, e):
191         if hasattr(e, "status") and e.status in test.result_cls.statuses:
192             status = e.status
193         else:
194             status = "ERROR"
195         message = unicode(getattr(e, "message", ""))
196         if message:
197             message += "\n"
198         message += traceback.format_exc(e)
199         return test.result_cls(status, message), []
200
201
202 class TestharnessExecutor(TestExecutor):
203     convert_result = testharness_result_converter
204
205
206 class RefTestExecutor(TestExecutor):
207     convert_result = reftest_result_converter
208
209     def __init__(self, browser, server_config, timeout_multiplier=1, screenshot_cache=None,
210                  debug_info=None, **kwargs):
211         TestExecutor.__init__(self, browser, server_config,
212                               timeout_multiplier=timeout_multiplier,
213                               debug_info=debug_info)
214
215         self.screenshot_cache = screenshot_cache
216
217
218 class RefTestImplementation(object):
219     def __init__(self, executor):
220         self.timeout_multiplier = executor.timeout_multiplier
221         self.executor = executor
222         # Cache of url:(screenshot hash, screenshot). Typically the
223         # screenshot is None, but we set this value if a test fails
224         # and the screenshot was taken from the cache so that we may
225         # retrieve the screenshot from the cache directly in the future
226         self.screenshot_cache = self.executor.screenshot_cache
227         self.message = None
228
229     def setup(self):
230         pass
231
232     def teardown(self):
233         pass
234
235     @property
236     def logger(self):
237         return self.executor.logger
238
239     def get_hash(self, test, viewport_size, dpi):
240         timeout = test.timeout * self.timeout_multiplier
241         key = (test.url, viewport_size, dpi)
242
243         if key not in self.screenshot_cache:
244             success, data = self.executor.screenshot(test, viewport_size, dpi)
245
246             if not success:
247                 return False, data
248
249             screenshot = data
250             hash_value = hashlib.sha1(screenshot).hexdigest()
251
252             self.screenshot_cache[key] = (hash_value, None)
253
254             rv = (hash_value, screenshot)
255         else:
256             rv = self.screenshot_cache[key]
257
258         self.message.append("%s %s" % (test.url, rv[0]))
259         return True, rv
260
261     def is_pass(self, lhs_hash, rhs_hash, relation):
262         assert relation in ("==", "!=")
263         self.message.append("Testing %s %s %s" % (lhs_hash, relation, rhs_hash))
264         return ((relation == "==" and lhs_hash == rhs_hash) or
265                 (relation == "!=" and lhs_hash != rhs_hash))
266
267     def run_test(self, test):
268         viewport_size = test.viewport_size
269         dpi = test.dpi
270         self.message = []
271
272         # Depth-first search of reference tree, with the goal
273         # of reachings a leaf node with only pass results
274
275         stack = list(((test, item[0]), item[1]) for item in reversed(test.references))
276         while stack:
277             hashes = [None, None]
278             screenshots = [None, None]
279
280             nodes, relation = stack.pop()
281
282             for i, node in enumerate(nodes):
283                 success, data = self.get_hash(node, viewport_size, dpi)
284                 if success is False:
285                     return {"status": data[0], "message": data[1]}
286
287                 hashes[i], screenshots[i] = data
288
289             if self.is_pass(hashes[0], hashes[1], relation):
290                 if nodes[1].references:
291                     stack.extend(list(((nodes[1], item[0]), item[1]) for item in reversed(nodes[1].references)))
292                 else:
293                     # We passed
294                     return {"status":"PASS", "message": None}
295
296         # We failed, so construct a failure message
297
298         for i, (node, screenshot) in enumerate(zip(nodes, screenshots)):
299             if screenshot is None:
300                 success, screenshot = self.retake_screenshot(node, viewport_size, dpi)
301                 if success:
302                     screenshots[i] = screenshot
303
304         log_data = [{"url": nodes[0].url, "screenshot": screenshots[0]}, relation,
305                     {"url": nodes[1].url, "screenshot": screenshots[1]}]
306
307         return {"status": "FAIL",
308                 "message": "\n".join(self.message),
309                 "extra": {"reftest_screenshots": log_data}}
310
311     def retake_screenshot(self, node, viewport_size, dpi):
312         success, data = self.executor.screenshot(node, viewport_size, dpi)
313         if not success:
314             return False, data
315
316         key = (node.url, viewport_size, dpi)
317         hash_val, _ = self.screenshot_cache[key]
318         self.screenshot_cache[key] = hash_val, data
319         return True, data
320
321
322 class WdspecExecutor(TestExecutor):
323     convert_result = pytest_result_converter
324     protocol_cls = None
325
326     def __init__(self, browser, server_config, webdriver_binary,
327                  webdriver_args, timeout_multiplier=1, capabilities=None,
328                  debug_info=None, **kwargs):
329         self.do_delayed_imports()
330         TestExecutor.__init__(self, browser, server_config,
331                               timeout_multiplier=timeout_multiplier,
332                               debug_info=debug_info)
333         self.webdriver_binary = webdriver_binary
334         self.webdriver_args = webdriver_args
335         self.timeout_multiplier = timeout_multiplier
336         self.capabilities = capabilities
337         self.protocol = self.protocol_cls(self, browser)
338
339     def is_alive(self):
340         return self.protocol.is_alive
341
342     def on_environment_change(self, new_environment):
343         pass
344
345     def do_test(self, test):
346         timeout = test.timeout * self.timeout_multiplier + extra_timeout
347
348         success, data = WdspecRun(self.do_wdspec,
349                                   self.protocol.session_config,
350                                   test.abs_path,
351                                   timeout).run()
352
353         if success:
354             return self.convert_result(test, data)
355
356         return (test.result_cls(*data), [])
357
358     def do_wdspec(self, session_config, path, timeout):
359         harness_result = ("OK", None)
360         subtest_results = pytestrunner.run(path,
361                                            self.server_config,
362                                            session_config,
363                                            timeout=timeout)
364         return (harness_result, subtest_results)
365
366     def do_delayed_imports(self):
367         global pytestrunner
368         from . import pytestrunner
369
370
371 class Protocol(object):
372     def __init__(self, executor, browser):
373         self.executor = executor
374         self.browser = browser
375
376     @property
377     def logger(self):
378         return self.executor.logger
379
380     def setup(self, runner):
381         pass
382
383     def teardown(self):
384         pass
385
386     def wait(self):
387         pass
388
389
390 class WdspecRun(object):
391     def __init__(self, func, session, path, timeout):
392         self.func = func
393         self.result = (None, None)
394         self.session = session
395         self.path = path
396         self.timeout = timeout
397         self.result_flag = threading.Event()
398
399     def run(self):
400         """Runs function in a thread and interrupts it if it exceeds the
401         given timeout.  Returns (True, (Result, [SubtestResult ...])) in
402         case of success, or (False, (status, extra information)) in the
403         event of failure.
404         """
405
406         executor = threading.Thread(target=self._run)
407         executor.start()
408
409         flag = self.result_flag.wait(self.timeout)
410         if self.result[1] is None:
411             self.result = False, ("EXTERNAL-TIMEOUT", None)
412
413         return self.result
414
415     def _run(self):
416         try:
417             self.result = True, self.func(self.session, self.path, self.timeout)
418         except (socket.timeout, IOError):
419             self.result = False, ("CRASH", None)
420         except Exception as e:
421             message = getattr(e, "message")
422             if message:
423                 message += "\n"
424             message += traceback.format_exc(e)
425             self.result = False, ("ERROR", message)
426         finally:
427             self.result_flag.set()
428
429
430 class WebDriverProtocol(Protocol):
431     server_cls = None
432
433     def __init__(self, executor, browser):
434         Protocol.__init__(self, executor, browser)
435         self.webdriver_binary = executor.webdriver_binary
436         self.webdriver_args = executor.webdriver_args
437         self.capabilities = self.executor.capabilities
438         self.session_config = None
439         self.server = None
440
441     def setup(self, runner):
442         """Connect to browser via the HTTP server."""
443         try:
444             self.server = self.server_cls(
445                 self.logger,
446                 binary=self.webdriver_binary,
447                 args=self.webdriver_args)
448             self.server.start(block=False)
449             self.logger.info(
450                 "WebDriver HTTP server listening at %s" % self.server.url)
451             self.session_config = {"host": self.server.host,
452                                    "port": self.server.port,
453                                    "capabilities": self.capabilities}
454         except Exception:
455             self.logger.error(traceback.format_exc())
456             self.executor.runner.send_message("init_failed")
457         else:
458             self.executor.runner.send_message("init_succeeded")
459
460     def teardown(self):
461         if self.server is not None and self.server.is_alive:
462             self.server.stop()
463
464     @property
465     def is_alive(self):
466         """Test that the connection is still alive.
467
468         Because the remote communication happens over HTTP we need to
469         make an explicit request to the remote.  It is allowed for
470         WebDriver spec tests to not have a WebDriver session, since this
471         may be what is tested.
472
473         An HTTP request to an invalid path that results in a 404 is
474         proof enough to us that the server is alive and kicking.
475         """
476         conn = httplib.HTTPConnection(self.server.host, self.server.port)
477         conn.request("HEAD", self.server.base_path + "invalid")
478         res = conn.getresponse()
479         return res.status == 404