1#!/usr/bin/env python 2# Copyright (C) 2010 Google Inc. All rights reserved. 3# Copyright (C) 2010 Gabor Rapcsanyi <rgabor@inf.u-szeged.hu>, University of Szeged 4# 5# Redistribution and use in source and binary forms, with or without 6# modification, are permitted provided that the following conditions are 7# met: 8# 9# * Redistributions of source code must retain the above copyright 10# notice, this list of conditions and the following disclaimer. 11# * Redistributions in binary form must reproduce the above 12# copyright notice, this list of conditions and the following disclaimer 13# in the documentation and/or other materials provided with the 14# distribution. 15# * Neither the Google name nor the names of its 16# contributors may be used to endorse or promote products derived from 17# this software without specific prior written permission. 18# 19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 31"""WebKit implementations of the Port interface.""" 32 33import base64 34import logging 35import operator 36import os 37import re 38import signal 39import sys 40import time 41import webbrowser 42 43from webkitpy.common.system import ospath 44from webkitpy.layout_tests.port import base 45from webkitpy.layout_tests.port import server_process 46 47_log = logging.getLogger("webkitpy.layout_tests.port.webkit") 48 49 50class WebKitPort(base.Port): 51 """WebKit implementation of the Port class.""" 52 53 def __init__(self, **kwargs): 54 base.Port.__init__(self, **kwargs) 55 self._cached_apache_path = None 56 57 # FIXME: disable pixel tests until they are run by default on the 58 # build machines. 59 if not hasattr(self._options, "pixel_tests") or self._options.pixel_tests == None: 60 self._options.pixel_tests = False 61 62 def baseline_path(self): 63 return self._webkit_baseline_path(self._name) 64 65 def baseline_search_path(self): 66 return [self._webkit_baseline_path(self._name)] 67 68 def path_to_test_expectations_file(self): 69 return self._filesystem.join(self._webkit_baseline_path(self._name), 70 'test_expectations.txt') 71 72 def _build_driver(self): 73 configuration = self.get_option('configuration') 74 return self._config.build_dumprendertree(configuration) 75 76 def _check_driver(self): 77 driver_path = self._path_to_driver() 78 if not self._filesystem.exists(driver_path): 79 _log.error("DumpRenderTree was not found at %s" % driver_path) 80 return False 81 return True 82 83 def check_build(self, needs_http): 84 if self.get_option('build') and not self._build_driver(): 85 return False 86 if not self._check_driver(): 87 return False 88 if self.get_option('pixel_tests'): 89 if not self.check_image_diff(): 90 return False 91 if not self._check_port_build(): 92 return False 93 return True 94 95 def _check_port_build(self): 96 # Ports can override this method to do additional checks. 97 return True 98 99 def check_image_diff(self, override_step=None, logging=True): 100 image_diff_path = self._path_to_image_diff() 101 if not self._filesystem.exists(image_diff_path): 102 _log.error("ImageDiff was not found at %s" % image_diff_path) 103 return False 104 return True 105 106 def diff_image(self, expected_contents, actual_contents, 107 diff_filename=None): 108 """Return True if the two files are different. Also write a delta 109 image of the two images into |diff_filename| if it is not None.""" 110 111 # Handle the case where the test didn't actually generate an image. 112 # FIXME: need unit tests for this. 113 if not actual_contents and not expected_contents: 114 return False 115 if not actual_contents or not expected_contents: 116 return True 117 118 sp = self._diff_image_request(expected_contents, actual_contents) 119 return self._diff_image_reply(sp, diff_filename) 120 121 def _diff_image_request(self, expected_contents, actual_contents): 122 # FIXME: There needs to be a more sane way of handling default 123 # values for options so that you can distinguish between a default 124 # value of None and a default value that wasn't set. 125 if self.get_option('tolerance') is not None: 126 tolerance = self.get_option('tolerance') 127 else: 128 tolerance = 0.1 129 command = [self._path_to_image_diff(), '--tolerance', str(tolerance)] 130 sp = server_process.ServerProcess(self, 'ImageDiff', command) 131 132 sp.write('Content-Length: %d\n%sContent-Length: %d\n%s' % 133 (len(actual_contents), actual_contents, 134 len(expected_contents), expected_contents)) 135 136 return sp 137 138 def _diff_image_reply(self, sp, diff_filename): 139 timeout = 2.0 140 deadline = time.time() + timeout 141 output = sp.read_line(timeout) 142 while not sp.timed_out and not sp.crashed and output: 143 if output.startswith('Content-Length'): 144 m = re.match('Content-Length: (\d+)', output) 145 content_length = int(m.group(1)) 146 timeout = deadline - time.time() 147 output = sp.read(timeout, content_length) 148 break 149 elif output.startswith('diff'): 150 break 151 else: 152 timeout = deadline - time.time() 153 output = sp.read_line(deadline) 154 155 result = True 156 if output.startswith('diff'): 157 m = re.match('diff: (.+)% (passed|failed)', output) 158 if m.group(2) == 'passed': 159 result = False 160 elif output and diff_filename: 161 self._filesystem.write_binary_file(diff_filename, output) 162 elif sp.timed_out: 163 _log.error("ImageDiff timed out") 164 elif sp.crashed: 165 _log.error("ImageDiff crashed") 166 sp.stop() 167 return result 168 169 def default_results_directory(self): 170 # Results are store relative to the built products to make it easy 171 # to have multiple copies of webkit checked out and built. 172 return self._build_path('layout-test-results') 173 174 def setup_test_run(self): 175 # This port doesn't require any specific configuration. 176 pass 177 178 def create_driver(self, worker_number): 179 return WebKitDriver(self, worker_number) 180 181 def _tests_for_other_platforms(self): 182 # By default we will skip any directory under LayoutTests/platform 183 # that isn't in our baseline search path (this mirrors what 184 # old-run-webkit-tests does in findTestsToRun()). 185 # Note this returns LayoutTests/platform/*, not platform/*/*. 186 entries = self._filesystem.glob(self._webkit_baseline_path('*')) 187 dirs_to_skip = [] 188 for entry in entries: 189 if self._filesystem.isdir(entry) and not entry in self.baseline_search_path(): 190 basename = self._filesystem.basename(entry) 191 dirs_to_skip.append('platform/%s' % basename) 192 return dirs_to_skip 193 194 def _runtime_feature_list(self): 195 """Return the supported features of DRT. If a port doesn't support 196 this DRT switch, it has to override this method to return None""" 197 driver_path = self._path_to_driver() 198 feature_list = ' '.join(os.popen(driver_path + " --print-supported-features 2>&1").readlines()) 199 if "SupportedFeatures:" in feature_list: 200 return feature_list 201 return None 202 203 def _supported_symbol_list(self): 204 """Return the supported symbols of WebCore.""" 205 webcore_library_path = self._path_to_webcore_library() 206 if not webcore_library_path: 207 return None 208 symbol_list = ' '.join(os.popen("nm " + webcore_library_path).readlines()) 209 return symbol_list 210 211 def _directories_for_features(self): 212 """Return the supported feature dictionary. The keys are the 213 features and the values are the directories in lists.""" 214 directories_for_features = { 215 "Accelerated Compositing": ["compositing"], 216 "3D Rendering": ["animations/3d", "transforms/3d"], 217 } 218 return directories_for_features 219 220 def _directories_for_symbols(self): 221 """Return the supported feature dictionary. The keys are the 222 symbols and the values are the directories in lists.""" 223 directories_for_symbol = { 224 "MathMLElement": ["mathml"], 225 "GraphicsLayer": ["compositing"], 226 "WebCoreHas3DRendering": ["animations/3d", "transforms/3d"], 227 "WebGLShader": ["fast/canvas/webgl", "compositing/webgl", "http/tests/canvas/webgl"], 228 "WMLElement": ["http/tests/wml", "fast/wml", "wml"], 229 "parseWCSSInputProperty": ["fast/wcss"], 230 "isXHTMLMPDocument": ["fast/xhtmlmp"], 231 } 232 return directories_for_symbol 233 234 def _skipped_tests_for_unsupported_features(self): 235 """Return the directories of unsupported tests. Search for the 236 symbols in the symbol_list, if found add the corresponding 237 directories to the skipped directory list.""" 238 feature_list = self._runtime_feature_list() 239 directories = self._directories_for_features() 240 241 # if DRT feature detection not supported 242 if not feature_list: 243 feature_list = self._supported_symbol_list() 244 directories = self._directories_for_symbols() 245 246 if not feature_list: 247 return [] 248 249 skipped_directories = [directories[feature] 250 for feature in directories.keys() 251 if feature not in feature_list] 252 return reduce(operator.add, skipped_directories) 253 254 def _tests_for_disabled_features(self): 255 # FIXME: This should use the feature detection from 256 # webkitperl/features.pm to match run-webkit-tests. 257 # For now we hard-code a list of features known to be disabled on 258 # the Mac platform. 259 disabled_feature_tests = [ 260 "fast/xhtmlmp", 261 "http/tests/wml", 262 "mathml", 263 "wml", 264 ] 265 # FIXME: webarchive tests expect to read-write from 266 # -expected.webarchive files instead of .txt files. 267 # This script doesn't know how to do that yet, so pretend they're 268 # just "disabled". 269 webarchive_tests = [ 270 "webarchive", 271 "svg/webarchive", 272 "http/tests/webarchive", 273 "svg/custom/image-with-prefix-in-webarchive.svg", 274 ] 275 unsupported_feature_tests = self._skipped_tests_for_unsupported_features() 276 return disabled_feature_tests + webarchive_tests + unsupported_feature_tests 277 278 def _tests_from_skipped_file_contents(self, skipped_file_contents): 279 tests_to_skip = [] 280 for line in skipped_file_contents.split('\n'): 281 line = line.strip() 282 if line.startswith('#') or not len(line): 283 continue 284 tests_to_skip.append(line) 285 return tests_to_skip 286 287 def _skipped_file_paths(self): 288 return [self._filesystem.join(self._webkit_baseline_path(self._name), 'Skipped')] 289 290 def _expectations_from_skipped_files(self): 291 tests_to_skip = [] 292 for filename in self._skipped_file_paths(): 293 if not self._filesystem.exists(filename): 294 _log.warn("Failed to open Skipped file: %s" % filename) 295 continue 296 skipped_file_contents = self._filesystem.read_text_file(filename) 297 tests_to_skip.extend(self._tests_from_skipped_file_contents(skipped_file_contents)) 298 return tests_to_skip 299 300 def test_expectations(self): 301 # The WebKit mac port uses a combination of a test_expectations file 302 # and 'Skipped' files. 303 expectations_path = self.path_to_test_expectations_file() 304 return self._filesystem.read_text_file(expectations_path) + self._skips() 305 306 def _skips(self): 307 # Each Skipped file contains a list of files 308 # or directories to be skipped during the test run. The total list 309 # of tests to skipped is given by the contents of the generic 310 # Skipped file found in platform/X plus a version-specific file 311 # found in platform/X-version. Duplicate entries are allowed. 312 # This routine reads those files and turns contents into the 313 # format expected by test_expectations. 314 315 tests_to_skip = self.skipped_layout_tests() 316 skip_lines = map(lambda test_path: "BUG_SKIPPED SKIP : %s = FAIL" % 317 test_path, tests_to_skip) 318 return "\n".join(skip_lines) 319 320 def skipped_layout_tests(self): 321 # Use a set to allow duplicates 322 tests_to_skip = set(self._expectations_from_skipped_files()) 323 tests_to_skip.update(self._tests_for_other_platforms()) 324 tests_to_skip.update(self._tests_for_disabled_features()) 325 return tests_to_skip 326 327 def _build_path(self, *comps): 328 return self._filesystem.join(self._config.build_directory( 329 self.get_option('configuration')), *comps) 330 331 def _path_to_driver(self): 332 return self._build_path('DumpRenderTree') 333 334 def _path_to_webcore_library(self): 335 return None 336 337 def _path_to_helper(self): 338 return None 339 340 def _path_to_image_diff(self): 341 return self._build_path('ImageDiff') 342 343 def _path_to_wdiff(self): 344 # FIXME: This does not exist on a default Mac OS X Leopard install. 345 return 'wdiff' 346 347 def _path_to_apache(self): 348 if not self._cached_apache_path: 349 # The Apache binary path can vary depending on OS and distribution 350 # See http://wiki.apache.org/httpd/DistrosDefaultLayout 351 for path in ["/usr/sbin/httpd", "/usr/sbin/apache2"]: 352 if self._filesystem.exists(path): 353 self._cached_apache_path = path 354 break 355 356 if not self._cached_apache_path: 357 _log.error("Could not find apache. Not installed or unknown path.") 358 359 return self._cached_apache_path 360 361 362class WebKitDriver(base.Driver): 363 """WebKit implementation of the DumpRenderTree interface.""" 364 365 def __init__(self, port, worker_number): 366 self._worker_number = worker_number 367 self._port = port 368 self._driver_tempdir = port._filesystem.mkdtemp(prefix='DumpRenderTree-') 369 370 def __del__(self): 371 self._port._filesystem.rmtree(str(self._driver_tempdir)) 372 373 def cmd_line(self): 374 cmd = self._command_wrapper(self._port.get_option('wrapper')) 375 cmd.append(self._port._path_to_driver()) 376 if self._port.get_option('pixel_tests'): 377 cmd.append('--pixel-tests') 378 cmd.extend(self._port.get_option('additional_drt_flag', [])) 379 cmd.append('-') 380 return cmd 381 382 def start(self): 383 environment = self._port.setup_environ_for_server() 384 environment['DYLD_FRAMEWORK_PATH'] = self._port._build_path() 385 environment['DUMPRENDERTREE_TEMP'] = str(self._driver_tempdir) 386 self._server_process = server_process.ServerProcess(self._port, 387 "DumpRenderTree", self.cmd_line(), environment) 388 389 def poll(self): 390 return self._server_process.poll() 391 392 def restart(self): 393 self._server_process.stop() 394 self._server_process.start() 395 return 396 397 # FIXME: This function is huge. 398 def run_test(self, driver_input): 399 uri = self._port.filename_to_uri(driver_input.filename) 400 if uri.startswith("file:///"): 401 command = uri[7:] 402 else: 403 command = uri 404 405 if driver_input.image_hash: 406 command += "'" + driver_input.image_hash 407 command += "\n" 408 409 start_time = time.time() 410 self._server_process.write(command) 411 412 text = None 413 image = None 414 actual_image_hash = None 415 audio = None 416 deadline = time.time() + int(driver_input.timeout) / 1000.0 417 418 # First block is either text or audio 419 block = self._read_block(deadline) 420 if block.content_type == 'audio/wav': 421 audio = block.decoded_content 422 else: 423 text = block.decoded_content 424 425 # Now read an optional second block of image data 426 block = self._read_block(deadline) 427 if block.content and block.content_type == 'image/png': 428 image = block.decoded_content 429 actual_image_hash = block.content_hash 430 431 error_lines = self._server_process.error.splitlines() 432 # FIXME: This is a hack. It is unclear why sometimes 433 # we do not get any error lines from the server_process 434 # probably we are not flushing stderr. 435 if error_lines and error_lines[-1] == "#EOF": 436 error_lines.pop() # Remove the expected "#EOF" 437 error = "\n".join(error_lines) 438 # FIXME: This seems like the wrong section of code to be doing 439 # this reset in. 440 self._server_process.error = "" 441 return base.DriverOutput(text, image, actual_image_hash, audio, 442 crash=self._server_process.crashed, test_time=time.time() - start_time, 443 timeout=self._server_process.timed_out, error=error) 444 445 def _read_block(self, deadline): 446 LENGTH_HEADER = 'Content-Length: ' 447 HASH_HEADER = 'ActualHash: ' 448 TYPE_HEADER = 'Content-Type: ' 449 ENCODING_HEADER = 'Content-Transfer-Encoding: ' 450 content_type = None 451 encoding = None 452 content_hash = None 453 content_length = None 454 455 # Content is treated as binary data even though the text output 456 # is usually UTF-8. 457 content = '' 458 timeout = deadline - time.time() 459 line = self._server_process.read_line(timeout) 460 while (not self._server_process.timed_out 461 and not self._server_process.crashed 462 and line.rstrip() != "#EOF"): 463 if line.startswith(TYPE_HEADER) and content_type is None: 464 content_type = line.split()[1] 465 elif line.startswith(ENCODING_HEADER) and encoding is None: 466 encoding = line.split()[1] 467 elif line.startswith(LENGTH_HEADER) and content_length is None: 468 timeout = deadline - time.time() 469 content_length = int(line[len(LENGTH_HEADER):]) 470 # FIXME: Technically there should probably be another blank 471 # line here, but DRT doesn't write one. 472 content = self._server_process.read(timeout, content_length) 473 elif line.startswith(HASH_HEADER): 474 content_hash = line.split()[1] 475 else: 476 content += line 477 line = self._server_process.read_line(timeout) 478 timeout = deadline - time.time() 479 return ContentBlock(content_type, encoding, content_hash, content) 480 481 def stop(self): 482 if self._server_process: 483 self._server_process.stop() 484 self._server_process = None 485 486 487class ContentBlock(object): 488 def __init__(self, content_type, encoding, content_hash, content): 489 self.content_type = content_type 490 self.encoding = encoding 491 self.content_hash = content_hash 492 self.content = content 493 if self.encoding == 'base64': 494 self.decoded_content = base64.b64decode(content) 495 else: 496 self.decoded_content = content 497