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