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 30 test infrastructure (the Port and Driver classes).""" 31 32 import cgi 33 import difflib 34 import errno 35 import itertools 36 import logging 37 import os 38 import operator 39 import optparse 40 import re 41 import sys 42 43 try: 44 from collections import OrderedDict 45 except ImportError: 46 # Needed for Python < 2.7 47 from webkitpy.thirdparty.ordered_dict import OrderedDict 48 49 50 from webkitpy.common import find_files 51 from webkitpy.common import read_checksum_from_png 52 from webkitpy.common.memoized import memoized 53 from webkitpy.common.system import path 54 from webkitpy.common.system.executive import ScriptError 55 from webkitpy.common.system.path import cygpath 56 from webkitpy.common.system.systemhost import SystemHost 57 from webkitpy.common.webkit_finder import WebKitFinder 58 from webkitpy.layout_tests.layout_package.bot_test_expectations import BotTestExpectationsFactory 59 from webkitpy.layout_tests.models.test_configuration import TestConfiguration 60 from webkitpy.layout_tests.port import config as port_config 61 from webkitpy.layout_tests.port import driver 62 from webkitpy.layout_tests.port import server_process 63 from webkitpy.layout_tests.port.factory import PortFactory 64 from webkitpy.layout_tests.servers import apache_http_server 65 from webkitpy.layout_tests.servers import http_server 66 from webkitpy.layout_tests.servers import websocket_server 67 68 _log = logging.getLogger(__name__) 69 70 71 # FIXME: This class should merge with WebKitPort now that Chromium behaves mostly like other webkit ports. 72 class Port(object): 73 """Abstract class for Port-specific hooks for the layout_test package.""" 74 75 # Subclasses override this. This should indicate the basic implementation 76 # part of the port name, e.g., 'mac', 'win', 'gtk'; there is probably (?) 77 # one unique value per class. 78 79 # FIXME: We should probably rename this to something like 'implementation_name'. 80 port_name = None 81 82 # Test names resemble unix relative paths, and use '/' as a directory separator. 83 TEST_PATH_SEPARATOR = '/' 84 85 ALL_BUILD_TYPES = ('debug', 'release') 86 87 CONTENT_SHELL_NAME = 'content_shell' 88 89 # True if the port as aac and mp3 codecs built in. 90 PORT_HAS_AUDIO_CODECS_BUILT_IN = False 91 92 @classmethod 93 def determine_full_port_name(cls, host, options, port_name): 94 """Return a fully-specified port name that can be used to construct objects.""" 95 # Subclasses will usually override this. 96 assert port_name.startswith(cls.port_name) 97 return port_name 98 99 def __init__(self, host, port_name, options=None, **kwargs): 100 101 # This value may be different from cls.port_name by having version modifiers 102 # and other fields appended to it (for example, 'qt-arm' or 'mac-wk2'). 103 self._name = port_name 104 105 # These are default values that should be overridden in a subclasses. 106 self._version = '' 107 self._architecture = 'x86' 108 109 # FIXME: Ideally we'd have a package-wide way to get a 110 # well-formed options object that had all of the necessary 111 # options defined on it. 112 self._options = options or optparse.Values() 113 114 self.host = host 115 self._executive = host.executive 116 self._filesystem = host.filesystem 117 self._webkit_finder = WebKitFinder(host.filesystem) 118 self._config = port_config.Config(self._executive, self._filesystem, self.port_name) 119 120 self._helper = None 121 self._http_server = None 122 self._websocket_server = None 123 self._image_differ = None 124 self._server_process_constructor = server_process.ServerProcess # overridable for testing 125 self._http_lock = None # FIXME: Why does this live on the port object? 126 127 # Python's Popen has a bug that causes any pipes opened to a 128 # process that can't be executed to be leaked. Since this 129 # code is specifically designed to tolerate exec failures 130 # to gracefully handle cases where wdiff is not installed, 131 # the bug results in a massive file descriptor leak. As a 132 # workaround, if an exec failure is ever experienced for 133 # wdiff, assume it's not available. This will leak one 134 # file descriptor but that's better than leaking each time 135 # wdiff would be run. 136 # 137 # http://mail.python.org/pipermail/python-list/ 138 # 2008-August/505753.html 139 # http://bugs.python.org/issue3210 140 self._wdiff_available = None 141 142 # FIXME: prettypatch.py knows this path, why is it copied here? 143 self._pretty_patch_path = self.path_from_webkit_base("Tools", "Scripts", "webkitruby", "PrettyPatch", "prettify.rb") 144 self._pretty_patch_available = None 145 146 if not hasattr(options, 'configuration') or not options.configuration: 147 self.set_option_default('configuration', self.default_configuration()) 148 self._test_configuration = None 149 self._reftest_list = {} 150 self._results_directory = None 151 152 def buildbot_archives_baselines(self): 153 return True 154 155 def additional_drt_flag(self): 156 if self.driver_name() == self.CONTENT_SHELL_NAME: 157 return ['--dump-render-tree'] 158 return [] 159 160 def supports_per_test_timeout(self): 161 return False 162 163 def default_pixel_tests(self): 164 # FIXME: Disable until they are run by default on build.webkit.org. 165 return False 166 167 def default_timeout_ms(self): 168 timeout_ms = 6 * 1000 169 if self.get_option('configuration') == 'Debug': 170 # Debug is 6x slower than Release 171 # FIXME: It should be closer to 2x. See crbug.com/254188 172 return 6 * timeout_ms 173 return timeout_ms 174 175 def driver_stop_timeout(self): 176 """ Returns the amount of time in seconds to wait before killing the process in driver.stop().""" 177 # We want to wait for at least 3 seconds, but if we are really slow, we want to be slow on cleanup as 178 # well (for things like ASAN, Valgrind, etc.) 179 return 3.0 * float(self.get_option('time_out_ms', '0')) / self.default_timeout_ms() 180 181 def wdiff_available(self): 182 if self._wdiff_available is None: 183 self._wdiff_available = self.check_wdiff(logging=False) 184 return self._wdiff_available 185 186 def pretty_patch_available(self): 187 if self._pretty_patch_available is None: 188 self._pretty_patch_available = self.check_pretty_patch(logging=False) 189 return self._pretty_patch_available 190 191 def default_child_processes(self): 192 """Return the number of drivers to use for this port.""" 193 return self._executive.cpu_count() 194 195 def default_max_locked_shards(self): 196 """Return the number of "locked" shards to run in parallel (like the http tests).""" 197 return 1 198 199 def worker_startup_delay_secs(self): 200 # FIXME: If we start workers up too quickly, DumpRenderTree appears 201 # to thrash on something and time out its first few tests. Until 202 # we can figure out what's going on, sleep a bit in between 203 # workers. See https://bugs.webkit.org/show_bug.cgi?id=79147 . 204 return 0.1 205 206 def baseline_path(self): 207 """Return the absolute path to the directory to store new baselines in for this port.""" 208 # FIXME: remove once all callers are calling either baseline_version_dir() or baseline_platform_dir() 209 return self.baseline_version_dir() 210 211 def baseline_platform_dir(self): 212 """Return the absolute path to the default (version-independent) platform-specific results.""" 213 return self._filesystem.join(self.layout_tests_dir(), 'platform', self.port_name) 214 215 def baseline_version_dir(self): 216 """Return the absolute path to the platform-and-version-specific results.""" 217 baseline_search_paths = self.baseline_search_path() 218 return baseline_search_paths[0] 219 220 def virtual_baseline_search_path(self, test_name): 221 suite = self.lookup_virtual_suite(test_name) 222 if not suite: 223 return None 224 return [self._filesystem.join(path, suite.name) for path in self.default_baseline_search_path()] 225 226 def baseline_search_path(self): 227 return self.get_option('additional_platform_directory', []) + self._compare_baseline() + self.default_baseline_search_path() 228 229 def default_baseline_search_path(self): 230 """Return a list of absolute paths to directories to search under for 231 baselines. The directories are searched in order.""" 232 search_paths = [] 233 search_paths.append(self.name()) 234 if self.name() != self.port_name: 235 search_paths.append(self.port_name) 236 return map(self._webkit_baseline_path, search_paths) 237 238 @memoized 239 def _compare_baseline(self): 240 factory = PortFactory(self.host) 241 target_port = self.get_option('compare_port') 242 if target_port: 243 return factory.get(target_port).default_baseline_search_path() 244 return [] 245 246 def check_build(self, needs_http): 247 """This routine is used to ensure that the build is up to date 248 and all the needed binaries are present.""" 249 if self.get_option('build'): 250 return False 251 if not self._check_driver(): 252 return False 253 if self.get_option('pixel_tests'): 254 if not self.check_image_diff(): 255 return False 256 if not self._check_port_build(): 257 return False 258 return True 259 260 def _check_driver(self): 261 driver_path = self._path_to_driver() 262 if not self._filesystem.exists(driver_path): 263 _log.error("%s was not found at %s" % (self.driver_name(), driver_path)) 264 return False 265 return True 266 267 def _check_port_build(self): 268 # Ports can override this method to do additional checks. 269 return True 270 271 def check_sys_deps(self, needs_http): 272 """If the port needs to do some runtime checks to ensure that the 273 tests can be run successfully, it should override this routine. 274 This step can be skipped with --nocheck-sys-deps. 275 276 Returns whether the system is properly configured.""" 277 if needs_http: 278 return self.check_httpd() 279 return True 280 281 def check_image_diff(self, override_step=None, logging=True): 282 """This routine is used to check whether image_diff binary exists.""" 283 image_diff_path = self._path_to_image_diff() 284 if not self._filesystem.exists(image_diff_path): 285 _log.error("image_diff was not found at %s" % image_diff_path) 286 return False 287 return True 288 289 def check_pretty_patch(self, logging=True): 290 """Checks whether we can use the PrettyPatch ruby script.""" 291 try: 292 _ = self._executive.run_command(['ruby', '--version']) 293 except OSError, e: 294 if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]: 295 if logging: 296 _log.warning("Ruby is not installed; can't generate pretty patches.") 297 _log.warning('') 298 return False 299 300 if not self._filesystem.exists(self._pretty_patch_path): 301 if logging: 302 _log.warning("Unable to find %s; can't generate pretty patches." % self._pretty_patch_path) 303 _log.warning('') 304 return False 305 306 return True 307 308 def check_wdiff(self, logging=True): 309 if not self._path_to_wdiff(): 310 # Don't need to log here since this is the port choosing not to use wdiff. 311 return False 312 313 try: 314 _ = self._executive.run_command([self._path_to_wdiff(), '--help']) 315 except OSError: 316 if logging: 317 message = self._wdiff_missing_message() 318 if message: 319 for line in message.splitlines(): 320 _log.warning(' ' + line) 321 _log.warning('') 322 return False 323 324 return True 325 326 def _wdiff_missing_message(self): 327 return 'wdiff is not installed; please install it to generate word-by-word diffs.' 328 329 def check_httpd(self): 330 if self._uses_apache(): 331 httpd_path = self._path_to_apache() 332 else: 333 httpd_path = self._path_to_lighttpd() 334 335 try: 336 server_name = self._filesystem.basename(httpd_path) 337 env = self.setup_environ_for_server(server_name) 338 if self._executive.run_command([httpd_path, "-v"], env=env, return_exit_code=True) != 0: 339 _log.error("httpd seems broken. Cannot run http tests.") 340 return False 341 return True 342 except OSError: 343 _log.error("No httpd found. Cannot run http tests.") 344 return False 345 346 def do_text_results_differ(self, expected_text, actual_text): 347 return expected_text != actual_text 348 349 def do_audio_results_differ(self, expected_audio, actual_audio): 350 return expected_audio != actual_audio 351 352 def diff_image(self, expected_contents, actual_contents): 353 """Compare two images and return a tuple of an image diff, and an error string. 354 355 If an error occurs (like image_diff isn't found, or crashes, we log an error and return True (for a diff). 356 """ 357 # If only one of them exists, return that one. 358 if not actual_contents and not expected_contents: 359 return (None, None) 360 if not actual_contents: 361 return (expected_contents, None) 362 if not expected_contents: 363 return (actual_contents, None) 364 365 tempdir = self._filesystem.mkdtemp() 366 367 expected_filename = self._filesystem.join(str(tempdir), "expected.png") 368 self._filesystem.write_binary_file(expected_filename, expected_contents) 369 370 actual_filename = self._filesystem.join(str(tempdir), "actual.png") 371 self._filesystem.write_binary_file(actual_filename, actual_contents) 372 373 diff_filename = self._filesystem.join(str(tempdir), "diff.png") 374 375 # image_diff needs native win paths as arguments, so we need to convert them if running under cygwin. 376 native_expected_filename = self._convert_path(expected_filename) 377 native_actual_filename = self._convert_path(actual_filename) 378 native_diff_filename = self._convert_path(diff_filename) 379 380 executable = self._path_to_image_diff() 381 # Note that although we are handed 'old', 'new', image_diff wants 'new', 'old'. 382 comand = [executable, '--diff', native_actual_filename, native_expected_filename, native_diff_filename] 383 384 result = None 385 err_str = None 386 try: 387 exit_code = self._executive.run_command(comand, return_exit_code=True) 388 if exit_code == 0: 389 # The images are the same. 390 result = None 391 elif exit_code == 1: 392 result = self._filesystem.read_binary_file(native_diff_filename) 393 else: 394 err_str = "image diff returned an exit code of %s" % exit_code 395 except OSError, e: 396 err_str = 'error running image diff: %s' % str(e) 397 finally: 398 self._filesystem.rmtree(str(tempdir)) 399 400 return (result, err_str or None) 401 402 def diff_text(self, expected_text, actual_text, expected_filename, actual_filename): 403 """Returns a string containing the diff of the two text strings 404 in 'unified diff' format.""" 405 406 # The filenames show up in the diff output, make sure they're 407 # raw bytes and not unicode, so that they don't trigger join() 408 # trying to decode the input. 409 def to_raw_bytes(string_value): 410 if isinstance(string_value, unicode): 411 return string_value.encode('utf-8') 412 return string_value 413 expected_filename = to_raw_bytes(expected_filename) 414 actual_filename = to_raw_bytes(actual_filename) 415 diff = difflib.unified_diff(expected_text.splitlines(True), 416 actual_text.splitlines(True), 417 expected_filename, 418 actual_filename) 419 return ''.join(diff) 420 421 def driver_name(self): 422 if self.get_option('driver_name'): 423 return self.get_option('driver_name') 424 return self.CONTENT_SHELL_NAME 425 426 def expected_baselines_by_extension(self, test_name): 427 """Returns a dict mapping baseline suffix to relative path for each baseline in 428 a test. For reftests, it returns ".==" or ".!=" instead of the suffix.""" 429 # FIXME: The name similarity between this and expected_baselines() below, is unfortunate. 430 # We should probably rename them both. 431 baseline_dict = {} 432 reference_files = self.reference_files(test_name) 433 if reference_files: 434 # FIXME: How should this handle more than one type of reftest? 435 baseline_dict['.' + reference_files[0][0]] = self.relative_test_filename(reference_files[0][1]) 436 437 for extension in self.baseline_extensions(): 438 path = self.expected_filename(test_name, extension, return_default=False) 439 baseline_dict[extension] = self.relative_test_filename(path) if path else path 440 441 return baseline_dict 442 443 def baseline_extensions(self): 444 """Returns a tuple of all of the non-reftest baseline extensions we use. The extensions include the leading '.'.""" 445 return ('.wav', '.txt', '.png') 446 447 def expected_baselines(self, test_name, suffix, all_baselines=False): 448 """Given a test name, finds where the baseline results are located. 449 450 Args: 451 test_name: name of test file (usually a relative path under LayoutTests/) 452 suffix: file suffix of the expected results, including dot; e.g. 453 '.txt' or '.png'. This should not be None, but may be an empty 454 string. 455 all_baselines: If True, return an ordered list of all baseline paths 456 for the given platform. If False, return only the first one. 457 Returns 458 a list of ( platform_dir, results_filename ), where 459 platform_dir - abs path to the top of the results tree (or test 460 tree) 461 results_filename - relative path from top of tree to the results 462 file 463 (port.join() of the two gives you the full path to the file, 464 unless None was returned.) 465 Return values will be in the format appropriate for the current 466 platform (e.g., "\\" for path separators on Windows). If the results 467 file is not found, then None will be returned for the directory, 468 but the expected relative pathname will still be returned. 469 470 This routine is generic but lives here since it is used in 471 conjunction with the other baseline and filename routines that are 472 platform specific. 473 """ 474 baseline_filename = self._filesystem.splitext(test_name)[0] + '-expected' + suffix 475 baseline_search_path = self.baseline_search_path() 476 477 baselines = [] 478 for platform_dir in baseline_search_path: 479 if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)): 480 baselines.append((platform_dir, baseline_filename)) 481 482 if not all_baselines and baselines: 483 return baselines 484 485 # If it wasn't found in a platform directory, return the expected 486 # result in the test directory, even if no such file actually exists. 487 platform_dir = self.layout_tests_dir() 488 if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)): 489 baselines.append((platform_dir, baseline_filename)) 490 491 if baselines: 492 return baselines 493 494 return [(None, baseline_filename)] 495 496 def expected_filename(self, test_name, suffix, return_default=True): 497 """Given a test name, returns an absolute path to its expected results. 498 499 If no expected results are found in any of the searched directories, 500 the directory in which the test itself is located will be returned. 501 The return value is in the format appropriate for the platform 502 (e.g., "\\" for path separators on windows). 503 504 Args: 505 test_name: name of test file (usually a relative path under LayoutTests/) 506 suffix: file suffix of the expected results, including dot; e.g. '.txt' 507 or '.png'. This should not be None, but may be an empty string. 508 platform: the most-specific directory name to use to build the 509 search list of directories, e.g., 'win', or 510 'chromium-cg-mac-leopard' (we follow the WebKit format) 511 return_default: if True, returns the path to the generic expectation if nothing 512 else is found; if False, returns None. 513 514 This routine is generic but is implemented here to live alongside 515 the other baseline and filename manipulation routines. 516 """ 517 # FIXME: The [0] here is very mysterious, as is the destructured return. 518 platform_dir, baseline_filename = self.expected_baselines(test_name, suffix)[0] 519 if platform_dir: 520 return self._filesystem.join(platform_dir, baseline_filename) 521 522 actual_test_name = self.lookup_virtual_test_base(test_name) 523 if actual_test_name: 524 return self.expected_filename(actual_test_name, suffix) 525 526 if return_default: 527 return self._filesystem.join(self.layout_tests_dir(), baseline_filename) 528 return None 529 530 def expected_checksum(self, test_name): 531 """Returns the checksum of the image we expect the test to produce, or None if it is a text-only test.""" 532 png_path = self.expected_filename(test_name, '.png') 533 534 if self._filesystem.exists(png_path): 535 with self._filesystem.open_binary_file_for_reading(png_path) as filehandle: 536 return read_checksum_from_png.read_checksum(filehandle) 537 538 return None 539 540 def expected_image(self, test_name): 541 """Returns the image we expect the test to produce.""" 542 baseline_path = self.expected_filename(test_name, '.png') 543 if not self._filesystem.exists(baseline_path): 544 return None 545 return self._filesystem.read_binary_file(baseline_path) 546 547 def expected_audio(self, test_name): 548 baseline_path = self.expected_filename(test_name, '.wav') 549 if not self._filesystem.exists(baseline_path): 550 return None 551 return self._filesystem.read_binary_file(baseline_path) 552 553 def expected_text(self, test_name): 554 """Returns the text output we expect the test to produce, or None 555 if we don't expect there to be any text output. 556 End-of-line characters are normalized to '\n'.""" 557 # FIXME: DRT output is actually utf-8, but since we don't decode the 558 # output from DRT (instead treating it as a binary string), we read the 559 # baselines as a binary string, too. 560 baseline_path = self.expected_filename(test_name, '.txt') 561 if not self._filesystem.exists(baseline_path): 562 return None 563 text = self._filesystem.read_binary_file(baseline_path) 564 return text.replace("\r\n", "\n") 565 566 def _get_reftest_list(self, test_name): 567 dirname = self._filesystem.join(self.layout_tests_dir(), self._filesystem.dirname(test_name)) 568 if dirname not in self._reftest_list: 569 self._reftest_list[dirname] = Port._parse_reftest_list(self._filesystem, dirname) 570 return self._reftest_list[dirname] 571 572 @staticmethod 573 def _parse_reftest_list(filesystem, test_dirpath): 574 reftest_list_path = filesystem.join(test_dirpath, 'reftest.list') 575 if not filesystem.isfile(reftest_list_path): 576 return None 577 reftest_list_file = filesystem.read_text_file(reftest_list_path) 578 579 parsed_list = {} 580 for line in reftest_list_file.split('\n'): 581 line = re.sub('#.+$', '', line) 582 split_line = line.split() 583 if len(split_line) < 3: 584 continue 585 expectation_type, test_file, ref_file = split_line 586 parsed_list.setdefault(filesystem.join(test_dirpath, test_file), []).append((expectation_type, filesystem.join(test_dirpath, ref_file))) 587 return parsed_list 588 589 def reference_files(self, test_name): 590 """Return a list of expectation (== or !=) and filename pairs""" 591 592 reftest_list = self._get_reftest_list(test_name) 593 if not reftest_list: 594 reftest_list = [] 595 for expectation, prefix in (('==', ''), ('!=', '-mismatch')): 596 for extention in Port._supported_file_extensions: 597 path = self.expected_filename(test_name, prefix + extention) 598 if self._filesystem.exists(path): 599 reftest_list.append((expectation, path)) 600 return reftest_list 601 602 return reftest_list.get(self._filesystem.join(self.layout_tests_dir(), test_name), []) # pylint: disable=E1103 603 604 def tests(self, paths): 605 """Return the list of tests found matching paths.""" 606 tests = self._real_tests(paths) 607 tests.extend(self._virtual_tests(paths, self.populated_virtual_test_suites())) 608 return tests 609 610 def _real_tests(self, paths): 611 # When collecting test cases, skip these directories 612 skipped_directories = set(['.svn', '_svn', 'platform', 'resources', 'script-tests', 'reference', 'reftest']) 613 files = find_files.find(self._filesystem, self.layout_tests_dir(), paths, skipped_directories, Port.is_test_file, self.test_key) 614 return [self.relative_test_filename(f) for f in files] 615 616 # When collecting test cases, we include any file with these extensions. 617 _supported_file_extensions = set(['.html', '.xml', '.xhtml', '.xht', '.pl', 618 '.htm', '.php', '.svg', '.mht']) 619 620 @staticmethod 621 # If any changes are made here be sure to update the isUsedInReftest method in old-run-webkit-tests as well. 622 def is_reference_html_file(filesystem, dirname, filename): 623 if filename.startswith('ref-') or filename.startswith('notref-'): 624 return True 625 filename_wihout_ext, unused = filesystem.splitext(filename) 626 for suffix in ['-expected', '-expected-mismatch', '-ref', '-notref']: 627 if filename_wihout_ext.endswith(suffix): 628 return True 629 return False 630 631 @staticmethod 632 def _has_supported_extension(filesystem, filename): 633 """Return true if filename is one of the file extensions we want to run a test on.""" 634 extension = filesystem.splitext(filename)[1] 635 return extension in Port._supported_file_extensions 636 637 @staticmethod 638 def is_test_file(filesystem, dirname, filename): 639 return Port._has_supported_extension(filesystem, filename) and not Port.is_reference_html_file(filesystem, dirname, filename) 640 641 def test_key(self, test_name): 642 """Turns a test name into a list with two sublists, the natural key of the 643 dirname, and the natural key of the basename. 644 645 This can be used when sorting paths so that files in a directory. 646 directory are kept together rather than being mixed in with files in 647 subdirectories.""" 648 dirname, basename = self.split_test(test_name) 649 return (self._natural_sort_key(dirname + self.TEST_PATH_SEPARATOR), self._natural_sort_key(basename)) 650 651 def _natural_sort_key(self, string_to_split): 652 """ Turns a string into a list of string and number chunks, i.e. "z23a" -> ["z", 23, "a"] 653 654 This can be used to implement "natural sort" order. See: 655 http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html 656 http://nedbatchelder.com/blog/200712.html#e20071211T054956 657 """ 658 def tryint(val): 659 try: 660 return int(val) 661 except ValueError: 662 return val 663 664 return [tryint(chunk) for chunk in re.split('(\d+)', string_to_split)] 665 666 def test_dirs(self): 667 """Returns the list of top-level test directories.""" 668 layout_tests_dir = self.layout_tests_dir() 669 return filter(lambda x: self._filesystem.isdir(self._filesystem.join(layout_tests_dir, x)), 670 self._filesystem.listdir(layout_tests_dir)) 671 672 @memoized 673 def test_isfile(self, test_name): 674 """Return True if the test name refers to a directory of tests.""" 675 # Used by test_expectations.py to apply rules to whole directories. 676 if self._filesystem.isfile(self.abspath_for_test(test_name)): 677 return True 678 base = self.lookup_virtual_test_base(test_name) 679 return base and self._filesystem.isfile(self.abspath_for_test(base)) 680 681 @memoized 682 def test_isdir(self, test_name): 683 """Return True if the test name refers to a directory of tests.""" 684 # Used by test_expectations.py to apply rules to whole directories. 685 if self._filesystem.isdir(self.abspath_for_test(test_name)): 686 return True 687 base = self.lookup_virtual_test_base(test_name) 688 return base and self._filesystem.isdir(self.abspath_for_test(base)) 689 690 @memoized 691 def test_exists(self, test_name): 692 """Return True if the test name refers to an existing test or baseline.""" 693 # Used by test_expectations.py to determine if an entry refers to a 694 # valid test and by printing.py to determine if baselines exist. 695 return self.test_isfile(test_name) or self.test_isdir(test_name) 696 697 def split_test(self, test_name): 698 """Splits a test name into the 'directory' part and the 'basename' part.""" 699 index = test_name.rfind(self.TEST_PATH_SEPARATOR) 700 if index < 1: 701 return ('', test_name) 702 return (test_name[0:index], test_name[index:]) 703 704 def normalize_test_name(self, test_name): 705 """Returns a normalized version of the test name or test directory.""" 706 if test_name.endswith('/'): 707 return test_name 708 if self.test_isdir(test_name): 709 return test_name + '/' 710 return test_name 711 712 def driver_cmd_line(self): 713 """Prints the DRT command line that will be used.""" 714 driver = self.create_driver(0) 715 return driver.cmd_line(self.get_option('pixel_tests'), []) 716 717 def update_baseline(self, baseline_path, data): 718 """Updates the baseline for a test. 719 720 Args: 721 baseline_path: the actual path to use for baseline, not the path to 722 the test. This function is used to update either generic or 723 platform-specific baselines, but we can't infer which here. 724 data: contents of the baseline. 725 """ 726 self._filesystem.write_binary_file(baseline_path, data) 727 728 # FIXME: update callers to create a finder and call it instead of these next five routines (which should be protected). 729 def webkit_base(self): 730 return self._webkit_finder.webkit_base() 731 732 def path_from_webkit_base(self, *comps): 733 return self._webkit_finder.path_from_webkit_base(*comps) 734 735 def path_to_script(self, script_name): 736 return self._webkit_finder.path_to_script(script_name) 737 738 def layout_tests_dir(self): 739 return self._webkit_finder.layout_tests_dir() 740 741 def perf_tests_dir(self): 742 return self._webkit_finder.perf_tests_dir() 743 744 def skipped_layout_tests(self, test_list): 745 """Returns tests skipped outside of the TestExpectations files.""" 746 return set(self._skipped_tests_for_unsupported_features(test_list)) 747 748 def _tests_from_skipped_file_contents(self, skipped_file_contents): 749 tests_to_skip = [] 750 for line in skipped_file_contents.split('\n'): 751 line = line.strip() 752 line = line.rstrip('/') # Best to normalize directory names to not include the trailing slash. 753 if line.startswith('#') or not len(line): 754 continue 755 tests_to_skip.append(line) 756 return tests_to_skip 757 758 def _expectations_from_skipped_files(self, skipped_file_paths): 759 tests_to_skip = [] 760 for search_path in skipped_file_paths: 761 filename = self._filesystem.join(self._webkit_baseline_path(search_path), "Skipped") 762 if not self._filesystem.exists(filename): 763 _log.debug("Skipped does not exist: %s" % filename) 764 continue 765 _log.debug("Using Skipped file: %s" % filename) 766 skipped_file_contents = self._filesystem.read_text_file(filename) 767 tests_to_skip.extend(self._tests_from_skipped_file_contents(skipped_file_contents)) 768 return tests_to_skip 769 770 @memoized 771 def skipped_perf_tests(self): 772 return self._expectations_from_skipped_files([self.perf_tests_dir()]) 773 774 def skips_perf_test(self, test_name): 775 for test_or_category in self.skipped_perf_tests(): 776 if test_or_category == test_name: 777 return True 778 category = self._filesystem.join(self.perf_tests_dir(), test_or_category) 779 if self._filesystem.isdir(category) and test_name.startswith(test_or_category): 780 return True 781 return False 782 783 def is_chromium(self): 784 return False 785 786 def name(self): 787 """Returns a name that uniquely identifies this particular type of port 788 (e.g., "mac-snowleopard" or "linux-x86_x64" and can be passed 789 to factory.get() to instantiate the port.""" 790 return self._name 791 792 def operating_system(self): 793 # Subclasses should override this default implementation. 794 return 'mac' 795 796 def version(self): 797 """Returns a string indicating the version of a given platform, e.g. 798 'leopard' or 'xp'. 799 800 This is used to help identify the exact port when parsing test 801 expectations, determining search paths, and logging information.""" 802 return self._version 803 804 def architecture(self): 805 return self._architecture 806 807 def get_option(self, name, default_value=None): 808 return getattr(self._options, name, default_value) 809 810 def set_option_default(self, name, default_value): 811 return self._options.ensure_value(name, default_value) 812 813 @memoized 814 def path_to_generic_test_expectations_file(self): 815 return self._filesystem.join(self.layout_tests_dir(), 'TestExpectations') 816 817 def relative_test_filename(self, filename): 818 """Returns a test_name a relative unix-style path for a filename under the LayoutTests 819 directory. Ports may legitimately return abspaths here if no relpath makes sense.""" 820 # Ports that run on windows need to override this method to deal with 821 # filenames with backslashes in them. 822 if filename.startswith(self.layout_tests_dir()): 823 return self.host.filesystem.relpath(filename, self.layout_tests_dir()) 824 else: 825 return self.host.filesystem.abspath(filename) 826 827 @memoized 828 def abspath_for_test(self, test_name): 829 """Returns the full path to the file for a given test name. This is the 830 inverse of relative_test_filename().""" 831 return self._filesystem.join(self.layout_tests_dir(), test_name) 832 833 def results_directory(self): 834 """Absolute path to the place to store the test results (uses --results-directory).""" 835 if not self._results_directory: 836 option_val = self.get_option('results_directory') or self.default_results_directory() 837 self._results_directory = self._filesystem.abspath(option_val) 838 return self._results_directory 839 840 def perf_results_directory(self): 841 return self._build_path() 842 843 def default_results_directory(self): 844 """Absolute path to the default place to store the test results.""" 845 # Results are store relative to the built products to make it easy 846 # to have multiple copies of webkit checked out and built. 847 return self._build_path('layout-test-results') 848 849 def setup_test_run(self): 850 """Perform port-specific work at the beginning of a test run.""" 851 pass 852 853 def clean_up_test_run(self): 854 """Perform port-specific work at the end of a test run.""" 855 if self._image_differ: 856 self._image_differ.stop() 857 self._image_differ = None 858 859 # FIXME: os.environ access should be moved to onto a common/system class to be more easily mockable. 860 def _value_or_default_from_environ(self, name, default=None): 861 if name in os.environ: 862 return os.environ[name] 863 return default 864 865 def _copy_value_from_environ_if_set(self, clean_env, name): 866 if name in os.environ: 867 clean_env[name] = os.environ[name] 868 869 def setup_environ_for_server(self, server_name=None): 870 # We intentionally copy only a subset of os.environ when 871 # launching subprocesses to ensure consistent test results. 872 clean_env = {} 873 variables_to_copy = [ 874 # For Linux: 875 'XAUTHORITY', 876 'HOME', 877 'LANG', 878 'LD_LIBRARY_PATH', 879 'DBUS_SESSION_BUS_ADDRESS', 880 'XDG_DATA_DIRS', 881 882 # Darwin: 883 'DYLD_LIBRARY_PATH', 884 'HOME', 885 886 # CYGWIN: 887 'HOMEDRIVE', 888 'HOMEPATH', 889 '_NT_SYMBOL_PATH', 890 891 # Windows: 892 'PATH', 893 894 # Most ports (?): 895 'WEBKIT_TESTFONTS', 896 'WEBKITOUTPUTDIR', 897 898 # Chromium: 899 'CHROME_DEVEL_SANDBOX', 900 'CHROME_IPC_LOGGING', 901 'ASAN_OPTIONS', 902 903 ] 904 for variable in variables_to_copy: 905 self._copy_value_from_environ_if_set(clean_env, variable) 906 907 # For Linux: 908 clean_env['DISPLAY'] = self._value_or_default_from_environ('DISPLAY', ':1') 909 910 for string_variable in self.get_option('additional_env_var', []): 911 [name, value] = string_variable.split('=', 1) 912 clean_env[name] = value 913 914 return clean_env 915 916 def show_results_html_file(self, results_filename): 917 """This routine should display the HTML file pointed at by 918 results_filename in a users' browser.""" 919 return self.host.user.open_url(path.abspath_to_uri(self.host.platform, results_filename)) 920 921 def create_driver(self, worker_number, no_timeout=False): 922 """Return a newly created Driver subclass for starting/stopping the test driver.""" 923 return self._driver_class()(self, worker_number, pixel_tests=self.get_option('pixel_tests'), no_timeout=no_timeout) 924 925 def start_helper(self): 926 """If a port needs to reconfigure graphics settings or do other 927 things to ensure a known test configuration, it should override this 928 method.""" 929 pass 930 931 def requires_http_server(self): 932 """Does the port require an HTTP server for running tests? This could 933 be the case when the tests aren't run on the host platform.""" 934 return False 935 936 def start_http_server(self, additional_dirs=None, number_of_servers=None): 937 """Start a web server. Raise an error if it can't start or is already running. 938 939 Ports can stub this out if they don't need a web server to be running.""" 940 assert not self._http_server, 'Already running an http server.' 941 942 if self._uses_apache(): 943 server = apache_http_server.LayoutTestApacheHttpd(self, self.results_directory(), additional_dirs=additional_dirs, number_of_servers=number_of_servers) 944 else: 945 server = http_server.Lighttpd(self, self.results_directory(), additional_dirs=additional_dirs, number_of_servers=number_of_servers) 946 947 server.start() 948 self._http_server = server 949 950 def start_websocket_server(self): 951 """Start a web server. Raise an error if it can't start or is already running. 952 953 Ports can stub this out if they don't need a websocket server to be running.""" 954 assert not self._websocket_server, 'Already running a websocket server.' 955 956 server = websocket_server.PyWebSocket(self, self.results_directory()) 957 server.start() 958 self._websocket_server = server 959 960 def http_server_supports_ipv6(self): 961 # Cygwin is the only platform to still use Apache 1.3, which only supports IPV4. 962 # Once it moves to Apache 2, we can drop this method altogether. 963 if self.host.platform.is_cygwin(): 964 return False 965 return True 966 967 def stop_helper(self): 968 """Shut down the test helper if it is running. Do nothing if 969 it isn't, or it isn't available. If a port overrides start_helper() 970 it must override this routine as well.""" 971 pass 972 973 def stop_http_server(self): 974 """Shut down the http server if it is running. Do nothing if it isn't.""" 975 if self._http_server: 976 self._http_server.stop() 977 self._http_server = None 978 979 def stop_websocket_server(self): 980 """Shut down the websocket server if it is running. Do nothing if it isn't.""" 981 if self._websocket_server: 982 self._websocket_server.stop() 983 self._websocket_server = None 984 985 # 986 # TEST EXPECTATION-RELATED METHODS 987 # 988 989 def test_configuration(self): 990 """Returns the current TestConfiguration for the port.""" 991 if not self._test_configuration: 992 self._test_configuration = TestConfiguration(self._version, self._architecture, self._options.configuration.lower()) 993 return self._test_configuration 994 995 # FIXME: Belongs on a Platform object. 996 @memoized 997 def all_test_configurations(self): 998 """Returns a list of TestConfiguration instances, representing all available 999 test configurations for this port.""" 1000 return self._generate_all_test_configurations() 1001 1002 # FIXME: Belongs on a Platform object. 1003 def configuration_specifier_macros(self): 1004 """Ports may provide a way to abbreviate configuration specifiers to conveniently 1005 refer to them as one term or alias specific values to more generic ones. For example: 1006 1007 (xp, vista, win7) -> win # Abbreviate all Windows versions into one namesake. 1008 (lucid) -> linux # Change specific name of the Linux distro to a more generic term. 1009 1010 Returns a dictionary, each key representing a macro term ('win', for example), 1011 and value being a list of valid configuration specifiers (such as ['xp', 'vista', 'win7']).""" 1012 return {} 1013 1014 def all_baseline_variants(self): 1015 """Returns a list of platform names sufficient to cover all the baselines. 1016 1017 The list should be sorted so that a later platform will reuse 1018 an earlier platform's baselines if they are the same (e.g., 1019 'snowleopard' should precede 'leopard').""" 1020 raise NotImplementedError 1021 1022 def expectations_dict(self): 1023 """Returns an OrderedDict of name -> expectations strings. 1024 The names are expected to be (but not required to be) paths in the filesystem. 1025 If the name is a path, the file can be considered updatable for things like rebaselining, 1026 so don't use names that are paths if they're not paths. 1027 Generally speaking the ordering should be files in the filesystem in cascade order 1028 (TestExpectations followed by Skipped, if the port honors both formats), 1029 then any built-in expectations (e.g., from compile-time exclusions), then --additional-expectations options.""" 1030 # FIXME: rename this to test_expectations() once all the callers are updated to know about the ordered dict. 1031 expectations = OrderedDict() 1032 1033 for path in self.expectations_files(): 1034 if self._filesystem.exists(path): 1035 expectations[path] = self._filesystem.read_text_file(path) 1036 1037 for path in self.get_option('additional_expectations', []): 1038 expanded_path = self._filesystem.expanduser(path) 1039 if self._filesystem.exists(expanded_path): 1040 _log.debug("reading additional_expectations from path '%s'" % path) 1041 expectations[path] = self._filesystem.read_text_file(expanded_path) 1042 else: 1043 _log.warning("additional_expectations path '%s' does not exist" % path) 1044 return expectations 1045 1046 def bot_expectations(self): 1047 if not self.get_option('ignore_flaky_tests'): 1048 return {} 1049 1050 full_port_name = self.determine_full_port_name(self.host, self._options, self.port_name) 1051 builder_category = self.get_option('ignore_builder_category', 'layout') 1052 factory = BotTestExpectationsFactory() 1053 expectations = factory.expectations_for_port(full_port_name, builder_category) 1054 1055 if not expectations: 1056 return {} 1057 1058 ignore_mode = self.get_option('ignore_flaky_tests') 1059 if ignore_mode == 'very-flaky' or ignore_mode == 'maybe-flaky': 1060 return expectations.flakes_by_path(ignore_mode == 'very-flaky') 1061 if ignore_mode == 'unexpected': 1062 return expectations.unexpected_results_by_path() 1063 _log.warning("Unexpected ignore mode: '%s'." % ignore_mode) 1064 return {} 1065 1066 def _port_specific_expectations_files(self): 1067 # Unlike baseline_search_path, we only want to search [WK2-PORT, PORT-VERSION, PORT] and any directories 1068 # included via --additional-platform-directory, not the full casade. 1069 search_paths = [self.port_name] 1070 1071 non_wk2_name = self.name().replace('-wk2', '') 1072 if non_wk2_name != self.port_name: 1073 search_paths.append(non_wk2_name) 1074 1075 search_paths.extend(self.get_option("additional_platform_directory", [])) 1076 1077 return [self._filesystem.join(self._webkit_baseline_path(d), 'TestExpectations') for d in search_paths] 1078 1079 def expectations_files(self): 1080 return [self.path_to_generic_test_expectations_file()] + self._port_specific_expectations_files() 1081 1082 def repository_paths(self): 1083 """Returns a list of (repository_name, repository_path) tuples of its depending code base. 1084 By default it returns a list that only contains a ('WebKit', <webkitRepositoryPath>) tuple.""" 1085 1086 # We use LayoutTest directory here because webkit_base isn't a part of WebKit repository in Chromium port 1087 # where turnk isn't checked out as a whole. 1088 return [('blink', self.layout_tests_dir())] 1089 1090 _WDIFF_DEL = '##WDIFF_DEL##' 1091 _WDIFF_ADD = '##WDIFF_ADD##' 1092 _WDIFF_END = '##WDIFF_END##' 1093 1094 def _format_wdiff_output_as_html(self, wdiff): 1095 wdiff = cgi.escape(wdiff) 1096 wdiff = wdiff.replace(self._WDIFF_DEL, "<span class=del>") 1097 wdiff = wdiff.replace(self._WDIFF_ADD, "<span class=add>") 1098 wdiff = wdiff.replace(self._WDIFF_END, "</span>") 1099 html = "<head><style>.del { background: #faa; } " 1100 html += ".add { background: #afa; }</style></head>" 1101 html += "<pre>%s</pre>" % wdiff 1102 return html 1103 1104 def _wdiff_command(self, actual_filename, expected_filename): 1105 executable = self._path_to_wdiff() 1106 return [executable, 1107 "--start-delete=%s" % self._WDIFF_DEL, 1108 "--end-delete=%s" % self._WDIFF_END, 1109 "--start-insert=%s" % self._WDIFF_ADD, 1110 "--end-insert=%s" % self._WDIFF_END, 1111 actual_filename, 1112 expected_filename] 1113 1114 @staticmethod 1115 def _handle_wdiff_error(script_error): 1116 # Exit 1 means the files differed, any other exit code is an error. 1117 if script_error.exit_code != 1: 1118 raise script_error 1119 1120 def _run_wdiff(self, actual_filename, expected_filename): 1121 """Runs wdiff and may throw exceptions. 1122 This is mostly a hook for unit testing.""" 1123 # Diffs are treated as binary as they may include multiple files 1124 # with conflicting encodings. Thus we do not decode the output. 1125 command = self._wdiff_command(actual_filename, expected_filename) 1126 wdiff = self._executive.run_command(command, decode_output=False, 1127 error_handler=self._handle_wdiff_error) 1128 return self._format_wdiff_output_as_html(wdiff) 1129 1130 _wdiff_error_html = "Failed to run wdiff, see error log." 1131 1132 def wdiff_text(self, actual_filename, expected_filename): 1133 """Returns a string of HTML indicating the word-level diff of the 1134 contents of the two filenames. Returns an empty string if word-level 1135 diffing isn't available.""" 1136 if not self.wdiff_available(): 1137 return "" 1138 try: 1139 # It's possible to raise a ScriptError we pass wdiff invalid paths. 1140 return self._run_wdiff(actual_filename, expected_filename) 1141 except OSError as e: 1142 if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]: 1143 # Silently ignore cases where wdiff is missing. 1144 self._wdiff_available = False 1145 return "" 1146 raise 1147 except ScriptError as e: 1148 _log.error("Failed to run wdiff: %s" % e) 1149 self._wdiff_available = False 1150 return self._wdiff_error_html 1151 1152 # This is a class variable so we can test error output easily. 1153 _pretty_patch_error_html = "Failed to run PrettyPatch, see error log." 1154 1155 def pretty_patch_text(self, diff_path): 1156 if self._pretty_patch_available is None: 1157 self._pretty_patch_available = self.check_pretty_patch(logging=False) 1158 if not self._pretty_patch_available: 1159 return self._pretty_patch_error_html 1160 command = ("ruby", "-I", self._filesystem.dirname(self._pretty_patch_path), 1161 self._pretty_patch_path, diff_path) 1162 try: 1163 # Diffs are treated as binary (we pass decode_output=False) as they 1164 # may contain multiple files of conflicting encodings. 1165 return self._executive.run_command(command, decode_output=False) 1166 except OSError, e: 1167 # If the system is missing ruby log the error and stop trying. 1168 self._pretty_patch_available = False 1169 _log.error("Failed to run PrettyPatch (%s): %s" % (command, e)) 1170 return self._pretty_patch_error_html 1171 except ScriptError, e: 1172 # If ruby failed to run for some reason, log the command 1173 # output and stop trying. 1174 self._pretty_patch_available = False 1175 _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, e.message_with_output())) 1176 return self._pretty_patch_error_html 1177 1178 def default_configuration(self): 1179 return self._config.default_configuration() 1180 1181 # 1182 # PROTECTED ROUTINES 1183 # 1184 # The routines below should only be called by routines in this class 1185 # or any of its subclasses. 1186 # 1187 1188 def _uses_apache(self): 1189 return True 1190 1191 # FIXME: This does not belong on the port object. 1192 @memoized 1193 def _path_to_apache(self): 1194 """Returns the full path to the apache binary. 1195 1196 This is needed only by ports that use the apache_http_server module.""" 1197 raise NotImplementedError('Port._path_to_apache') 1198 1199 # FIXME: This belongs on some platform abstraction instead of Port. 1200 def _is_redhat_based(self): 1201 return self._filesystem.exists('/etc/redhat-release') 1202 1203 def _is_debian_based(self): 1204 return self._filesystem.exists('/etc/debian_version') 1205 1206 def _apache_version(self): 1207 config = self._executive.run_command([self._path_to_apache(), '-v']) 1208 return re.sub(r'(?:.|\n)*Server version: Apache/(\d+\.\d+)(?:.|\n)*', r'\1', config) 1209 1210 # We pass sys_platform into this method to make it easy to unit test. 1211 def _apache_config_file_name_for_platform(self, sys_platform): 1212 if sys_platform == 'cygwin': 1213 return 'cygwin-httpd.conf' # CYGWIN is the only platform to still use Apache 1.3. 1214 if sys_platform.startswith('linux'): 1215 if self._is_redhat_based(): 1216 return 'fedora-httpd-' + self._apache_version() + '.conf' 1217 if self._is_debian_based(): 1218 return 'apache2-debian-httpd.conf' 1219 # All platforms use apache2 except for CYGWIN (and Mac OS X Tiger and prior, which we no longer support). 1220 return "apache2-httpd.conf" 1221 1222 def _path_to_apache_config_file(self): 1223 """Returns the full path to the apache configuration file. 1224 1225 If the WEBKIT_HTTP_SERVER_CONF_PATH environment variable is set, its 1226 contents will be used instead. 1227 1228 This is needed only by ports that use the apache_http_server module.""" 1229 config_file_from_env = os.environ.get('WEBKIT_HTTP_SERVER_CONF_PATH') 1230 if config_file_from_env: 1231 if not self._filesystem.exists(config_file_from_env): 1232 raise IOError('%s was not found on the system' % config_file_from_env) 1233 return config_file_from_env 1234 1235 config_file_name = self._apache_config_file_name_for_platform(sys.platform) 1236 return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', config_file_name) 1237 1238 def _build_path(self, *comps): 1239 build_directory = self.get_option('build_directory') 1240 if build_directory: 1241 root_directory = self._filesystem.join(build_directory, self.get_option('configuration')) 1242 else: 1243 root_directory = self._config.build_directory(self.get_option('configuration')) 1244 return self._filesystem.join(self._filesystem.abspath(root_directory), *comps) 1245 1246 def _path_to_driver(self, configuration=None): 1247 """Returns the full path to the test driver.""" 1248 return self._build_path(self.driver_name()) 1249 1250 def _path_to_webcore_library(self): 1251 """Returns the full path to a built copy of WebCore.""" 1252 return None 1253 1254 def _path_to_helper(self): 1255 """Returns the full path to the layout_test_helper binary, which 1256 is used to help configure the system for the test run, or None 1257 if no helper is needed. 1258 1259 This is likely only used by start/stop_helper().""" 1260 return None 1261 1262 def _path_to_image_diff(self): 1263 """Returns the full path to the image_diff binary, or None if it is not available. 1264 1265 This is likely used only by diff_image()""" 1266 return self._build_path('image_diff') 1267 1268 def _path_to_lighttpd(self): 1269 """Returns the path to the LigHTTPd binary. 1270 1271 This is needed only by ports that use the http_server.py module.""" 1272 raise NotImplementedError('Port._path_to_lighttpd') 1273 1274 def _path_to_lighttpd_modules(self): 1275 """Returns the path to the LigHTTPd modules directory. 1276 1277 This is needed only by ports that use the http_server.py module.""" 1278 raise NotImplementedError('Port._path_to_lighttpd_modules') 1279 1280 def _path_to_lighttpd_php(self): 1281 """Returns the path to the LigHTTPd PHP executable. 1282 1283 This is needed only by ports that use the http_server.py module.""" 1284 raise NotImplementedError('Port._path_to_lighttpd_php') 1285 1286 @memoized 1287 def _path_to_wdiff(self): 1288 """Returns the full path to the wdiff binary, or None if it is not available. 1289 1290 This is likely used only by wdiff_text()""" 1291 for path in ("/usr/bin/wdiff", "/usr/bin/dwdiff"): 1292 if self._filesystem.exists(path): 1293 return path 1294 return None 1295 1296 def _webkit_baseline_path(self, platform): 1297 """Return the full path to the top of the baseline tree for a 1298 given platform.""" 1299 return self._filesystem.join(self.layout_tests_dir(), 'platform', platform) 1300 1301 # FIXME: Belongs on a Platform object. 1302 def _generate_all_test_configurations(self): 1303 """Generates a list of TestConfiguration instances, representing configurations 1304 for a platform across all OSes, architectures, build and graphics types.""" 1305 raise NotImplementedError('Port._generate_all_test_configurations') 1306 1307 def _driver_class(self): 1308 """Returns the port's driver implementation.""" 1309 return driver.Driver 1310 1311 def _get_crash_log(self, name, pid, stdout, stderr, newer_than): 1312 name_str = name or '<unknown process name>' 1313 pid_str = str(pid or '<unknown>') 1314 stdout_lines = (stdout or '<empty>').decode('utf8', 'replace').splitlines() 1315 stderr_lines = (stderr or '<empty>').decode('utf8', 'replace').splitlines() 1316 return (stderr, 'crash log for %s (pid %s):\n%s\n%s\n' % (name_str, pid_str, 1317 '\n'.join(('STDOUT: ' + l) for l in stdout_lines), 1318 '\n'.join(('STDERR: ' + l) for l in stderr_lines))) 1319 1320 def look_for_new_crash_logs(self, crashed_processes, start_time): 1321 pass 1322 1323 def look_for_new_samples(self, unresponsive_processes, start_time): 1324 pass 1325 1326 def sample_process(self, name, pid): 1327 pass 1328 1329 def virtual_test_suites(self): 1330 return [] 1331 1332 @memoized 1333 def populated_virtual_test_suites(self): 1334 suites = self.virtual_test_suites() 1335 1336 # Sanity-check the suites to make sure they don't point to other suites. 1337 suite_dirs = [suite.name for suite in suites] 1338 for suite in suites: 1339 assert suite.base not in suite_dirs 1340 1341 for suite in suites: 1342 base_tests = self._real_tests([suite.base]) 1343 suite.tests = {} 1344 for test in base_tests: 1345 suite.tests[test.replace(suite.base, suite.name, 1)] = test 1346 return suites 1347 1348 def _virtual_tests(self, paths, suites): 1349 virtual_tests = list() 1350 for suite in suites: 1351 if paths: 1352 for test in suite.tests: 1353 if any(test.startswith(p) for p in paths): 1354 virtual_tests.append(test) 1355 else: 1356 virtual_tests.extend(suite.tests.keys()) 1357 return virtual_tests 1358 1359 def is_virtual_test(self, test_name): 1360 return bool(self.lookup_virtual_suite(test_name)) 1361 1362 def lookup_virtual_suite(self, test_name): 1363 for suite in self.populated_virtual_test_suites(): 1364 if test_name.startswith(suite.name): 1365 return suite 1366 return None 1367 1368 def lookup_virtual_test_base(self, test_name): 1369 suite = self.lookup_virtual_suite(test_name) 1370 if not suite: 1371 return None 1372 return test_name.replace(suite.name, suite.base, 1) 1373 1374 def lookup_virtual_test_args(self, test_name): 1375 for suite in self.populated_virtual_test_suites(): 1376 if test_name.startswith(suite.name): 1377 return suite.args 1378 return [] 1379 1380 def should_run_as_pixel_test(self, test_input): 1381 if not self._options.pixel_tests: 1382 return False 1383 if self._options.pixel_test_directories: 1384 return any(test_input.test_name.startswith(directory) for directory in self._options.pixel_test_directories) 1385 return True 1386 1387 def _modules_to_search_for_symbols(self): 1388 path = self._path_to_webcore_library() 1389 if path: 1390 return [path] 1391 return [] 1392 1393 def _symbols_string(self): 1394 symbols = '' 1395 for path_to_module in self._modules_to_search_for_symbols(): 1396 try: 1397 symbols += self._executive.run_command(['nm', path_to_module], error_handler=self._executive.ignore_error) 1398 except OSError, e: 1399 _log.warn("Failed to run nm: %s. Can't determine supported features correctly." % e) 1400 return symbols 1401 1402 # Ports which use compile-time feature detection should define this method and return 1403 # a dictionary mapping from symbol substrings to possibly disabled test directories. 1404 # When the symbol substrings are not matched, the directories will be skipped. 1405 # If ports don't ever enable certain features, then those directories can just be 1406 # in the Skipped list instead of compile-time-checked here. 1407 def _missing_symbol_to_skipped_tests(self): 1408 if self.PORT_HAS_AUDIO_CODECS_BUILT_IN: 1409 return {} 1410 else: 1411 return { 1412 "ff_mp3_decoder": ["webaudio/codec-tests/mp3"], 1413 "ff_aac_decoder": ["webaudio/codec-tests/aac"], 1414 } 1415 1416 def _has_test_in_directories(self, directory_lists, test_list): 1417 if not test_list: 1418 return False 1419 1420 directories = itertools.chain.from_iterable(directory_lists) 1421 for directory, test in itertools.product(directories, test_list): 1422 if test.startswith(directory): 1423 return True 1424 return False 1425 1426 def _skipped_tests_for_unsupported_features(self, test_list): 1427 # Only check the symbols of there are tests in the test_list that might get skipped. 1428 # This is a performance optimization to avoid the calling nm. 1429 # Runtime feature detection not supported, fallback to static detection: 1430 # Disable any tests for symbols missing from the executable or libraries. 1431 if self._has_test_in_directories(self._missing_symbol_to_skipped_tests().values(), test_list): 1432 symbols_string = self._symbols_string() 1433 if symbols_string is not None: 1434 return reduce(operator.add, [directories for symbol_substring, directories in self._missing_symbol_to_skipped_tests().items() if symbol_substring not in symbols_string], []) 1435 return [] 1436 1437 def _convert_path(self, path): 1438 """Handles filename conversion for subprocess command line args.""" 1439 # See note above in diff_image() for why we need this. 1440 if sys.platform == 'cygwin': 1441 return cygpath(path) 1442 return path 1443 1444 1445 class VirtualTestSuite(object): 1446 def __init__(self, name, base, args, tests=None): 1447 self.name = name 1448 self.base = base 1449 self.args = args 1450 self.tests = tests or set() 1451 1452 def __repr__(self): 1453 return "VirtualTestSuite('%s', '%s', %s)" % (self.name, self.base, self.args) 1454