1# Copyright (C) 2010 Google Inc. All rights reserved. 2# 3# Redistribution and use in source and binary forms, with or without 4# modification, are permitted provided that the following conditions are 5# met: 6# 7# * Redistributions of source code must retain the above copyright 8# notice, this list of conditions and the following disclaimer. 9# * Redistributions in binary form must reproduce the above 10# copyright notice, this list of conditions and the following disclaimer 11# in the documentation and/or other materials provided with the 12# distribution. 13# * Neither the Google name nor the names of its 14# contributors may be used to endorse or promote products derived from 15# this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29"""Abstract base class of Port-specific entry points for the layout tests 30test infrastructure (the Port and Driver classes).""" 31 32import cgi 33import difflib 34import errno 35import itertools 36import logging 37import os 38import operator 39import optparse 40import re 41import sys 42 43try: 44 from collections import OrderedDict 45except ImportError: 46 # Needed for Python < 2.7 47 from webkitpy.thirdparty.ordered_dict import OrderedDict 48 49 50from webkitpy.common import find_files 51from webkitpy.common import read_checksum_from_png 52from webkitpy.common.memoized import memoized 53from webkitpy.common.system import path 54from webkitpy.common.system.executive import ScriptError 55from webkitpy.common.system.path import cygpath 56from webkitpy.common.system.systemhost import SystemHost 57from webkitpy.common.webkit_finder import WebKitFinder 58from webkitpy.layout_tests.layout_package.bot_test_expectations import BotTestExpectationsFactory 59from webkitpy.layout_tests.models import test_run_results 60from webkitpy.layout_tests.models.test_configuration import TestConfiguration 61from webkitpy.layout_tests.port import config as port_config 62from webkitpy.layout_tests.port import driver 63from webkitpy.layout_tests.port import server_process 64from webkitpy.layout_tests.port.factory import PortFactory 65from webkitpy.layout_tests.servers import apache_http_server 66from webkitpy.layout_tests.servers import http_server 67from webkitpy.layout_tests.servers import websocket_server 68 69_log = logging.getLogger(__name__) 70 71 72# FIXME: This class should merge with WebKitPort now that Chromium behaves mostly like other webkit ports. 73class Port(object): 74 """Abstract class for Port-specific hooks for the layout_test package.""" 75 76 # Subclasses override this. This should indicate the basic implementation 77 # part of the port name, e.g., 'mac', 'win', 'gtk'; there is probably (?) 78 # one unique value per class. 79 80 # FIXME: We should probably rename this to something like 'implementation_name'. 81 port_name = None 82 83 # Test names resemble unix relative paths, and use '/' as a directory separator. 84 TEST_PATH_SEPARATOR = '/' 85 86 ALL_BUILD_TYPES = ('debug', 'release') 87 88 CONTENT_SHELL_NAME = 'content_shell' 89 90 # True if the port as aac and mp3 codecs built in. 91 PORT_HAS_AUDIO_CODECS_BUILT_IN = False 92 93 ALL_SYSTEMS = ( 94 ('snowleopard', 'x86'), 95 ('lion', 'x86'), 96 97 # FIXME: We treat Retina (High-DPI) devices as if they are running 98 # a different operating system version. This isn't accurate, but will work until 99 # we need to test and support baselines across multiple O/S versions. 100 ('retina', 'x86'), 101 102 ('mountainlion', 'x86'), 103 ('mavericks', 'x86'), 104 ('xp', 'x86'), 105 ('win7', 'x86'), 106 ('lucid', 'x86'), 107 ('lucid', 'x86_64'), 108 # FIXME: Technically this should be 'arm', but adding a third architecture type breaks TestConfigurationConverter. 109 # If we need this to be 'arm' in the future, then we first have to fix TestConfigurationConverter. 110 ('icecreamsandwich', 'x86'), 111 ) 112 113 ALL_BASELINE_VARIANTS = [ 114 'mac-mavericks', 'mac-mountainlion', 'mac-retina', 'mac-lion', 'mac-snowleopard', 115 'win-win7', 'win-xp', 116 'linux-x86_64', 'linux-x86', 117 ] 118 119 CONFIGURATION_SPECIFIER_MACROS = { 120 'mac': ['snowleopard', 'lion', 'retina', 'mountainlion', 'mavericks'], 121 'win': ['xp', 'win7'], 122 'linux': ['lucid'], 123 'android': ['icecreamsandwich'], 124 } 125 126 DEFAULT_BUILD_DIRECTORIES = ('out',) 127 128 # overridden in subclasses. 129 FALLBACK_PATHS = {} 130 131 SUPPORTED_VERSIONS = [] 132 133 @classmethod 134 def latest_platform_fallback_path(cls): 135 return cls.FALLBACK_PATHS[cls.SUPPORTED_VERSIONS[-1]] 136 137 @classmethod 138 def _static_build_path(cls, filesystem, build_directory, chromium_base, configuration, comps): 139 if build_directory: 140 return filesystem.join(build_directory, configuration, *comps) 141 142 hits = [] 143 for directory in cls.DEFAULT_BUILD_DIRECTORIES: 144 base_dir = filesystem.join(chromium_base, directory, configuration) 145 path = filesystem.join(base_dir, *comps) 146 if filesystem.exists(path): 147 hits.append((filesystem.mtime(path), path)) 148 149 if hits: 150 hits.sort(reverse=True) 151 return hits[0][1] # Return the newest file found. 152 153 # We have to default to something, so pick the last one. 154 return filesystem.join(base_dir, *comps) 155 156 @classmethod 157 def determine_full_port_name(cls, host, options, port_name): 158 """Return a fully-specified port name that can be used to construct objects.""" 159 # Subclasses will usually override this. 160 assert port_name.startswith(cls.port_name) 161 return port_name 162 163 def __init__(self, host, port_name, options=None, **kwargs): 164 165 # This value may be different from cls.port_name by having version modifiers 166 # and other fields appended to it (for example, 'qt-arm' or 'mac-wk2'). 167 self._name = port_name 168 169 # These are default values that should be overridden in a subclasses. 170 self._version = '' 171 self._architecture = 'x86' 172 173 # FIXME: Ideally we'd have a package-wide way to get a 174 # well-formed options object that had all of the necessary 175 # options defined on it. 176 self._options = options or optparse.Values() 177 178 self.host = host 179 self._executive = host.executive 180 self._filesystem = host.filesystem 181 self._webkit_finder = WebKitFinder(host.filesystem) 182 self._config = port_config.Config(self._executive, self._filesystem, self.port_name) 183 184 self._helper = None 185 self._http_server = None 186 self._websocket_server = None 187 self._image_differ = None 188 self._server_process_constructor = server_process.ServerProcess # overridable for testing 189 self._http_lock = None # FIXME: Why does this live on the port object? 190 self._dump_reader = None 191 192 # Python's Popen has a bug that causes any pipes opened to a 193 # process that can't be executed to be leaked. Since this 194 # code is specifically designed to tolerate exec failures 195 # to gracefully handle cases where wdiff is not installed, 196 # the bug results in a massive file descriptor leak. As a 197 # workaround, if an exec failure is ever experienced for 198 # wdiff, assume it's not available. This will leak one 199 # file descriptor but that's better than leaking each time 200 # wdiff would be run. 201 # 202 # http://mail.python.org/pipermail/python-list/ 203 # 2008-August/505753.html 204 # http://bugs.python.org/issue3210 205 self._wdiff_available = None 206 207 # FIXME: prettypatch.py knows this path, why is it copied here? 208 self._pretty_patch_path = self.path_from_webkit_base("Tools", "Scripts", "webkitruby", "PrettyPatch", "prettify.rb") 209 self._pretty_patch_available = None 210 211 if not hasattr(options, 'configuration') or not options.configuration: 212 self.set_option_default('configuration', self.default_configuration()) 213 self._test_configuration = None 214 self._reftest_list = {} 215 self._results_directory = None 216 217 def buildbot_archives_baselines(self): 218 return True 219 220 def additional_drt_flag(self): 221 if self.driver_name() == self.CONTENT_SHELL_NAME: 222 return ['--dump-render-tree'] 223 return [] 224 225 def supports_per_test_timeout(self): 226 return False 227 228 def default_pixel_tests(self): 229 return True 230 231 def default_smoke_test_only(self): 232 return False 233 234 def default_timeout_ms(self): 235 timeout_ms = 6 * 1000 236 if self.get_option('configuration') == 'Debug': 237 # Debug is usually 2x-3x slower than Release. 238 return 3 * timeout_ms 239 return timeout_ms 240 241 def driver_stop_timeout(self): 242 """ Returns the amount of time in seconds to wait before killing the process in driver.stop().""" 243 # We want to wait for at least 3 seconds, but if we are really slow, we want to be slow on cleanup as 244 # well (for things like ASAN, Valgrind, etc.) 245 return 3.0 * float(self.get_option('time_out_ms', '0')) / self.default_timeout_ms() 246 247 def wdiff_available(self): 248 if self._wdiff_available is None: 249 self._wdiff_available = self.check_wdiff(logging=False) 250 return self._wdiff_available 251 252 def pretty_patch_available(self): 253 if self._pretty_patch_available is None: 254 self._pretty_patch_available = self.check_pretty_patch(logging=False) 255 return self._pretty_patch_available 256 257 def default_child_processes(self): 258 """Return the number of drivers to use for this port.""" 259 return self._executive.cpu_count() 260 261 def default_max_locked_shards(self): 262 """Return the number of "locked" shards to run in parallel (like the http tests).""" 263 max_locked_shards = int(self.default_child_processes()) / 4 264 if not max_locked_shards: 265 return 1 266 return max_locked_shards 267 268 def baseline_path(self): 269 """Return the absolute path to the directory to store new baselines in for this port.""" 270 # FIXME: remove once all callers are calling either baseline_version_dir() or baseline_platform_dir() 271 return self.baseline_version_dir() 272 273 def baseline_platform_dir(self): 274 """Return the absolute path to the default (version-independent) platform-specific results.""" 275 return self._filesystem.join(self.layout_tests_dir(), 'platform', self.port_name) 276 277 def baseline_version_dir(self): 278 """Return the absolute path to the platform-and-version-specific results.""" 279 baseline_search_paths = self.baseline_search_path() 280 return baseline_search_paths[0] 281 282 def virtual_baseline_search_path(self, test_name): 283 suite = self.lookup_virtual_suite(test_name) 284 if not suite: 285 return None 286 return [self._filesystem.join(path, suite.name) for path in self.default_baseline_search_path()] 287 288 def baseline_search_path(self): 289 return self.get_option('additional_platform_directory', []) + self._compare_baseline() + self.default_baseline_search_path() 290 291 def default_baseline_search_path(self): 292 """Return a list of absolute paths to directories to search under for 293 baselines. The directories are searched in order.""" 294 return map(self._webkit_baseline_path, self.FALLBACK_PATHS[self.version()]) 295 296 @memoized 297 def _compare_baseline(self): 298 factory = PortFactory(self.host) 299 target_port = self.get_option('compare_port') 300 if target_port: 301 return factory.get(target_port).default_baseline_search_path() 302 return [] 303 304 def _check_file_exists(self, path_to_file, file_description, 305 override_step=None, logging=True): 306 """Verify the file is present where expected or log an error. 307 308 Args: 309 file_name: The (human friendly) name or description of the file 310 you're looking for (e.g., "HTTP Server"). Used for error logging. 311 override_step: An optional string to be logged if the check fails. 312 logging: Whether or not log the error messages.""" 313 if not self._filesystem.exists(path_to_file): 314 if logging: 315 _log.error('Unable to find %s' % file_description) 316 _log.error(' at %s' % path_to_file) 317 if override_step: 318 _log.error(' %s' % override_step) 319 _log.error('') 320 return False 321 return True 322 323 def check_build(self, needs_http, printer): 324 result = True 325 326 dump_render_tree_binary_path = self._path_to_driver() 327 result = self._check_file_exists(dump_render_tree_binary_path, 328 'test driver') and result 329 if not result and self.get_option('build'): 330 result = self._check_driver_build_up_to_date( 331 self.get_option('configuration')) 332 else: 333 _log.error('') 334 335 helper_path = self._path_to_helper() 336 if helper_path: 337 result = self._check_file_exists(helper_path, 338 'layout test helper') and result 339 340 if self.get_option('pixel_tests'): 341 result = self.check_image_diff( 342 'To override, invoke with --no-pixel-tests') and result 343 344 # It's okay if pretty patch and wdiff aren't available, but we will at least log messages. 345 self._pretty_patch_available = self.check_pretty_patch() 346 self._wdiff_available = self.check_wdiff() 347 348 if self._dump_reader: 349 result = self._dump_reader.check_is_functional() and result 350 351 return test_run_results.OK_EXIT_STATUS if result else test_run_results.UNEXPECTED_ERROR_EXIT_STATUS 352 353 def _check_driver(self): 354 driver_path = self._path_to_driver() 355 if not self._filesystem.exists(driver_path): 356 _log.error("%s was not found at %s" % (self.driver_name(), driver_path)) 357 return False 358 return True 359 360 def _check_port_build(self): 361 # Ports can override this method to do additional checks. 362 return True 363 364 def check_sys_deps(self, needs_http): 365 """If the port needs to do some runtime checks to ensure that the 366 tests can be run successfully, it should override this routine. 367 This step can be skipped with --nocheck-sys-deps. 368 369 Returns whether the system is properly configured.""" 370 cmd = [self._path_to_driver(), '--check-layout-test-sys-deps'] 371 372 local_error = ScriptError() 373 374 def error_handler(script_error): 375 local_error.exit_code = script_error.exit_code 376 377 output = self._executive.run_command(cmd, error_handler=error_handler) 378 if local_error.exit_code: 379 _log.error('System dependencies check failed.') 380 _log.error('To override, invoke with --nocheck-sys-deps') 381 _log.error('') 382 _log.error(output) 383 return test_run_results.SYS_DEPS_EXIT_STATUS 384 return test_run_results.OK_EXIT_STATUS 385 386 def check_image_diff(self, override_step=None, logging=True): 387 """This routine is used to check whether image_diff binary exists.""" 388 image_diff_path = self._path_to_image_diff() 389 if not self._filesystem.exists(image_diff_path): 390 _log.error("image_diff was not found at %s" % image_diff_path) 391 return False 392 return True 393 394 def check_pretty_patch(self, logging=True): 395 """Checks whether we can use the PrettyPatch ruby script.""" 396 try: 397 _ = self._executive.run_command(['ruby', '--version']) 398 except OSError, e: 399 if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]: 400 if logging: 401 _log.warning("Ruby is not installed; can't generate pretty patches.") 402 _log.warning('') 403 return False 404 405 if not self._filesystem.exists(self._pretty_patch_path): 406 if logging: 407 _log.warning("Unable to find %s; can't generate pretty patches." % self._pretty_patch_path) 408 _log.warning('') 409 return False 410 411 return True 412 413 def check_wdiff(self, logging=True): 414 if not self._path_to_wdiff(): 415 # Don't need to log here since this is the port choosing not to use wdiff. 416 return False 417 418 try: 419 _ = self._executive.run_command([self._path_to_wdiff(), '--help']) 420 except OSError: 421 if logging: 422 message = self._wdiff_missing_message() 423 if message: 424 for line in message.splitlines(): 425 _log.warning(' ' + line) 426 _log.warning('') 427 return False 428 429 return True 430 431 def _wdiff_missing_message(self): 432 return 'wdiff is not installed; please install it to generate word-by-word diffs.' 433 434 def check_httpd(self): 435 if self._uses_apache(): 436 httpd_path = self._path_to_apache() 437 else: 438 httpd_path = self._path_to_lighttpd() 439 440 try: 441 server_name = self._filesystem.basename(httpd_path) 442 env = self.setup_environ_for_server(server_name) 443 if self._executive.run_command([httpd_path, "-v"], env=env, return_exit_code=True) != 0: 444 _log.error("httpd seems broken. Cannot run http tests.") 445 return False 446 return True 447 except OSError: 448 _log.error("No httpd found. Cannot run http tests.") 449 return False 450 451 def do_text_results_differ(self, expected_text, actual_text): 452 return expected_text != actual_text 453 454 def do_audio_results_differ(self, expected_audio, actual_audio): 455 return expected_audio != actual_audio 456 457 def diff_image(self, expected_contents, actual_contents): 458 """Compare two images and return a tuple of an image diff, and an error string. 459 460 If an error occurs (like image_diff isn't found, or crashes, we log an error and return True (for a diff). 461 """ 462 # If only one of them exists, return that one. 463 if not actual_contents and not expected_contents: 464 return (None, None) 465 if not actual_contents: 466 return (expected_contents, None) 467 if not expected_contents: 468 return (actual_contents, None) 469 470 tempdir = self._filesystem.mkdtemp() 471 472 expected_filename = self._filesystem.join(str(tempdir), "expected.png") 473 self._filesystem.write_binary_file(expected_filename, expected_contents) 474 475 actual_filename = self._filesystem.join(str(tempdir), "actual.png") 476 self._filesystem.write_binary_file(actual_filename, actual_contents) 477 478 diff_filename = self._filesystem.join(str(tempdir), "diff.png") 479 480 # image_diff needs native win paths as arguments, so we need to convert them if running under cygwin. 481 native_expected_filename = self._convert_path(expected_filename) 482 native_actual_filename = self._convert_path(actual_filename) 483 native_diff_filename = self._convert_path(diff_filename) 484 485 executable = self._path_to_image_diff() 486 # Note that although we are handed 'old', 'new', image_diff wants 'new', 'old'. 487 comand = [executable, '--diff', native_actual_filename, native_expected_filename, native_diff_filename] 488 489 result = None 490 err_str = None 491 try: 492 exit_code = self._executive.run_command(comand, return_exit_code=True) 493 if exit_code == 0: 494 # The images are the same. 495 result = None 496 elif exit_code == 1: 497 result = self._filesystem.read_binary_file(native_diff_filename) 498 else: 499 err_str = "image diff returned an exit code of %s" % exit_code 500 except OSError, e: 501 err_str = 'error running image diff: %s' % str(e) 502 finally: 503 self._filesystem.rmtree(str(tempdir)) 504 505 return (result, err_str or None) 506 507 def diff_text(self, expected_text, actual_text, expected_filename, actual_filename): 508 """Returns a string containing the diff of the two text strings 509 in 'unified diff' format.""" 510 511 # The filenames show up in the diff output, make sure they're 512 # raw bytes and not unicode, so that they don't trigger join() 513 # trying to decode the input. 514 def to_raw_bytes(string_value): 515 if isinstance(string_value, unicode): 516 return string_value.encode('utf-8') 517 return string_value 518 expected_filename = to_raw_bytes(expected_filename) 519 actual_filename = to_raw_bytes(actual_filename) 520 diff = difflib.unified_diff(expected_text.splitlines(True), 521 actual_text.splitlines(True), 522 expected_filename, 523 actual_filename) 524 return ''.join(diff) 525 526 def driver_name(self): 527 if self.get_option('driver_name'): 528 return self.get_option('driver_name') 529 return self.CONTENT_SHELL_NAME 530 531 def expected_baselines_by_extension(self, test_name): 532 """Returns a dict mapping baseline suffix to relative path for each baseline in 533 a test. For reftests, it returns ".==" or ".!=" instead of the suffix.""" 534 # FIXME: The name similarity between this and expected_baselines() below, is unfortunate. 535 # We should probably rename them both. 536 baseline_dict = {} 537 reference_files = self.reference_files(test_name) 538 if reference_files: 539 # FIXME: How should this handle more than one type of reftest? 540 baseline_dict['.' + reference_files[0][0]] = self.relative_test_filename(reference_files[0][1]) 541 542 for extension in self.baseline_extensions(): 543 path = self.expected_filename(test_name, extension, return_default=False) 544 baseline_dict[extension] = self.relative_test_filename(path) if path else path 545 546 return baseline_dict 547 548 def baseline_extensions(self): 549 """Returns a tuple of all of the non-reftest baseline extensions we use. The extensions include the leading '.'.""" 550 return ('.wav', '.txt', '.png') 551 552 def expected_baselines(self, test_name, suffix, all_baselines=False): 553 """Given a test name, finds where the baseline results are located. 554 555 Args: 556 test_name: name of test file (usually a relative path under LayoutTests/) 557 suffix: file suffix of the expected results, including dot; e.g. 558 '.txt' or '.png'. This should not be None, but may be an empty 559 string. 560 all_baselines: If True, return an ordered list of all baseline paths 561 for the given platform. If False, return only the first one. 562 Returns 563 a list of ( platform_dir, results_filename ), where 564 platform_dir - abs path to the top of the results tree (or test 565 tree) 566 results_filename - relative path from top of tree to the results 567 file 568 (port.join() of the two gives you the full path to the file, 569 unless None was returned.) 570 Return values will be in the format appropriate for the current 571 platform (e.g., "\\" for path separators on Windows). If the results 572 file is not found, then None will be returned for the directory, 573 but the expected relative pathname will still be returned. 574 575 This routine is generic but lives here since it is used in 576 conjunction with the other baseline and filename routines that are 577 platform specific. 578 """ 579 baseline_filename = self._filesystem.splitext(test_name)[0] + '-expected' + suffix 580 baseline_search_path = self.baseline_search_path() 581 582 baselines = [] 583 for platform_dir in baseline_search_path: 584 if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)): 585 baselines.append((platform_dir, baseline_filename)) 586 587 if not all_baselines and baselines: 588 return baselines 589 590 # If it wasn't found in a platform directory, return the expected 591 # result in the test directory, even if no such file actually exists. 592 platform_dir = self.layout_tests_dir() 593 if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)): 594 baselines.append((platform_dir, baseline_filename)) 595 596 if baselines: 597 return baselines 598 599 return [(None, baseline_filename)] 600 601 def expected_filename(self, test_name, suffix, return_default=True): 602 """Given a test name, returns an absolute path to its expected results. 603 604 If no expected results are found in any of the searched directories, 605 the directory in which the test itself is located will be returned. 606 The return value is in the format appropriate for the platform 607 (e.g., "\\" for path separators on windows). 608 609 Args: 610 test_name: name of test file (usually a relative path under LayoutTests/) 611 suffix: file suffix of the expected results, including dot; e.g. '.txt' 612 or '.png'. This should not be None, but may be an empty string. 613 platform: the most-specific directory name to use to build the 614 search list of directories, e.g., 'win', or 615 'chromium-cg-mac-leopard' (we follow the WebKit format) 616 return_default: if True, returns the path to the generic expectation if nothing 617 else is found; if False, returns None. 618 619 This routine is generic but is implemented here to live alongside 620 the other baseline and filename manipulation routines. 621 """ 622 # FIXME: The [0] here is very mysterious, as is the destructured return. 623 platform_dir, baseline_filename = self.expected_baselines(test_name, suffix)[0] 624 if platform_dir: 625 return self._filesystem.join(platform_dir, baseline_filename) 626 627 actual_test_name = self.lookup_virtual_test_base(test_name) 628 if actual_test_name: 629 return self.expected_filename(actual_test_name, suffix) 630 631 if return_default: 632 return self._filesystem.join(self.layout_tests_dir(), baseline_filename) 633 return None 634 635 def expected_checksum(self, test_name): 636 """Returns the checksum of the image we expect the test to produce, or None if it is a text-only test.""" 637 png_path = self.expected_filename(test_name, '.png') 638 639 if self._filesystem.exists(png_path): 640 with self._filesystem.open_binary_file_for_reading(png_path) as filehandle: 641 return read_checksum_from_png.read_checksum(filehandle) 642 643 return None 644 645 def expected_image(self, test_name): 646 """Returns the image we expect the test to produce.""" 647 baseline_path = self.expected_filename(test_name, '.png') 648 if not self._filesystem.exists(baseline_path): 649 return None 650 return self._filesystem.read_binary_file(baseline_path) 651 652 def expected_audio(self, test_name): 653 baseline_path = self.expected_filename(test_name, '.wav') 654 if not self._filesystem.exists(baseline_path): 655 return None 656 return self._filesystem.read_binary_file(baseline_path) 657 658 def expected_text(self, test_name): 659 """Returns the text output we expect the test to produce, or None 660 if we don't expect there to be any text output. 661 End-of-line characters are normalized to '\n'.""" 662 # FIXME: DRT output is actually utf-8, but since we don't decode the 663 # output from DRT (instead treating it as a binary string), we read the 664 # baselines as a binary string, too. 665 baseline_path = self.expected_filename(test_name, '.txt') 666 if not self._filesystem.exists(baseline_path): 667 return None 668 text = self._filesystem.read_binary_file(baseline_path) 669 return text.replace("\r\n", "\n") 670 671 def _get_reftest_list(self, test_name): 672 dirname = self._filesystem.join(self.layout_tests_dir(), self._filesystem.dirname(test_name)) 673 if dirname not in self._reftest_list: 674 self._reftest_list[dirname] = Port._parse_reftest_list(self._filesystem, dirname) 675 return self._reftest_list[dirname] 676 677 @staticmethod 678 def _parse_reftest_list(filesystem, test_dirpath): 679 reftest_list_path = filesystem.join(test_dirpath, 'reftest.list') 680 if not filesystem.isfile(reftest_list_path): 681 return None 682 reftest_list_file = filesystem.read_text_file(reftest_list_path) 683 684 parsed_list = {} 685 for line in reftest_list_file.split('\n'): 686 line = re.sub('#.+$', '', line) 687 split_line = line.split() 688 if len(split_line) < 3: 689 continue 690 expectation_type, test_file, ref_file = split_line 691 parsed_list.setdefault(filesystem.join(test_dirpath, test_file), []).append((expectation_type, filesystem.join(test_dirpath, ref_file))) 692 return parsed_list 693 694 def reference_files(self, test_name): 695 """Return a list of expectation (== or !=) and filename pairs""" 696 697 reftest_list = self._get_reftest_list(test_name) 698 if not reftest_list: 699 reftest_list = [] 700 for expectation, prefix in (('==', ''), ('!=', '-mismatch')): 701 for extention in Port._supported_file_extensions: 702 path = self.expected_filename(test_name, prefix + extention) 703 if self._filesystem.exists(path): 704 reftest_list.append((expectation, path)) 705 return reftest_list 706 707 return reftest_list.get(self._filesystem.join(self.layout_tests_dir(), test_name), []) # pylint: disable=E1103 708 709 def tests(self, paths): 710 """Return the list of tests found matching paths.""" 711 tests = self._real_tests(paths) 712 tests.extend(self._virtual_tests(paths, self.populated_virtual_test_suites())) 713 return tests 714 715 def _real_tests(self, paths): 716 # When collecting test cases, skip these directories 717 skipped_directories = set(['.svn', '_svn', 'platform', 'resources', 'script-tests', 'reference', 'reftest']) 718 files = find_files.find(self._filesystem, self.layout_tests_dir(), paths, skipped_directories, Port.is_test_file, self.test_key) 719 return [self.relative_test_filename(f) for f in files] 720 721 # When collecting test cases, we include any file with these extensions. 722 _supported_file_extensions = set(['.html', '.xml', '.xhtml', '.xht', '.pl', 723 '.htm', '.php', '.svg', '.mht']) 724 725 @staticmethod 726 # If any changes are made here be sure to update the isUsedInReftest method in old-run-webkit-tests as well. 727 def is_reference_html_file(filesystem, dirname, filename): 728 if filename.startswith('ref-') or filename.startswith('notref-'): 729 return True 730 filename_wihout_ext, unused = filesystem.splitext(filename) 731 for suffix in ['-expected', '-expected-mismatch', '-ref', '-notref']: 732 if filename_wihout_ext.endswith(suffix): 733 return True 734 return False 735 736 @staticmethod 737 def _has_supported_extension(filesystem, filename): 738 """Return true if filename is one of the file extensions we want to run a test on.""" 739 extension = filesystem.splitext(filename)[1] 740 return extension in Port._supported_file_extensions 741 742 @staticmethod 743 def is_test_file(filesystem, dirname, filename): 744 return Port._has_supported_extension(filesystem, filename) and not Port.is_reference_html_file(filesystem, dirname, filename) 745 746 ALL_TEST_TYPES = ['audio', 'harness', 'pixel', 'ref', 'text', 'unknown'] 747 748 def test_type(self, test_name): 749 fs = self._filesystem 750 if fs.exists(self.expected_filename(test_name, '.png')): 751 return 'pixel' 752 if fs.exists(self.expected_filename(test_name, '.wav')): 753 return 'audio' 754 if self.reference_files(test_name): 755 return 'ref' 756 txt = self.expected_text(test_name) 757 if txt: 758 if 'layer at (0,0) size 800x600' in txt: 759 return 'pixel' 760 for line in txt.splitlines(): 761 if line.startswith('FAIL') or line.startswith('TIMEOUT') or line.startswith('PASS'): 762 return 'harness' 763 return 'text' 764 return 'unknown' 765 766 def test_key(self, test_name): 767 """Turns a test name into a list with two sublists, the natural key of the 768 dirname, and the natural key of the basename. 769 770 This can be used when sorting paths so that files in a directory. 771 directory are kept together rather than being mixed in with files in 772 subdirectories.""" 773 dirname, basename = self.split_test(test_name) 774 return (self._natural_sort_key(dirname + self.TEST_PATH_SEPARATOR), self._natural_sort_key(basename)) 775 776 def _natural_sort_key(self, string_to_split): 777 """ Turns a string into a list of string and number chunks, i.e. "z23a" -> ["z", 23, "a"] 778 779 This can be used to implement "natural sort" order. See: 780 http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html 781 http://nedbatchelder.com/blog/200712.html#e20071211T054956 782 """ 783 def tryint(val): 784 try: 785 return int(val) 786 except ValueError: 787 return val 788 789 return [tryint(chunk) for chunk in re.split('(\d+)', string_to_split)] 790 791 def test_dirs(self): 792 """Returns the list of top-level test directories.""" 793 layout_tests_dir = self.layout_tests_dir() 794 return filter(lambda x: self._filesystem.isdir(self._filesystem.join(layout_tests_dir, x)), 795 self._filesystem.listdir(layout_tests_dir)) 796 797 @memoized 798 def test_isfile(self, test_name): 799 """Return True if the test name refers to a directory of tests.""" 800 # Used by test_expectations.py to apply rules to whole directories. 801 if self._filesystem.isfile(self.abspath_for_test(test_name)): 802 return True 803 base = self.lookup_virtual_test_base(test_name) 804 return base and self._filesystem.isfile(self.abspath_for_test(base)) 805 806 @memoized 807 def test_isdir(self, test_name): 808 """Return True if the test name refers to a directory of tests.""" 809 # Used by test_expectations.py to apply rules to whole directories. 810 if self._filesystem.isdir(self.abspath_for_test(test_name)): 811 return True 812 base = self.lookup_virtual_test_base(test_name) 813 return base and self._filesystem.isdir(self.abspath_for_test(base)) 814 815 @memoized 816 def test_exists(self, test_name): 817 """Return True if the test name refers to an existing test or baseline.""" 818 # Used by test_expectations.py to determine if an entry refers to a 819 # valid test and by printing.py to determine if baselines exist. 820 return self.test_isfile(test_name) or self.test_isdir(test_name) 821 822 def split_test(self, test_name): 823 """Splits a test name into the 'directory' part and the 'basename' part.""" 824 index = test_name.rfind(self.TEST_PATH_SEPARATOR) 825 if index < 1: 826 return ('', test_name) 827 return (test_name[0:index], test_name[index:]) 828 829 def normalize_test_name(self, test_name): 830 """Returns a normalized version of the test name or test directory.""" 831 if test_name.endswith('/'): 832 return test_name 833 if self.test_isdir(test_name): 834 return test_name + '/' 835 return test_name 836 837 def driver_cmd_line(self): 838 """Prints the DRT command line that will be used.""" 839 driver = self.create_driver(0) 840 return driver.cmd_line(self.get_option('pixel_tests'), []) 841 842 def update_baseline(self, baseline_path, data): 843 """Updates the baseline for a test. 844 845 Args: 846 baseline_path: the actual path to use for baseline, not the path to 847 the test. This function is used to update either generic or 848 platform-specific baselines, but we can't infer which here. 849 data: contents of the baseline. 850 """ 851 self._filesystem.write_binary_file(baseline_path, data) 852 853 # FIXME: update callers to create a finder and call it instead of these next five routines (which should be protected). 854 def webkit_base(self): 855 return self._webkit_finder.webkit_base() 856 857 def path_from_webkit_base(self, *comps): 858 return self._webkit_finder.path_from_webkit_base(*comps) 859 860 def path_from_chromium_base(self, *comps): 861 return self._webkit_finder.path_from_chromium_base(*comps) 862 863 def path_to_script(self, script_name): 864 return self._webkit_finder.path_to_script(script_name) 865 866 def layout_tests_dir(self): 867 return self._webkit_finder.layout_tests_dir() 868 869 def perf_tests_dir(self): 870 return self._webkit_finder.perf_tests_dir() 871 872 def skipped_layout_tests(self, test_list): 873 """Returns tests skipped outside of the TestExpectations files.""" 874 return set(self._skipped_tests_for_unsupported_features(test_list)) 875 876 def _tests_from_skipped_file_contents(self, skipped_file_contents): 877 tests_to_skip = [] 878 for line in skipped_file_contents.split('\n'): 879 line = line.strip() 880 line = line.rstrip('/') # Best to normalize directory names to not include the trailing slash. 881 if line.startswith('#') or not len(line): 882 continue 883 tests_to_skip.append(line) 884 return tests_to_skip 885 886 def _expectations_from_skipped_files(self, skipped_file_paths): 887 tests_to_skip = [] 888 for search_path in skipped_file_paths: 889 filename = self._filesystem.join(self._webkit_baseline_path(search_path), "Skipped") 890 if not self._filesystem.exists(filename): 891 _log.debug("Skipped does not exist: %s" % filename) 892 continue 893 _log.debug("Using Skipped file: %s" % filename) 894 skipped_file_contents = self._filesystem.read_text_file(filename) 895 tests_to_skip.extend(self._tests_from_skipped_file_contents(skipped_file_contents)) 896 return tests_to_skip 897 898 @memoized 899 def skipped_perf_tests(self): 900 return self._expectations_from_skipped_files([self.perf_tests_dir()]) 901 902 def skips_perf_test(self, test_name): 903 for test_or_category in self.skipped_perf_tests(): 904 if test_or_category == test_name: 905 return True 906 category = self._filesystem.join(self.perf_tests_dir(), test_or_category) 907 if self._filesystem.isdir(category) and test_name.startswith(test_or_category): 908 return True 909 return False 910 911 def is_chromium(self): 912 return True 913 914 def name(self): 915 """Returns a name that uniquely identifies this particular type of port 916 (e.g., "mac-snowleopard" or "linux-x86_x64" and can be passed 917 to factory.get() to instantiate the port.""" 918 return self._name 919 920 def operating_system(self): 921 # Subclasses should override this default implementation. 922 return 'mac' 923 924 def version(self): 925 """Returns a string indicating the version of a given platform, e.g. 926 'leopard' or 'xp'. 927 928 This is used to help identify the exact port when parsing test 929 expectations, determining search paths, and logging information.""" 930 return self._version 931 932 def architecture(self): 933 return self._architecture 934 935 def get_option(self, name, default_value=None): 936 return getattr(self._options, name, default_value) 937 938 def set_option_default(self, name, default_value): 939 return self._options.ensure_value(name, default_value) 940 941 @memoized 942 def path_to_generic_test_expectations_file(self): 943 return self._filesystem.join(self.layout_tests_dir(), 'TestExpectations') 944 945 def relative_test_filename(self, filename): 946 """Returns a test_name a relative unix-style path for a filename under the LayoutTests 947 directory. Ports may legitimately return abspaths here if no relpath makes sense.""" 948 # Ports that run on windows need to override this method to deal with 949 # filenames with backslashes in them. 950 if filename.startswith(self.layout_tests_dir()): 951 return self.host.filesystem.relpath(filename, self.layout_tests_dir()) 952 else: 953 return self.host.filesystem.abspath(filename) 954 955 @memoized 956 def abspath_for_test(self, test_name): 957 """Returns the full path to the file for a given test name. This is the 958 inverse of relative_test_filename().""" 959 return self._filesystem.join(self.layout_tests_dir(), test_name) 960 961 def results_directory(self): 962 """Absolute path to the place to store the test results (uses --results-directory).""" 963 if not self._results_directory: 964 option_val = self.get_option('results_directory') or self.default_results_directory() 965 self._results_directory = self._filesystem.abspath(option_val) 966 return self._results_directory 967 968 def perf_results_directory(self): 969 return self._build_path() 970 971 def default_results_directory(self): 972 """Absolute path to the default place to store the test results.""" 973 try: 974 return self.path_from_chromium_base('webkit', self.get_option('configuration'), 'layout-test-results') 975 except AssertionError: 976 return self._build_path('layout-test-results') 977 978 def setup_test_run(self): 979 """Perform port-specific work at the beginning of a test run.""" 980 # Delete the disk cache if any to ensure a clean test run. 981 dump_render_tree_binary_path = self._path_to_driver() 982 cachedir = self._filesystem.dirname(dump_render_tree_binary_path) 983 cachedir = self._filesystem.join(cachedir, "cache") 984 if self._filesystem.exists(cachedir): 985 self._filesystem.rmtree(cachedir) 986 987 if self._dump_reader: 988 self._filesystem.maybe_make_directory(self._dump_reader.crash_dumps_directory()) 989 990 def num_workers(self, requested_num_workers): 991 """Returns the number of available workers (possibly less than the number requested).""" 992 return requested_num_workers 993 994 def clean_up_test_run(self): 995 """Perform port-specific work at the end of a test run.""" 996 if self._image_differ: 997 self._image_differ.stop() 998 self._image_differ = None 999 1000 # FIXME: os.environ access should be moved to onto a common/system class to be more easily mockable. 1001 def _value_or_default_from_environ(self, name, default=None): 1002 if name in os.environ: 1003 return os.environ[name] 1004 return default 1005 1006 def _copy_value_from_environ_if_set(self, clean_env, name): 1007 if name in os.environ: 1008 clean_env[name] = os.environ[name] 1009 1010 def setup_environ_for_server(self, server_name=None): 1011 # We intentionally copy only a subset of os.environ when 1012 # launching subprocesses to ensure consistent test results. 1013 clean_env = { 1014 'LOCAL_RESOURCE_ROOT': self.layout_tests_dir(), # FIXME: Is this used? 1015 } 1016 variables_to_copy = [ 1017 'WEBKIT_TESTFONTS', # FIXME: Is this still used? 1018 'WEBKITOUTPUTDIR', # FIXME: Is this still used? 1019 'CHROME_DEVEL_SANDBOX', 1020 'CHROME_IPC_LOGGING', 1021 'ASAN_OPTIONS', 1022 'VALGRIND_LIB', 1023 'VALGRIND_LIB_INNER', 1024 ] 1025 if self.host.platform.is_linux() or self.host.platform.is_freebsd(): 1026 variables_to_copy += [ 1027 'XAUTHORITY', 1028 'HOME', 1029 'LANG', 1030 'LD_LIBRARY_PATH', 1031 'DBUS_SESSION_BUS_ADDRESS', 1032 'XDG_DATA_DIRS', 1033 ] 1034 clean_env['DISPLAY'] = self._value_or_default_from_environ('DISPLAY', ':1') 1035 if self.host.platform.is_mac(): 1036 clean_env['DYLD_LIBRARY_PATH'] = self._build_path() 1037 clean_env['DYLD_FRAMEWORK_PATH'] = self._build_path() 1038 variables_to_copy += [ 1039 'HOME', 1040 ] 1041 if self.host.platform.is_win(): 1042 variables_to_copy += [ 1043 'PATH', 1044 'GYP_DEFINES', # Required to locate win sdk. 1045 ] 1046 if self.host.platform.is_cygwin(): 1047 variables_to_copy += [ 1048 'HOMEDRIVE', 1049 'HOMEPATH', 1050 '_NT_SYMBOL_PATH', 1051 ] 1052 1053 for variable in variables_to_copy: 1054 self._copy_value_from_environ_if_set(clean_env, variable) 1055 1056 for string_variable in self.get_option('additional_env_var', []): 1057 [name, value] = string_variable.split('=', 1) 1058 clean_env[name] = value 1059 1060 return clean_env 1061 1062 def show_results_html_file(self, results_filename): 1063 """This routine should display the HTML file pointed at by 1064 results_filename in a users' browser.""" 1065 return self.host.user.open_url(path.abspath_to_uri(self.host.platform, results_filename)) 1066 1067 def create_driver(self, worker_number, no_timeout=False): 1068 """Return a newly created Driver subclass for starting/stopping the test driver.""" 1069 return self._driver_class()(self, worker_number, pixel_tests=self.get_option('pixel_tests'), no_timeout=no_timeout) 1070 1071 def start_helper(self): 1072 """If a port needs to reconfigure graphics settings or do other 1073 things to ensure a known test configuration, it should override this 1074 method.""" 1075 helper_path = self._path_to_helper() 1076 if helper_path: 1077 _log.debug("Starting layout helper %s" % helper_path) 1078 # Note: Not thread safe: http://bugs.python.org/issue2320 1079 self._helper = self._executive.popen([helper_path], 1080 stdin=self._executive.PIPE, stdout=self._executive.PIPE, stderr=None) 1081 is_ready = self._helper.stdout.readline() 1082 if not is_ready.startswith('ready'): 1083 _log.error("layout_test_helper failed to be ready") 1084 1085 def requires_http_server(self): 1086 """Does the port require an HTTP server for running tests? This could 1087 be the case when the tests aren't run on the host platform.""" 1088 return False 1089 1090 def start_http_server(self, additional_dirs=None, number_of_servers=None): 1091 """Start a web server. Raise an error if it can't start or is already running. 1092 1093 Ports can stub this out if they don't need a web server to be running.""" 1094 assert not self._http_server, 'Already running an http server.' 1095 1096 if self._uses_apache(): 1097 server = apache_http_server.LayoutTestApacheHttpd(self, self.results_directory(), additional_dirs=additional_dirs, number_of_servers=number_of_servers) 1098 else: 1099 server = http_server.Lighttpd(self, self.results_directory(), additional_dirs=additional_dirs, number_of_servers=number_of_servers) 1100 1101 server.start() 1102 self._http_server = server 1103 1104 def start_websocket_server(self): 1105 """Start a web server. Raise an error if it can't start or is already running. 1106 1107 Ports can stub this out if they don't need a websocket server to be running.""" 1108 assert not self._websocket_server, 'Already running a websocket server.' 1109 1110 server = websocket_server.PyWebSocket(self, self.results_directory()) 1111 server.start() 1112 self._websocket_server = server 1113 1114 def http_server_supports_ipv6(self): 1115 # Cygwin is the only platform to still use Apache 1.3, which only supports IPV4. 1116 # Once it moves to Apache 2, we can drop this method altogether. 1117 if self.host.platform.is_cygwin(): 1118 return False 1119 return True 1120 1121 def stop_helper(self): 1122 """Shut down the test helper if it is running. Do nothing if 1123 it isn't, or it isn't available. If a port overrides start_helper() 1124 it must override this routine as well.""" 1125 if self._helper: 1126 _log.debug("Stopping layout test helper") 1127 try: 1128 self._helper.stdin.write("x\n") 1129 self._helper.stdin.close() 1130 self._helper.wait() 1131 except IOError, e: 1132 pass 1133 finally: 1134 self._helper = None 1135 1136 def stop_http_server(self): 1137 """Shut down the http server if it is running. Do nothing if it isn't.""" 1138 if self._http_server: 1139 self._http_server.stop() 1140 self._http_server = None 1141 1142 def stop_websocket_server(self): 1143 """Shut down the websocket server if it is running. Do nothing if it isn't.""" 1144 if self._websocket_server: 1145 self._websocket_server.stop() 1146 self._websocket_server = None 1147 1148 # 1149 # TEST EXPECTATION-RELATED METHODS 1150 # 1151 1152 def test_configuration(self): 1153 """Returns the current TestConfiguration for the port.""" 1154 if not self._test_configuration: 1155 self._test_configuration = TestConfiguration(self._version, self._architecture, self._options.configuration.lower()) 1156 return self._test_configuration 1157 1158 # FIXME: Belongs on a Platform object. 1159 @memoized 1160 def all_test_configurations(self): 1161 """Returns a list of TestConfiguration instances, representing all available 1162 test configurations for this port.""" 1163 return self._generate_all_test_configurations() 1164 1165 # FIXME: Belongs on a Platform object. 1166 def configuration_specifier_macros(self): 1167 """Ports may provide a way to abbreviate configuration specifiers to conveniently 1168 refer to them as one term or alias specific values to more generic ones. For example: 1169 1170 (xp, vista, win7) -> win # Abbreviate all Windows versions into one namesake. 1171 (lucid) -> linux # Change specific name of the Linux distro to a more generic term. 1172 1173 Returns a dictionary, each key representing a macro term ('win', for example), 1174 and value being a list of valid configuration specifiers (such as ['xp', 'vista', 'win7']).""" 1175 return self.CONFIGURATION_SPECIFIER_MACROS 1176 1177 def all_baseline_variants(self): 1178 """Returns a list of platform names sufficient to cover all the baselines. 1179 1180 The list should be sorted so that a later platform will reuse 1181 an earlier platform's baselines if they are the same (e.g., 1182 'snowleopard' should precede 'leopard').""" 1183 return self.ALL_BASELINE_VARIANTS 1184 1185 def _generate_all_test_configurations(self): 1186 """Returns a sequence of the TestConfigurations the port supports.""" 1187 # By default, we assume we want to test every graphics type in 1188 # every configuration on every system. 1189 test_configurations = [] 1190 for version, architecture in self.ALL_SYSTEMS: 1191 for build_type in self.ALL_BUILD_TYPES: 1192 test_configurations.append(TestConfiguration(version, architecture, build_type)) 1193 return test_configurations 1194 1195 try_builder_names = frozenset([ 1196 'linux_layout', 1197 'mac_layout', 1198 'win_layout', 1199 'linux_layout_rel', 1200 'mac_layout_rel', 1201 'win_layout_rel', 1202 ]) 1203 1204 def warn_if_bug_missing_in_test_expectations(self): 1205 return True 1206 1207 def _port_specific_expectations_files(self): 1208 paths = [] 1209 paths.append(self.path_from_chromium_base('skia', 'skia_test_expectations.txt')) 1210 paths.append(self._filesystem.join(self.layout_tests_dir(), 'NeverFixTests')) 1211 paths.append(self._filesystem.join(self.layout_tests_dir(), 'StaleTestExpectations')) 1212 paths.append(self._filesystem.join(self.layout_tests_dir(), 'SlowTests')) 1213 1214 builder_name = self.get_option('builder_name', 'DUMMY_BUILDER_NAME') 1215 if builder_name == 'DUMMY_BUILDER_NAME' or '(deps)' in builder_name or builder_name in self.try_builder_names: 1216 paths.append(self.path_from_chromium_base('webkit', 'tools', 'layout_tests', 'test_expectations.txt')) 1217 return paths 1218 1219 def expectations_dict(self): 1220 """Returns an OrderedDict of name -> expectations strings. 1221 The names are expected to be (but not required to be) paths in the filesystem. 1222 If the name is a path, the file can be considered updatable for things like rebaselining, 1223 so don't use names that are paths if they're not paths. 1224 Generally speaking the ordering should be files in the filesystem in cascade order 1225 (TestExpectations followed by Skipped, if the port honors both formats), 1226 then any built-in expectations (e.g., from compile-time exclusions), then --additional-expectations options.""" 1227 # FIXME: rename this to test_expectations() once all the callers are updated to know about the ordered dict. 1228 expectations = OrderedDict() 1229 1230 for path in self.expectations_files(): 1231 if self._filesystem.exists(path): 1232 expectations[path] = self._filesystem.read_text_file(path) 1233 1234 for path in self.get_option('additional_expectations', []): 1235 expanded_path = self._filesystem.expanduser(path) 1236 if self._filesystem.exists(expanded_path): 1237 _log.debug("reading additional_expectations from path '%s'" % path) 1238 expectations[path] = self._filesystem.read_text_file(expanded_path) 1239 else: 1240 _log.warning("additional_expectations path '%s' does not exist" % path) 1241 return expectations 1242 1243 def bot_expectations(self): 1244 if not self.get_option('ignore_flaky_tests'): 1245 return {} 1246 1247 full_port_name = self.determine_full_port_name(self.host, self._options, self.port_name) 1248 builder_category = self.get_option('ignore_builder_category', 'layout') 1249 factory = BotTestExpectationsFactory() 1250 expectations = factory.expectations_for_port(full_port_name, builder_category) 1251 1252 if not expectations: 1253 return {} 1254 1255 ignore_mode = self.get_option('ignore_flaky_tests') 1256 if ignore_mode == 'very-flaky' or ignore_mode == 'maybe-flaky': 1257 return expectations.flakes_by_path(ignore_mode == 'very-flaky') 1258 if ignore_mode == 'unexpected': 1259 return expectations.unexpected_results_by_path() 1260 _log.warning("Unexpected ignore mode: '%s'." % ignore_mode) 1261 return {} 1262 1263 def expectations_files(self): 1264 return [self.path_to_generic_test_expectations_file()] + self._port_specific_expectations_files() 1265 1266 def repository_paths(self): 1267 """Returns a list of (repository_name, repository_path) tuples of its depending code base.""" 1268 return [('blink', self.layout_tests_dir()), 1269 ('chromium', self.path_from_chromium_base('build'))] 1270 1271 _WDIFF_DEL = '##WDIFF_DEL##' 1272 _WDIFF_ADD = '##WDIFF_ADD##' 1273 _WDIFF_END = '##WDIFF_END##' 1274 1275 def _format_wdiff_output_as_html(self, wdiff): 1276 wdiff = cgi.escape(wdiff) 1277 wdiff = wdiff.replace(self._WDIFF_DEL, "<span class=del>") 1278 wdiff = wdiff.replace(self._WDIFF_ADD, "<span class=add>") 1279 wdiff = wdiff.replace(self._WDIFF_END, "</span>") 1280 html = "<head><style>.del { background: #faa; } " 1281 html += ".add { background: #afa; }</style></head>" 1282 html += "<pre>%s</pre>" % wdiff 1283 return html 1284 1285 def _wdiff_command(self, actual_filename, expected_filename): 1286 executable = self._path_to_wdiff() 1287 return [executable, 1288 "--start-delete=%s" % self._WDIFF_DEL, 1289 "--end-delete=%s" % self._WDIFF_END, 1290 "--start-insert=%s" % self._WDIFF_ADD, 1291 "--end-insert=%s" % self._WDIFF_END, 1292 actual_filename, 1293 expected_filename] 1294 1295 @staticmethod 1296 def _handle_wdiff_error(script_error): 1297 # Exit 1 means the files differed, any other exit code is an error. 1298 if script_error.exit_code != 1: 1299 raise script_error 1300 1301 def _run_wdiff(self, actual_filename, expected_filename): 1302 """Runs wdiff and may throw exceptions. 1303 This is mostly a hook for unit testing.""" 1304 # Diffs are treated as binary as they may include multiple files 1305 # with conflicting encodings. Thus we do not decode the output. 1306 command = self._wdiff_command(actual_filename, expected_filename) 1307 wdiff = self._executive.run_command(command, decode_output=False, 1308 error_handler=self._handle_wdiff_error) 1309 return self._format_wdiff_output_as_html(wdiff) 1310 1311 _wdiff_error_html = "Failed to run wdiff, see error log." 1312 1313 def wdiff_text(self, actual_filename, expected_filename): 1314 """Returns a string of HTML indicating the word-level diff of the 1315 contents of the two filenames. Returns an empty string if word-level 1316 diffing isn't available.""" 1317 if not self.wdiff_available(): 1318 return "" 1319 try: 1320 # It's possible to raise a ScriptError we pass wdiff invalid paths. 1321 return self._run_wdiff(actual_filename, expected_filename) 1322 except OSError as e: 1323 if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]: 1324 # Silently ignore cases where wdiff is missing. 1325 self._wdiff_available = False 1326 return "" 1327 raise 1328 except ScriptError as e: 1329 _log.error("Failed to run wdiff: %s" % e) 1330 self._wdiff_available = False 1331 return self._wdiff_error_html 1332 1333 # This is a class variable so we can test error output easily. 1334 _pretty_patch_error_html = "Failed to run PrettyPatch, see error log." 1335 1336 def pretty_patch_text(self, diff_path): 1337 if self._pretty_patch_available is None: 1338 self._pretty_patch_available = self.check_pretty_patch(logging=False) 1339 if not self._pretty_patch_available: 1340 return self._pretty_patch_error_html 1341 command = ("ruby", "-I", self._filesystem.dirname(self._pretty_patch_path), 1342 self._pretty_patch_path, diff_path) 1343 try: 1344 # Diffs are treated as binary (we pass decode_output=False) as they 1345 # may contain multiple files of conflicting encodings. 1346 return self._executive.run_command(command, decode_output=False) 1347 except OSError, e: 1348 # If the system is missing ruby log the error and stop trying. 1349 self._pretty_patch_available = False 1350 _log.error("Failed to run PrettyPatch (%s): %s" % (command, e)) 1351 return self._pretty_patch_error_html 1352 except ScriptError, e: 1353 # If ruby failed to run for some reason, log the command 1354 # output and stop trying. 1355 self._pretty_patch_available = False 1356 _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, e.message_with_output())) 1357 return self._pretty_patch_error_html 1358 1359 def default_configuration(self): 1360 return self._config.default_configuration() 1361 1362 def clobber_old_port_specific_results(self): 1363 pass 1364 1365 # 1366 # PROTECTED ROUTINES 1367 # 1368 # The routines below should only be called by routines in this class 1369 # or any of its subclasses. 1370 # 1371 1372 def _uses_apache(self): 1373 return True 1374 1375 # FIXME: This does not belong on the port object. 1376 @memoized 1377 def _path_to_apache(self): 1378 """Returns the full path to the apache binary. 1379 1380 This is needed only by ports that use the apache_http_server module.""" 1381 raise NotImplementedError('Port._path_to_apache') 1382 1383 # FIXME: This belongs on some platform abstraction instead of Port. 1384 def _is_redhat_based(self): 1385 return self._filesystem.exists('/etc/redhat-release') 1386 1387 def _is_debian_based(self): 1388 return self._filesystem.exists('/etc/debian_version') 1389 1390 def _apache_version(self): 1391 config = self._executive.run_command([self._path_to_apache(), '-v']) 1392 return re.sub(r'(?:.|\n)*Server version: Apache/(\d+\.\d+)(?:.|\n)*', r'\1', config) 1393 1394 # We pass sys_platform into this method to make it easy to unit test. 1395 def _apache_config_file_name_for_platform(self, sys_platform): 1396 if sys_platform == 'cygwin': 1397 return 'cygwin-httpd.conf' # CYGWIN is the only platform to still use Apache 1.3. 1398 if sys_platform.startswith('linux'): 1399 if self._is_redhat_based(): 1400 return 'fedora-httpd-' + self._apache_version() + '.conf' 1401 if self._is_debian_based(): 1402 return 'debian-httpd-' + self._apache_version() + '.conf' 1403 # All platforms use apache2 except for CYGWIN (and Mac OS X Tiger and prior, which we no longer support). 1404 return "apache2-httpd.conf" 1405 1406 def _path_to_apache_config_file(self): 1407 """Returns the full path to the apache configuration file. 1408 1409 If the WEBKIT_HTTP_SERVER_CONF_PATH environment variable is set, its 1410 contents will be used instead. 1411 1412 This is needed only by ports that use the apache_http_server module.""" 1413 config_file_from_env = os.environ.get('WEBKIT_HTTP_SERVER_CONF_PATH') 1414 if config_file_from_env: 1415 if not self._filesystem.exists(config_file_from_env): 1416 raise IOError('%s was not found on the system' % config_file_from_env) 1417 return config_file_from_env 1418 1419 config_file_name = self._apache_config_file_name_for_platform(sys.platform) 1420 return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', config_file_name) 1421 1422 def _path_to_driver(self, configuration=None): 1423 """Returns the full path to the test driver.""" 1424 return self._build_path(self.driver_name()) 1425 1426 def _path_to_webcore_library(self): 1427 """Returns the full path to a built copy of WebCore.""" 1428 return None 1429 1430 def _path_to_helper(self): 1431 """Returns the full path to the layout_test_helper binary, which 1432 is used to help configure the system for the test run, or None 1433 if no helper is needed. 1434 1435 This is likely only used by start/stop_helper().""" 1436 return None 1437 1438 def _path_to_image_diff(self): 1439 """Returns the full path to the image_diff binary, or None if it is not available. 1440 1441 This is likely used only by diff_image()""" 1442 return self._build_path('image_diff') 1443 1444 def _path_to_lighttpd(self): 1445 """Returns the path to the LigHTTPd binary. 1446 1447 This is needed only by ports that use the http_server.py module.""" 1448 raise NotImplementedError('Port._path_to_lighttpd') 1449 1450 def _path_to_lighttpd_modules(self): 1451 """Returns the path to the LigHTTPd modules directory. 1452 1453 This is needed only by ports that use the http_server.py module.""" 1454 raise NotImplementedError('Port._path_to_lighttpd_modules') 1455 1456 def _path_to_lighttpd_php(self): 1457 """Returns the path to the LigHTTPd PHP executable. 1458 1459 This is needed only by ports that use the http_server.py module.""" 1460 raise NotImplementedError('Port._path_to_lighttpd_php') 1461 1462 @memoized 1463 def _path_to_wdiff(self): 1464 """Returns the full path to the wdiff binary, or None if it is not available. 1465 1466 This is likely used only by wdiff_text()""" 1467 for path in ("/usr/bin/wdiff", "/usr/bin/dwdiff"): 1468 if self._filesystem.exists(path): 1469 return path 1470 return None 1471 1472 def _webkit_baseline_path(self, platform): 1473 """Return the full path to the top of the baseline tree for a 1474 given platform.""" 1475 return self._filesystem.join(self.layout_tests_dir(), 'platform', platform) 1476 1477 def _driver_class(self): 1478 """Returns the port's driver implementation.""" 1479 return driver.Driver 1480 1481 def _get_crash_log(self, name, pid, stdout, stderr, newer_than): 1482 if stderr and 'AddressSanitizer' in stderr: 1483 # Running the AddressSanitizer take a lot of memory, so we need to 1484 # serialize access to it across all the concurrently running drivers. 1485 1486 # FIXME: investigate using LLVM_SYMBOLIZER_PATH here to reduce the overhead. 1487 asan_filter_path = self.path_from_chromium_base('tools', 'valgrind', 'asan', 'asan_symbolize.py') 1488 if self._filesystem.exists(asan_filter_path): 1489 output = self._executive.run_command(['flock', sys.executable, asan_filter_path], input=stderr, decode_output=False) 1490 stderr = self._executive.run_command(['c++filt'], input=output, decode_output=False) 1491 1492 name_str = name or '<unknown process name>' 1493 pid_str = str(pid or '<unknown>') 1494 stdout_lines = (stdout or '<empty>').decode('utf8', 'replace').splitlines() 1495 stderr_lines = (stderr or '<empty>').decode('utf8', 'replace').splitlines() 1496 return (stderr, 'crash log for %s (pid %s):\n%s\n%s\n' % (name_str, pid_str, 1497 '\n'.join(('STDOUT: ' + l) for l in stdout_lines), 1498 '\n'.join(('STDERR: ' + l) for l in stderr_lines))) 1499 1500 def look_for_new_crash_logs(self, crashed_processes, start_time): 1501 pass 1502 1503 def look_for_new_samples(self, unresponsive_processes, start_time): 1504 pass 1505 1506 def sample_process(self, name, pid): 1507 pass 1508 1509 def virtual_test_suites(self): 1510 return [ 1511 VirtualTestSuite('gpu', 1512 'fast/canvas', 1513 ['--enable-accelerated-2d-canvas']), 1514 VirtualTestSuite('gpu', 1515 'canvas/philip', 1516 ['--enable-accelerated-2d-canvas']), 1517 VirtualTestSuite('threaded', 1518 'compositing/visibility', 1519 ['--enable-threaded-compositing']), 1520 VirtualTestSuite('threaded', 1521 'compositing/webgl', 1522 ['--enable-threaded-compositing']), 1523 VirtualTestSuite('gpu', 1524 'fast/hidpi', 1525 ['--force-compositing-mode']), 1526 VirtualTestSuite('softwarecompositing', 1527 'compositing', 1528 ['--enable-software-compositing', '--disable-gpu-compositing'], 1529 use_legacy_naming=True), 1530 VirtualTestSuite('deferred', 1531 'fast/images', 1532 ['--enable-deferred-image-decoding', '--enable-per-tile-painting', '--force-compositing-mode']), 1533 VirtualTestSuite('deferred', 1534 'inspector/timeline', 1535 ['--enable-deferred-image-decoding', '--enable-per-tile-painting', '--force-compositing-mode']), 1536 VirtualTestSuite('gpu/compositedscrolling/overflow', 1537 'compositing/overflow', 1538 ['--enable-accelerated-overflow-scroll'], 1539 use_legacy_naming=True), 1540 VirtualTestSuite('gpu/compositedscrolling/scrollbars', 1541 'scrollbars', 1542 ['--enable-accelerated-overflow-scroll'], 1543 use_legacy_naming=True), 1544 VirtualTestSuite('threaded', 1545 'animations', 1546 ['--enable-threaded-compositing']), 1547 VirtualTestSuite('threaded', 1548 'transitions', 1549 ['--enable-threaded-compositing']), 1550 VirtualTestSuite('legacy-animations-engine', 1551 'animations', 1552 ['--disable-web-animations-css']), 1553 VirtualTestSuite('legacy-animations-engine', 1554 'transitions', 1555 ['--disable-web-animations-css']), 1556 VirtualTestSuite('stable', 1557 'webexposed', 1558 ['--stable-release-mode']), 1559 VirtualTestSuite('stable', 1560 'media/stable', 1561 ['--stable-release-mode']), 1562 VirtualTestSuite('android', 1563 'fullscreen', 1564 ['--force-compositing-mode', '--allow-webui-compositing', '--enable-threaded-compositing', 1565 '--enable-fixed-position-compositing', '--enable-accelerated-overflow-scroll', '--enable-accelerated-scrollable-frames', 1566 '--enable-composited-scrolling-for-frames', '--enable-gesture-tap-highlight', '--enable-pinch', 1567 '--enable-overlay-fullscreen-video', '--enable-overlay-scrollbars', '--enable-overscroll-notifications', 1568 '--enable-fixed-layout', '--enable-viewport', '--disable-canvas-aa', 1569 '--disable-composited-antialiasing']), 1570 VirtualTestSuite('implsidepainting', 1571 'inspector/timeline', 1572 ['--enable-threaded-compositing', '--enable-impl-side-painting', '--force-compositing-mode']), 1573 VirtualTestSuite('fasttextautosizing', 1574 'fast/text-autosizing', 1575 ['--enable-fast-text-autosizing']), 1576 VirtualTestSuite('serviceworker', 1577 'http/tests/serviceworker', 1578 ['--enable-service-worker']), 1579 ] 1580 1581 @memoized 1582 def populated_virtual_test_suites(self): 1583 suites = self.virtual_test_suites() 1584 1585 # Sanity-check the suites to make sure they don't point to other suites. 1586 suite_dirs = [suite.name for suite in suites] 1587 for suite in suites: 1588 assert suite.base not in suite_dirs 1589 1590 for suite in suites: 1591 base_tests = self._real_tests([suite.base]) 1592 suite.tests = {} 1593 for test in base_tests: 1594 suite.tests[test.replace(suite.base, suite.name, 1)] = test 1595 return suites 1596 1597 def _virtual_tests(self, paths, suites): 1598 virtual_tests = list() 1599 for suite in suites: 1600 if paths: 1601 for test in suite.tests: 1602 if any(test.startswith(p) for p in paths): 1603 virtual_tests.append(test) 1604 else: 1605 virtual_tests.extend(suite.tests.keys()) 1606 return virtual_tests 1607 1608 def is_virtual_test(self, test_name): 1609 return bool(self.lookup_virtual_suite(test_name)) 1610 1611 def lookup_virtual_suite(self, test_name): 1612 for suite in self.populated_virtual_test_suites(): 1613 if test_name.startswith(suite.name): 1614 return suite 1615 return None 1616 1617 def lookup_virtual_test_base(self, test_name): 1618 suite = self.lookup_virtual_suite(test_name) 1619 if not suite: 1620 return None 1621 return test_name.replace(suite.name, suite.base, 1) 1622 1623 def lookup_virtual_test_args(self, test_name): 1624 for suite in self.populated_virtual_test_suites(): 1625 if test_name.startswith(suite.name): 1626 return suite.args 1627 return [] 1628 1629 def should_run_as_pixel_test(self, test_input): 1630 if not self._options.pixel_tests: 1631 return False 1632 if self._options.pixel_test_directories: 1633 return any(test_input.test_name.startswith(directory) for directory in self._options.pixel_test_directories) 1634 return True 1635 1636 def _modules_to_search_for_symbols(self): 1637 path = self._path_to_webcore_library() 1638 if path: 1639 return [path] 1640 return [] 1641 1642 def _symbols_string(self): 1643 symbols = '' 1644 for path_to_module in self._modules_to_search_for_symbols(): 1645 try: 1646 symbols += self._executive.run_command(['nm', path_to_module], error_handler=self._executive.ignore_error) 1647 except OSError, e: 1648 _log.warn("Failed to run nm: %s. Can't determine supported features correctly." % e) 1649 return symbols 1650 1651 # Ports which use compile-time feature detection should define this method and return 1652 # a dictionary mapping from symbol substrings to possibly disabled test directories. 1653 # When the symbol substrings are not matched, the directories will be skipped. 1654 # If ports don't ever enable certain features, then those directories can just be 1655 # in the Skipped list instead of compile-time-checked here. 1656 def _missing_symbol_to_skipped_tests(self): 1657 if self.PORT_HAS_AUDIO_CODECS_BUILT_IN: 1658 return {} 1659 else: 1660 return { 1661 "ff_mp3_decoder": ["webaudio/codec-tests/mp3"], 1662 "ff_aac_decoder": ["webaudio/codec-tests/aac"], 1663 } 1664 1665 def _has_test_in_directories(self, directory_lists, test_list): 1666 if not test_list: 1667 return False 1668 1669 directories = itertools.chain.from_iterable(directory_lists) 1670 for directory, test in itertools.product(directories, test_list): 1671 if test.startswith(directory): 1672 return True 1673 return False 1674 1675 def _skipped_tests_for_unsupported_features(self, test_list): 1676 # Only check the symbols of there are tests in the test_list that might get skipped. 1677 # This is a performance optimization to avoid the calling nm. 1678 # Runtime feature detection not supported, fallback to static detection: 1679 # Disable any tests for symbols missing from the executable or libraries. 1680 if self._has_test_in_directories(self._missing_symbol_to_skipped_tests().values(), test_list): 1681 symbols_string = self._symbols_string() 1682 if symbols_string is not None: 1683 return reduce(operator.add, [directories for symbol_substring, directories in self._missing_symbol_to_skipped_tests().items() if symbol_substring not in symbols_string], []) 1684 return [] 1685 1686 def _convert_path(self, path): 1687 """Handles filename conversion for subprocess command line args.""" 1688 # See note above in diff_image() for why we need this. 1689 if sys.platform == 'cygwin': 1690 return cygpath(path) 1691 return path 1692 1693 def _build_path(self, *comps): 1694 return self._build_path_with_configuration(None, *comps) 1695 1696 def _build_path_with_configuration(self, configuration, *comps): 1697 # Note that we don't do the option caching that the 1698 # base class does, because finding the right directory is relatively 1699 # fast. 1700 configuration = configuration or self.get_option('configuration') 1701 return self._static_build_path(self._filesystem, self.get_option('build_directory'), 1702 self.path_from_chromium_base(), configuration, comps) 1703 1704 def _check_driver_build_up_to_date(self, configuration): 1705 if configuration in ('Debug', 'Release'): 1706 try: 1707 debug_path = self._path_to_driver('Debug') 1708 release_path = self._path_to_driver('Release') 1709 1710 debug_mtime = self._filesystem.mtime(debug_path) 1711 release_mtime = self._filesystem.mtime(release_path) 1712 1713 if (debug_mtime > release_mtime and configuration == 'Release' or 1714 release_mtime > debug_mtime and configuration == 'Debug'): 1715 most_recent_binary = 'Release' if configuration == 'Debug' else 'Debug' 1716 _log.warning('You are running the %s binary. However the %s binary appears to be more recent. ' 1717 'Please pass --%s.', configuration, most_recent_binary, most_recent_binary.lower()) 1718 _log.warning('') 1719 # This will fail if we don't have both a debug and release binary. 1720 # That's fine because, in this case, we must already be running the 1721 # most up-to-date one. 1722 except OSError: 1723 pass 1724 return True 1725 1726 def _chromium_baseline_path(self, platform): 1727 if platform is None: 1728 platform = self.name() 1729 return self.path_from_webkit_base('LayoutTests', 'platform', platform) 1730 1731class VirtualTestSuite(object): 1732 def __init__(self, name, base, args, use_legacy_naming=False, tests=None): 1733 if use_legacy_naming: 1734 self.name = 'virtual/' + name 1735 else: 1736 if name.find('/') != -1: 1737 _log.error("Virtual test suites names cannot contain /'s: %s" % name) 1738 return 1739 self.name = 'virtual/' + name + '/' + base 1740 self.base = base 1741 self.args = args 1742 self.tests = tests or set() 1743 1744 def __repr__(self): 1745 return "VirtualTestSuite('%s', '%s', %s)" % (self.name, self.base, self.args) 1746