Clean up ChunkedUpdateDrawingAreaProxy
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / layout_tests / port / chromium.py
1 #!/usr/bin/env python
2 # Copyright (C) 2010 Google Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 #     * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 #     * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 #     * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 """Chromium implementations of the Port interface."""
31
32 from __future__ import with_statement
33
34 import codecs
35 import errno
36 import logging
37 import os
38 import re
39 import shutil
40 import signal
41 import subprocess
42 import sys
43 import tempfile
44 import time
45 import webbrowser
46
47 from webkitpy.common.system.path import cygpath
48 from webkitpy.layout_tests.layout_package import test_expectations
49 from webkitpy.layout_tests.layout_package import test_output
50
51 import base
52 import http_server
53
54 # Chromium DRT on OSX uses WebKitDriver.
55 if sys.platform == 'darwin':
56     import webkit
57
58 import websocket_server
59
60 _log = logging.getLogger("webkitpy.layout_tests.port.chromium")
61
62
63 # FIXME: This function doesn't belong in this package.
64 def check_file_exists(path_to_file, file_description, override_step=None,
65                       logging=True):
66     """Verify the file is present where expected or log an error.
67
68     Args:
69         file_name: The (human friendly) name or description of the file
70             you're looking for (e.g., "HTTP Server"). Used for error logging.
71         override_step: An optional string to be logged if the check fails.
72         logging: Whether or not log the error messages."""
73     if not os.path.exists(path_to_file):
74         if logging:
75             _log.error('Unable to find %s' % file_description)
76             _log.error('    at %s' % path_to_file)
77             if override_step:
78                 _log.error('    %s' % override_step)
79                 _log.error('')
80         return False
81     return True
82
83
84 class ChromiumPort(base.Port):
85     """Abstract base class for Chromium implementations of the Port class."""
86
87     def __init__(self, **kwargs):
88         base.Port.__init__(self, **kwargs)
89         self._chromium_base_dir = None
90
91     def baseline_path(self):
92         return self._webkit_baseline_path(self._name)
93
94     def check_build(self, needs_http):
95         result = True
96
97         dump_render_tree_binary_path = self._path_to_driver()
98         result = check_file_exists(dump_render_tree_binary_path,
99                                     'test driver') and result
100         if result and self.get_option('build'):
101             result = self._check_driver_build_up_to_date(
102                 self.get_option('configuration'))
103         else:
104             _log.error('')
105
106         helper_path = self._path_to_helper()
107         if helper_path:
108             result = check_file_exists(helper_path,
109                                        'layout test helper') and result
110
111         if self.get_option('pixel_tests'):
112             result = self.check_image_diff(
113                 'To override, invoke with --no-pixel-tests') and result
114
115         # It's okay if pretty patch isn't available, but we will at
116         # least log a message.
117         self.check_pretty_patch()
118
119         return result
120
121     def check_sys_deps(self, needs_http):
122         cmd = [self._path_to_driver(), '--check-layout-test-sys-deps']
123         if self._executive.run_command(cmd, return_exit_code=True):
124             _log.error('System dependencies check failed.')
125             _log.error('To override, invoke with --nocheck-sys-deps')
126             _log.error('')
127             return False
128         return True
129
130     def check_image_diff(self, override_step=None, logging=True):
131         image_diff_path = self._path_to_image_diff()
132         return check_file_exists(image_diff_path, 'image diff exe',
133                                  override_step, logging)
134
135     def diff_image(self, expected_contents, actual_contents,
136                    diff_filename=None):
137         executable = self._path_to_image_diff()
138
139         tempdir = tempfile.mkdtemp()
140         expected_filename = os.path.join(tempdir, "expected.png")
141         with open(expected_filename, 'w+b') as file:
142             file.write(expected_contents)
143         actual_filename = os.path.join(tempdir, "actual.png")
144         with open(actual_filename, 'w+b') as file:
145             file.write(actual_contents)
146
147         if diff_filename:
148             cmd = [executable, '--diff', expected_filename,
149                    actual_filename, diff_filename]
150         else:
151             cmd = [executable, expected_filename, actual_filename]
152
153         result = True
154         try:
155             exit_code = self._executive.run_command(cmd, return_exit_code=True)
156             if exit_code == 0:
157                 # The images are the same.
158                 result = False
159             elif exit_code != 1:
160                 _log.error("image diff returned an exit code of "
161                            + str(exit_code))
162                 # Returning False here causes the script to think that we
163                 # successfully created the diff even though we didn't.  If
164                 # we return True, we think that the images match but the hashes
165                 # don't match.
166                 # FIXME: Figure out why image_diff returns other values.
167                 result = False
168         except OSError, e:
169             if e.errno == errno.ENOENT or e.errno == errno.EACCES:
170                 _compare_available = False
171             else:
172                 raise e
173         finally:
174             shutil.rmtree(tempdir, ignore_errors=True)
175         return result
176
177     def driver_name(self):
178         if self._options.use_test_shell:
179             return "test_shell"
180         return "DumpRenderTree"
181
182     def path_from_chromium_base(self, *comps):
183         """Returns the full path to path made by joining the top of the
184         Chromium source tree and the list of path components in |*comps|."""
185         if not self._chromium_base_dir:
186             abspath = os.path.abspath(__file__)
187             offset = abspath.find('third_party')
188             if offset == -1:
189                 self._chromium_base_dir = os.path.join(
190                     abspath[0:abspath.find('WebKitTools')],
191                     'WebKit', 'chromium')
192             else:
193                 self._chromium_base_dir = abspath[0:offset]
194         return os.path.join(self._chromium_base_dir, *comps)
195
196     def path_to_test_expectations_file(self):
197         return self.path_from_webkit_base('LayoutTests', 'platform',
198             'chromium', 'test_expectations.txt')
199
200     def results_directory(self):
201         try:
202             return self.path_from_chromium_base('webkit',
203                 self.get_option('configuration'),
204                 self.get_option('results_directory'))
205         except AssertionError:
206             return self._build_path(self.get_option('configuration'),
207                                     self.get_option('results_directory'))
208
209     def setup_test_run(self):
210         # Delete the disk cache if any to ensure a clean test run.
211         dump_render_tree_binary_path = self._path_to_driver()
212         cachedir = os.path.split(dump_render_tree_binary_path)[0]
213         cachedir = os.path.join(cachedir, "cache")
214         if os.path.exists(cachedir):
215             shutil.rmtree(cachedir)
216
217     def create_driver(self, worker_number):
218         """Starts a new Driver and returns a handle to it."""
219         if not self.get_option('use_test_shell') and sys.platform == 'darwin':
220             return webkit.WebKitDriver(self, worker_number)
221         return ChromiumDriver(self, worker_number)
222
223     def start_helper(self):
224         helper_path = self._path_to_helper()
225         if helper_path:
226             _log.debug("Starting layout helper %s" % helper_path)
227             # Note: Not thread safe: http://bugs.python.org/issue2320
228             self._helper = subprocess.Popen([helper_path],
229                 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
230             is_ready = self._helper.stdout.readline()
231             if not is_ready.startswith('ready'):
232                 _log.error("layout_test_helper failed to be ready")
233
234     def stop_helper(self):
235         if self._helper:
236             _log.debug("Stopping layout test helper")
237             self._helper.stdin.write("x\n")
238             self._helper.stdin.close()
239             # wait() is not threadsafe and can throw OSError due to:
240             # http://bugs.python.org/issue1731717
241             self._helper.wait()
242
243     def test_base_platform_names(self):
244         return ('linux', 'mac', 'win')
245
246     def test_expectations(self):
247         """Returns the test expectations for this port.
248
249         Basically this string should contain the equivalent of a
250         test_expectations file. See test_expectations.py for more details."""
251         expectations_path = self.path_to_test_expectations_file()
252         with codecs.open(expectations_path, "r", "utf-8") as file:
253             return file.read()
254
255     def test_expectations_overrides(self):
256         try:
257             overrides_path = self.path_from_chromium_base('webkit', 'tools',
258                 'layout_tests', 'test_expectations.txt')
259         except AssertionError:
260             return None
261         if not os.path.exists(overrides_path):
262             return None
263         with codecs.open(overrides_path, "r", "utf-8") as file:
264             return file.read()
265
266     def skipped_layout_tests(self, extra_test_files=None):
267         expectations_str = self.test_expectations()
268         overrides_str = self.test_expectations_overrides()
269         test_platform_name = self.test_platform_name()
270         is_debug_mode = False
271
272         all_test_files = self.tests([])
273         if extra_test_files:
274             all_test_files.update(extra_test_files)
275
276         expectations = test_expectations.TestExpectations(
277             self, all_test_files, expectations_str, test_platform_name,
278             is_debug_mode, is_lint_mode=False, overrides=overrides_str)
279         tests_dir = self.layout_tests_dir()
280         return [self.relative_test_filename(test)
281                 for test in expectations.get_tests_with_result_type(test_expectations.SKIP)]
282
283     def test_platform_names(self):
284         return self.test_base_platform_names() + ('win-xp',
285             'win-vista', 'win-7')
286
287     def test_platform_name_to_name(self, test_platform_name):
288         if test_platform_name in self.test_platform_names():
289             return 'chromium-' + test_platform_name
290         raise ValueError('Unsupported test_platform_name: %s' %
291                          test_platform_name)
292
293     def test_repository_paths(self):
294         # Note: for JSON file's backward-compatibility we use 'chrome' rather
295         # than 'chromium' here.
296         repos = super(ChromiumPort, self).test_repository_paths()
297         repos.append(('chrome', self.path_from_chromium_base()))
298         return repos
299
300     #
301     # PROTECTED METHODS
302     #
303     # These routines should only be called by other methods in this file
304     # or any subclasses.
305     #
306
307     def _check_driver_build_up_to_date(self, configuration):
308         if configuration in ('Debug', 'Release'):
309             try:
310                 debug_path = self._path_to_driver('Debug')
311                 release_path = self._path_to_driver('Release')
312
313                 debug_mtime = os.stat(debug_path).st_mtime
314                 release_mtime = os.stat(release_path).st_mtime
315
316                 if (debug_mtime > release_mtime and configuration == 'Release' or
317                     release_mtime > debug_mtime and configuration == 'Debug'):
318                     _log.warning('You are not running the most '
319                                  'recent DumpRenderTree binary. You need to '
320                                  'pass --debug or not to select between '
321                                  'Debug and Release.')
322                     _log.warning('')
323             # This will fail if we don't have both a debug and release binary.
324             # That's fine because, in this case, we must already be running the
325             # most up-to-date one.
326             except OSError:
327                 pass
328         return True
329
330     def _chromium_baseline_path(self, platform):
331         if platform is None:
332             platform = self.name()
333         return self.path_from_webkit_base('LayoutTests', 'platform', platform)
334
335     def _convert_path(self, path):
336         """Handles filename conversion for subprocess command line args."""
337         # See note above in diff_image() for why we need this.
338         if sys.platform == 'cygwin':
339             return cygpath(path)
340         return path
341
342     def _path_to_image_diff(self):
343         binary_name = 'ImageDiff'
344         if self.get_option('use_test_shell'):
345             binary_name = 'image_diff'
346         return self._build_path(self.get_option('configuration'), binary_name)
347
348
349 class ChromiumDriver(base.Driver):
350     """Abstract interface for test_shell."""
351
352     def __init__(self, port, worker_number):
353         self._port = port
354         self._worker_number = worker_number
355         self._image_path = None
356         if self._port.get_option('pixel_tests'):
357             self._image_path = os.path.join(
358                 self._port.get_option('results_directory'),
359                 'png_result%s.png' % self._worker_number)
360
361     def cmd_line(self):
362         cmd = self._command_wrapper(self._port.get_option('wrapper'))
363         cmd.append(self._port._path_to_driver())
364         if self._port.get_option('pixel_tests'):
365             # See note above in diff_image() for why we need _convert_path().
366             cmd.append("--pixel-tests=" +
367                        self._port._convert_path(self._image_path))
368
369         if self._port.get_option('use_test_shell'):
370             cmd.append('--layout-tests')
371         else:
372             cmd.append('--test-shell')
373
374         if self._port.get_option('startup_dialog'):
375             cmd.append('--testshell-startup-dialog')
376
377         if self._port.get_option('gp_fault_error_box'):
378             cmd.append('--gp-fault-error-box')
379
380         if self._port.get_option('js_flags') is not None:
381             cmd.append('--js-flags="' + self._port.get_option('js_flags') + '"')
382
383         if self._port.get_option('multiple_loads') > 0:
384             cmd.append('--multiple-loads=' + str(self._port.get_option('multiple_loads')))
385
386         # test_shell does not support accelerated compositing.
387         if not self._port.get_option("use_test_shell"):
388             if self._port.get_option('accelerated_compositing'):
389                 cmd.append('--enable-accelerated-compositing')
390             if self._port.get_option('accelerated_2d_canvas'):
391                 cmd.append('--enable-accelerated-2d-canvas')
392         return cmd
393
394     def start(self):
395         # FIXME: Should be an error to call this method twice.
396         cmd = self.cmd_line()
397
398         # We need to pass close_fds=True to work around Python bug #2320
399         # (otherwise we can hang when we kill DumpRenderTree when we are running
400         # multiple threads). See http://bugs.python.org/issue2320 .
401         # Note that close_fds isn't supported on Windows, but this bug only
402         # shows up on Mac and Linux.
403         close_flag = sys.platform not in ('win32', 'cygwin')
404         self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
405                                       stdout=subprocess.PIPE,
406                                       stderr=subprocess.STDOUT,
407                                       close_fds=close_flag)
408
409     def poll(self):
410         # poll() is not threadsafe and can throw OSError due to:
411         # http://bugs.python.org/issue1731717
412         return self._proc.poll()
413
414     def _write_command_and_read_line(self, input=None):
415         """Returns a tuple: (line, did_crash)"""
416         try:
417             if input:
418                 if isinstance(input, unicode):
419                     # TestShell expects utf-8
420                     input = input.encode("utf-8")
421                 self._proc.stdin.write(input)
422             # DumpRenderTree text output is always UTF-8.  However some tests
423             # (e.g. webarchive) may spit out binary data instead of text so we
424             # don't bother to decode the output (for either DRT or test_shell).
425             line = self._proc.stdout.readline()
426             # We could assert() here that line correctly decodes as UTF-8.
427             return (line, False)
428         except IOError, e:
429             _log.error("IOError communicating w/ test_shell: " + str(e))
430             return (None, True)
431
432     def _test_shell_command(self, uri, timeoutms, checksum):
433         cmd = uri
434         if timeoutms:
435             cmd += ' ' + str(timeoutms)
436         if checksum:
437             cmd += ' ' + checksum
438         cmd += "\n"
439         return cmd
440
441     def _output_image(self):
442         """Returns the image output which driver generated."""
443         png_path = self._image_path
444         if png_path and os.path.isfile(png_path):
445             with open(png_path, 'rb') as image_file:
446                 return image_file.read()
447         else:
448             return None
449
450     def _output_image_with_retry(self):
451         # Retry a few more times because open() sometimes fails on Windows,
452         # raising "IOError: [Errno 13] Permission denied:"
453         retry_num = 50
454         timeout_seconds = 5.0
455         for i in range(retry_num):
456             try:
457                 return self._output_image()
458             except IOError, e:
459                 if e.errno == errno.EACCES:
460                     time.sleep(timeout_seconds / retry_num)
461                 else:
462                     raise e
463         return self._output_image()
464
465     def run_test(self, test_input):
466         output = []
467         error = []
468         crash = False
469         timeout = False
470         actual_uri = None
471         actual_checksum = None
472
473         start_time = time.time()
474
475         uri = self._port.filename_to_uri(test_input.filename)
476         cmd = self._test_shell_command(uri, test_input.timeout,
477                                        test_input.image_hash)
478         (line, crash) = self._write_command_and_read_line(input=cmd)
479
480         while not crash and line.rstrip() != "#EOF":
481             # Make sure we haven't crashed.
482             if line == '' and self.poll() is not None:
483                 # This is hex code 0xc000001d, which is used for abrupt
484                 # termination. This happens if we hit ctrl+c from the prompt
485                 # and we happen to be waiting on test_shell.
486                 # sdoyon: Not sure for which OS and in what circumstances the
487                 # above code is valid. What works for me under Linux to detect
488                 # ctrl+c is for the subprocess returncode to be negative
489                 # SIGINT. And that agrees with the subprocess documentation.
490                 if (-1073741510 == self._proc.returncode or
491                     - signal.SIGINT == self._proc.returncode):
492                     raise KeyboardInterrupt
493                 crash = True
494                 break
495
496             # Don't include #URL lines in our output
497             if line.startswith("#URL:"):
498                 actual_uri = line.rstrip()[5:]
499                 if uri != actual_uri:
500                     # GURL capitalizes the drive letter of a file URL.
501                     if (not re.search("^file:///[a-z]:", uri) or
502                         uri.lower() != actual_uri.lower()):
503                         _log.fatal("Test got out of sync:\n|%s|\n|%s|" %
504                                    (uri, actual_uri))
505                         raise AssertionError("test out of sync")
506             elif line.startswith("#MD5:"):
507                 actual_checksum = line.rstrip()[5:]
508             elif line.startswith("#TEST_TIMED_OUT"):
509                 timeout = True
510                 # Test timed out, but we still need to read until #EOF.
511             elif actual_uri:
512                 output.append(line)
513             else:
514                 error.append(line)
515
516             (line, crash) = self._write_command_and_read_line(input=None)
517
518         run_time = time.time() - start_time
519         return test_output.TestOutput(
520             ''.join(output), self._output_image_with_retry(), actual_checksum,
521             crash, run_time, timeout, ''.join(error))
522
523     def stop(self):
524         if self._proc:
525             self._proc.stdin.close()
526             self._proc.stdout.close()
527             if self._proc.stderr:
528                 self._proc.stderr.close()
529             if sys.platform not in ('win32', 'cygwin'):
530                 # Closing stdin/stdout/stderr hangs sometimes on OS X,
531                 # (see __init__(), above), and anyway we don't want to hang
532                 # the harness if test_shell is buggy, so we wait a couple
533                 # seconds to give test_shell a chance to clean up, but then
534                 # force-kill the process if necessary.
535                 KILL_TIMEOUT = 3.0
536                 timeout = time.time() + KILL_TIMEOUT
537                 # poll() is not threadsafe and can throw OSError due to:
538                 # http://bugs.python.org/issue1731717
539                 while self._proc.poll() is None and time.time() < timeout:
540                     time.sleep(0.1)
541                 # poll() is not threadsafe and can throw OSError due to:
542                 # http://bugs.python.org/issue1731717
543                 if self._proc.poll() is None:
544                     _log.warning('stopping test driver timed out, '
545                                  'killing it')
546                     self._port._executive.kill_process(self._proc.pid)