1 #!/usr/bin/env python 2 # Copyright (C) 2010 Google Inc. All rights reserved. 3 # 4 # Redistribution and use in source and binary forms, with or without 5 # modification, are permitted provided that the following conditions are 6 # met: 7 # 8 # * Redistributions of source code must retain the above copyright 9 # notice, this list of conditions and the following disclaimer. 10 # * Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following disclaimer 12 # in the documentation and/or other materials provided with the 13 # distribution. 14 # * Neither the Google name nor the names of its 15 # contributors may be used to endorse or promote products derived from 16 # this software without specific prior written permission. 17 # 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30 """Abstract base class of Port-specific entrypoints for the layout tests 31 test infrastructure (the Port and Driver classes).""" 32 33 from __future__ import with_statement 34 35 import cgi 36 import difflib 37 import errno 38 import os 39 import shlex 40 import sys 41 import time 42 43 # Handle Python < 2.6 where multiprocessing isn't available. 44 try: 45 import multiprocessing 46 except ImportError: 47 multiprocessing = None 48 49 from webkitpy.common import system 50 from webkitpy.common.system import filesystem 51 from webkitpy.common.system import logutils 52 from webkitpy.common.system import path 53 from webkitpy.common.system.executive import Executive, ScriptError 54 from webkitpy.common.system.user import User 55 from webkitpy.layout_tests import read_checksum_from_png 56 from webkitpy.layout_tests.port import apache_http_server 57 from webkitpy.layout_tests.port import config as port_config 58 from webkitpy.layout_tests.port import http_lock 59 from webkitpy.layout_tests.port import http_server 60 from webkitpy.layout_tests.port import test_files 61 from webkitpy.layout_tests.port import websocket_server 62 63 _log = logutils.get_logger(__file__) 64 65 66 class DummyOptions(object): 67 """Fake implementation of optparse.Values. Cloned from 68 webkitpy.tool.mocktool.MockOptions. 69 70 """ 71 72 def __init__(self, **kwargs): 73 # The caller can set option values using keyword arguments. We don't 74 # set any values by default because we don't know how this 75 # object will be used. Generally speaking unit tests should 76 # subclass this or provider wrapper functions that set a common 77 # set of options. 78 for key, value in kwargs.items(): 79 self.__dict__[key] = value 80 81 82 # FIXME: This class should merge with webkitpy.webkit_port at some point. 83 class Port(object): 84 """Abstract class for Port-specific hooks for the layout_test package.""" 85 86 def __init__(self, port_name=None, options=None, 87 executive=None, 88 user=None, 89 filesystem=None, 90 config=None, 91 **kwargs): 92 self._name = port_name 93 94 # These are default values that should be overridden in a subclasses. 95 # FIXME: These should really be passed in. 96 self._operating_system = 'mac' 97 self._version = '' 98 self._architecture = 'x86' 99 self._graphics_type = 'cpu' 100 101 # FIXME: Ideally we'd have a package-wide way to get a 102 # well-formed options object that had all of the necessary 103 # options defined on it. 104 self._options = options or DummyOptions() 105 106 self._executive = executive or Executive() 107 self._user = user or User() 108 self._filesystem = filesystem or system.filesystem.FileSystem() 109 self._config = config or port_config.Config(self._executive, self._filesystem) 110 111 self._helper = None 112 self._http_server = None 113 self._webkit_base_dir = None 114 self._websocket_server = None 115 self._http_lock = None 116 117 # Python's Popen has a bug that causes any pipes opened to a 118 # process that can't be executed to be leaked. Since this 119 # code is specifically designed to tolerate exec failures 120 # to gracefully handle cases where wdiff is not installed, 121 # the bug results in a massive file descriptor leak. As a 122 # workaround, if an exec failure is ever experienced for 123 # wdiff, assume it's not available. This will leak one 124 # file descriptor but that's better than leaking each time 125 # wdiff would be run. 126 # 127 # http://mail.python.org/pipermail/python-list/ 128 # 2008-August/505753.html 129 # http://bugs.python.org/issue3210 130 self._wdiff_available = True 131 132 # FIXME: prettypatch.py knows this path, why is it copied here? 133 self._pretty_patch_path = self.path_from_webkit_base("Websites", 134 "bugs.webkit.org", "PrettyPatch", "prettify.rb") 135 self._pretty_patch_available = None 136 137 if not hasattr(self._options, 'configuration') or self._options.configuration is None: 138 self._options.configuration = self.default_configuration() 139 self._test_configuration = None 140 self._multiprocessing_is_available = (multiprocessing is not None) 141 self._results_directory = None 142 143 def wdiff_available(self): 144 return bool(self._wdiff_available) 145 146 def pretty_patch_available(self): 147 return bool(self._pretty_patch_available) 148 149 def default_child_processes(self): 150 """Return the number of DumpRenderTree instances to use for this 151 port.""" 152 return self._executive.cpu_count() 153 154 def default_worker_model(self): 155 if self._multiprocessing_is_available: 156 return 'processes' 157 return 'threads' 158 159 def baseline_path(self): 160 """Return the absolute path to the directory to store new baselines 161 in for this port.""" 162 raise NotImplementedError('Port.baseline_path') 163 164 def baseline_search_path(self): 165 """Return a list of absolute paths to directories to search under for 166 baselines. The directories are searched in order.""" 167 raise NotImplementedError('Port.baseline_search_path') 168 169 def check_build(self, needs_http): 170 """This routine is used to ensure that the build is up to date 171 and all the needed binaries are present.""" 172 raise NotImplementedError('Port.check_build') 173 174 def check_sys_deps(self, needs_http): 175 """If the port needs to do some runtime checks to ensure that the 176 tests can be run successfully, it should override this routine. 177 This step can be skipped with --nocheck-sys-deps. 178 179 Returns whether the system is properly configured.""" 180 return True 181 182 def check_image_diff(self, override_step=None, logging=True): 183 """This routine is used to check whether image_diff binary exists.""" 184 raise NotImplementedError('Port.check_image_diff') 185 186 def check_pretty_patch(self, logging=True): 187 """Checks whether we can use the PrettyPatch ruby script.""" 188 # check if Ruby is installed 189 try: 190 result = self._executive.run_command(['ruby', '--version']) 191 except OSError, e: 192 if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]: 193 if logging: 194 _log.error("Ruby is not installed; can't generate pretty patches.") 195 _log.error('') 196 return False 197 198 if not self.path_exists(self._pretty_patch_path): 199 if logging: 200 _log.error("Unable to find %s; can't generate pretty patches." % self._pretty_patch_path) 201 _log.error('') 202 return False 203 204 return True 205 206 def compare_text(self, expected_text, actual_text): 207 """Return whether or not the two strings are *not* equal. This 208 routine is used to diff text output. 209 210 While this is a generic routine, we include it in the Port 211 interface so that it can be overriden for testing purposes.""" 212 return expected_text != actual_text 213 214 def compare_audio(self, expected_audio, actual_audio): 215 """Return whether the two audio files are *not* equal.""" 216 return expected_audio != actual_audio 217 218 def diff_image(self, expected_contents, actual_contents, 219 diff_filename=None, tolerance=0): 220 """Compare two images and produce a delta image file. 221 222 Return True if the two images are different, False if they are the same. 223 Also produce a delta image of the two images and write that into 224 |diff_filename| if it is not None. 225 226 |tolerance| should be a percentage value (0.0 - 100.0). 227 If it is omitted, the port default tolerance value is used. 228 229 """ 230 raise NotImplementedError('Port.diff_image') 231 232 233 def diff_text(self, expected_text, actual_text, 234 expected_filename, actual_filename): 235 """Returns a string containing the diff of the two text strings 236 in 'unified diff' format. 237 238 While this is a generic routine, we include it in the Port 239 interface so that it can be overriden for testing purposes.""" 240 241 # The filenames show up in the diff output, make sure they're 242 # raw bytes and not unicode, so that they don't trigger join() 243 # trying to decode the input. 244 def to_raw_bytes(str): 245 if isinstance(str, unicode): 246 return str.encode('utf-8') 247 return str 248 expected_filename = to_raw_bytes(expected_filename) 249 actual_filename = to_raw_bytes(actual_filename) 250 diff = difflib.unified_diff(expected_text.splitlines(True), 251 actual_text.splitlines(True), 252 expected_filename, 253 actual_filename) 254 return ''.join(diff) 255 256 def driver_name(self): 257 """Returns the name of the actual binary that is performing the test, 258 so that it can be referred to in log messages. In most cases this 259 will be DumpRenderTree, but if a port uses a binary with a different 260 name, it can be overridden here.""" 261 return "DumpRenderTree" 262 263 def expected_baselines(self, filename, suffix, all_baselines=False): 264 """Given a test name, finds where the baseline results are located. 265 266 Args: 267 filename: absolute filename to test file 268 suffix: file suffix of the expected results, including dot; e.g. 269 '.txt' or '.png'. This should not be None, but may be an empty 270 string. 271 all_baselines: If True, return an ordered list of all baseline paths 272 for the given platform. If False, return only the first one. 273 Returns 274 a list of ( platform_dir, results_filename ), where 275 platform_dir - abs path to the top of the results tree (or test 276 tree) 277 results_filename - relative path from top of tree to the results 278 file 279 (port.join() of the two gives you the full path to the file, 280 unless None was returned.) 281 Return values will be in the format appropriate for the current 282 platform (e.g., "\\" for path separators on Windows). If the results 283 file is not found, then None will be returned for the directory, 284 but the expected relative pathname will still be returned. 285 286 This routine is generic but lives here since it is used in 287 conjunction with the other baseline and filename routines that are 288 platform specific. 289 """ 290 testname = self._filesystem.splitext(self.relative_test_filename(filename))[0] 291 292 baseline_filename = testname + '-expected' + suffix 293 294 baseline_search_path = self.get_option('additional_platform_directory', []) + self.baseline_search_path() 295 296 baselines = [] 297 for platform_dir in baseline_search_path: 298 if self.path_exists(self._filesystem.join(platform_dir, 299 baseline_filename)): 300 baselines.append((platform_dir, baseline_filename)) 301 302 if not all_baselines and baselines: 303 return baselines 304 305 # If it wasn't found in a platform directory, return the expected 306 # result in the test directory, even if no such file actually exists. 307 platform_dir = self.layout_tests_dir() 308 if self.path_exists(self._filesystem.join(platform_dir, 309 baseline_filename)): 310 baselines.append((platform_dir, baseline_filename)) 311 312 if baselines: 313 return baselines 314 315 return [(None, baseline_filename)] 316 317 def expected_filename(self, filename, suffix): 318 """Given a test name, returns an absolute path to its expected results. 319 320 If no expected results are found in any of the searched directories, 321 the directory in which the test itself is located will be returned. 322 The return value is in the format appropriate for the platform 323 (e.g., "\\" for path separators on windows). 324 325 Args: 326 filename: absolute filename to test file 327 suffix: file suffix of the expected results, including dot; e.g. '.txt' 328 or '.png'. This should not be None, but may be an empty string. 329 platform: the most-specific directory name to use to build the 330 search list of directories, e.g., 'chromium-win', or 331 'chromium-mac-leopard' (we follow the WebKit format) 332 333 This routine is generic but is implemented here to live alongside 334 the other baseline and filename manipulation routines. 335 """ 336 platform_dir, baseline_filename = self.expected_baselines( 337 filename, suffix)[0] 338 if platform_dir: 339 return self._filesystem.join(platform_dir, baseline_filename) 340 return self._filesystem.join(self.layout_tests_dir(), baseline_filename) 341 342 def expected_checksum(self, test): 343 """Returns the checksum of the image we expect the test to produce, or None if it is a text-only test.""" 344 png_path = self.expected_filename(test, '.png') 345 checksum_path = self._filesystem.splitext(png_path)[0] + '.checksum' 346 347 if self.path_exists(checksum_path): 348 return self._filesystem.read_binary_file(checksum_path) 349 350 if self.path_exists(png_path): 351 with self._filesystem.open_binary_file_for_reading(png_path) as filehandle: 352 return read_checksum_from_png.read_checksum(filehandle) 353 354 return None 355 356 def expected_image(self, test): 357 """Returns the image we expect the test to produce.""" 358 path = self.expected_filename(test, '.png') 359 if not self.path_exists(path): 360 return None 361 return self._filesystem.read_binary_file(path) 362 363 def expected_audio(self, test): 364 path = self.expected_filename(test, '.wav') 365 if not self.path_exists(path): 366 return None 367 return self._filesystem.read_binary_file(path) 368 369 def expected_text(self, test): 370 """Returns the text output we expect the test to produce, or None 371 if we don't expect there to be any text output. 372 End-of-line characters are normalized to '\n'.""" 373 # FIXME: DRT output is actually utf-8, but since we don't decode the 374 # output from DRT (instead treating it as a binary string), we read the 375 # baselines as a binary string, too. 376 path = self.expected_filename(test, '.txt') 377 if not self.path_exists(path): 378 return None 379 text = self._filesystem.read_binary_file(path) 380 return text.replace("\r\n", "\n") 381 382 def reftest_expected_filename(self, filename): 383 """Return the filename of reference we expect the test matches.""" 384 return self.expected_filename(filename, '.html') 385 386 def reftest_expected_mismatch_filename(self, filename): 387 """Return the filename of reference we don't expect the test matches.""" 388 return self.expected_filename(filename, '-mismatch.html') 389 390 def filename_to_uri(self, filename): 391 """Convert a test file (which is an absolute path) to a URI.""" 392 LAYOUTTEST_HTTP_DIR = "http/tests/" 393 LAYOUTTEST_WEBSOCKET_DIR = "http/tests/websocket/tests/" 394 395 relative_path = self.relative_test_filename(filename) 396 port = None 397 use_ssl = False 398 399 if (relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR) 400 or relative_path.startswith(LAYOUTTEST_HTTP_DIR)): 401 relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):] 402 port = 8000 403 404 # Make http/tests/local run as local files. This is to mimic the 405 # logic in run-webkit-tests. 406 # 407 # TODO(dpranke): remove the SSL reference? 408 if (port and not relative_path.startswith("local/")): 409 if relative_path.startswith("ssl/"): 410 port += 443 411 protocol = "https" 412 else: 413 protocol = "http" 414 return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path) 415 416 return path.abspath_to_uri(self._filesystem.abspath(filename)) 417 418 def tests(self, paths): 419 """Return the list of tests found (relative to layout_tests_dir().""" 420 return test_files.find(self, paths) 421 422 def test_dirs(self): 423 """Returns the list of top-level test directories. 424 425 Used by --clobber-old-results.""" 426 layout_tests_dir = self.layout_tests_dir() 427 return filter(lambda x: self._filesystem.isdir(self._filesystem.join(layout_tests_dir, x)), 428 self._filesystem.listdir(layout_tests_dir)) 429 430 def path_isdir(self, path): 431 """Return True if the path refers to a directory of tests.""" 432 # Used by test_expectations.py to apply rules to whole directories. 433 return self._filesystem.isdir(path) 434 435 def path_exists(self, path): 436 """Return True if the path refers to an existing test or baseline.""" 437 # Used by test_expectations.py to determine if an entry refers to a 438 # valid test and by printing.py to determine if baselines exist. 439 return self._filesystem.exists(path) 440 441 def driver_cmd_line(self): 442 """Prints the DRT command line that will be used.""" 443 driver = self.create_driver(0) 444 return driver.cmd_line() 445 446 def update_baseline(self, path, data): 447 """Updates the baseline for a test. 448 449 Args: 450 path: the actual path to use for baseline, not the path to 451 the test. This function is used to update either generic or 452 platform-specific baselines, but we can't infer which here. 453 data: contents of the baseline. 454 """ 455 self._filesystem.write_binary_file(path, data) 456 457 def uri_to_test_name(self, uri): 458 """Return the base layout test name for a given URI. 459 460 This returns the test name for a given URI, e.g., if you passed in 461 "file:///src/LayoutTests/fast/html/keygen.html" it would return 462 "fast/html/keygen.html". 463 464 """ 465 test = uri 466 if uri.startswith("file:///"): 467 prefix = path.abspath_to_uri(self.layout_tests_dir()) + "/" 468 return test[len(prefix):] 469 470 if uri.startswith("http://127.0.0.1:8880/"): 471 # websocket tests 472 return test.replace('http://127.0.0.1:8880/', '') 473 474 if uri.startswith("http://"): 475 # regular HTTP test 476 return test.replace('http://127.0.0.1:8000/', 'http/tests/') 477 478 if uri.startswith("https://"): 479 return test.replace('https://127.0.0.1:8443/', 'http/tests/') 480 481 raise NotImplementedError('unknown url type: %s' % uri) 482 483 def layout_tests_dir(self): 484 """Return the absolute path to the top of the LayoutTests directory.""" 485 return self.path_from_webkit_base('LayoutTests') 486 487 def skips_layout_test(self, test_name): 488 """Figures out if the givent test is being skipped or not. 489 490 Test categories are handled as well.""" 491 for test_or_category in self.skipped_layout_tests(): 492 if test_or_category == test_name: 493 return True 494 category = self._filesystem.join(self.layout_tests_dir(), 495 test_or_category) 496 if (self._filesystem.isdir(category) and 497 test_name.startswith(test_or_category)): 498 return True 499 return False 500 501 def maybe_make_directory(self, *path): 502 """Creates the specified directory if it doesn't already exist.""" 503 self._filesystem.maybe_make_directory(*path) 504 505 def name(self): 506 """Return the name of the port (e.g., 'mac', 'chromium-win-xp').""" 507 return self._name 508 509 def operating_system(self): 510 return self._operating_system 511 512 def version(self): 513 """Returns a string indicating the version of a given platform, e.g. 514 'leopard' or 'xp'. 515 516 This is used to help identify the exact port when parsing test 517 expectations, determining search paths, and logging information.""" 518 return self._version 519 520 def graphics_type(self): 521 """Returns whether the port uses accelerated graphics ('gpu') or not 522 ('cpu').""" 523 return self._graphics_type 524 525 def architecture(self): 526 return self._architecture 527 528 def real_name(self): 529 """Returns the actual name of the port, not the delegate's.""" 530 return self.name() 531 532 def get_option(self, name, default_value=None): 533 # FIXME: Eventually we should not have to do a test for 534 # hasattr(), and we should be able to just do 535 # self.options.value. See additional FIXME in the constructor. 536 if hasattr(self._options, name): 537 return getattr(self._options, name) 538 return default_value 539 540 def set_option_default(self, name, default_value): 541 if not hasattr(self._options, name): 542 return setattr(self._options, name, default_value) 543 544 def path_from_webkit_base(self, *comps): 545 """Returns the full path to path made by joining the top of the 546 WebKit source tree and the list of path components in |*comps|.""" 547 return self._config.path_from_webkit_base(*comps) 548 549 def script_path(self, script_name): 550 return self._config.script_path(script_name) 551 552 def script_shell_command(self, script_name): 553 return self._config.script_shell_command(script_name) 554 555 def path_to_test_expectations_file(self): 556 """Update the test expectations to the passed-in string. 557 558 This is used by the rebaselining tool. Raises NotImplementedError 559 if the port does not use expectations files.""" 560 raise NotImplementedError('Port.path_to_test_expectations_file') 561 562 def relative_test_filename(self, filename): 563 """Relative unix-style path for a filename under the LayoutTests 564 directory. Filenames outside the LayoutTests directory should raise 565 an error.""" 566 # FIXME: On Windows, does this return test_names with forward slashes, 567 # or windows-style relative paths? 568 assert filename.startswith(self.layout_tests_dir()), "%s did not start with %s" % (filename, self.layout_tests_dir()) 569 return filename[len(self.layout_tests_dir()) + 1:] 570 571 def abspath_for_test(self, test_name): 572 """Returns the full path to the file for a given test name. This is the 573 inverse of relative_test_filename().""" 574 return self._filesystem.normpath(self._filesystem.join(self.layout_tests_dir(), test_name)) 575 576 def results_directory(self): 577 """Absolute path to the place to store the test results (uses --results-directory).""" 578 if not self._results_directory: 579 option_val = self.get_option('results_directory') or self.default_results_directory() 580 self._results_directory = self._filesystem.abspath(option_val) 581 return self._results_directory 582 583 def default_results_directory(self): 584 """Absolute path to the default place to store the test results.""" 585 raise NotImplementedError() 586 587 def setup_test_run(self): 588 """Perform port-specific work at the beginning of a test run.""" 589 pass 590 591 def setup_environ_for_server(self): 592 """Perform port-specific work at the beginning of a server launch. 593 594 Returns: 595 Operating-system's environment. 596 """ 597 return os.environ.copy() 598 599 def show_results_html_file(self, results_filename): 600 """This routine should display the HTML file pointed at by 601 results_filename in a users' browser.""" 602 return self._user.open_url(results_filename) 603 604 def create_driver(self, worker_number): 605 """Return a newly created base.Driver subclass for starting/stopping 606 the test driver.""" 607 raise NotImplementedError('Port.create_driver') 608 609 def start_helper(self): 610 """If a port needs to reconfigure graphics settings or do other 611 things to ensure a known test configuration, it should override this 612 method.""" 613 pass 614 615 def start_http_server(self): 616 """Start a web server if it is available. Do nothing if 617 it isn't. This routine is allowed to (and may) fail if a server 618 is already running.""" 619 if self.get_option('use_apache'): 620 self._http_server = apache_http_server.LayoutTestApacheHttpd(self, 621 self.results_directory()) 622 else: 623 self._http_server = http_server.Lighttpd(self, self.results_directory()) 624 self._http_server.start() 625 626 def start_websocket_server(self): 627 """Start a websocket server if it is available. Do nothing if 628 it isn't. This routine is allowed to (and may) fail if a server 629 is already running.""" 630 self._websocket_server = websocket_server.PyWebSocket(self, self.results_directory()) 631 self._websocket_server.start() 632 633 def acquire_http_lock(self): 634 self._http_lock = http_lock.HttpLock(None) 635 self._http_lock.wait_for_httpd_lock() 636 637 def stop_helper(self): 638 """Shut down the test helper if it is running. Do nothing if 639 it isn't, or it isn't available. If a port overrides start_helper() 640 it must override this routine as well.""" 641 pass 642 643 def stop_http_server(self): 644 """Shut down the http server if it is running. Do nothing if 645 it isn't, or it isn't available.""" 646 if self._http_server: 647 self._http_server.stop() 648 649 def stop_websocket_server(self): 650 """Shut down the websocket server if it is running. Do nothing if 651 it isn't, or it isn't available.""" 652 if self._websocket_server: 653 self._websocket_server.stop() 654 655 def release_http_lock(self): 656 if self._http_lock: 657 self._http_lock.cleanup_http_lock() 658 659 # 660 # TEST EXPECTATION-RELATED METHODS 661 # 662 663 def test_configuration(self): 664 """Returns the current TestConfiguration for the port.""" 665 if not self._test_configuration: 666 self._test_configuration = TestConfiguration(self) 667 return self._test_configuration 668 669 def all_test_configurations(self): 670 return self.test_configuration().all_test_configurations() 671 672 def all_baseline_variants(self): 673 """Returns a list of platform names sufficient to cover all the baselines. 674 675 The list should be sorted so that a later platform will reuse 676 an earlier platform's baselines if they are the same (e.g., 677 'snowleopard' should precede 'leopard').""" 678 raise NotImplementedError 679 680 def test_expectations(self): 681 """Returns the test expectations for this port. 682 683 Basically this string should contain the equivalent of a 684 test_expectations file. See test_expectations.py for more details.""" 685 return self._filesystem.read_text_file(self.path_to_test_expectations_file()) 686 687 def test_expectations_overrides(self): 688 """Returns an optional set of overrides for the test_expectations. 689 690 This is used by ports that have code in two repositories, and where 691 it is possible that you might need "downstream" expectations that 692 temporarily override the "upstream" expectations until the port can 693 sync up the two repos.""" 694 return None 695 696 def test_repository_paths(self): 697 """Returns a list of (repository_name, repository_path) tuples 698 of its depending code base. By default it returns a list that only 699 contains a ('webkit', <webkitRepossitoryPath>) tuple. 700 """ 701 return [('webkit', self.layout_tests_dir())] 702 703 704 _WDIFF_DEL = '##WDIFF_DEL##' 705 _WDIFF_ADD = '##WDIFF_ADD##' 706 _WDIFF_END = '##WDIFF_END##' 707 708 def _format_wdiff_output_as_html(self, wdiff): 709 wdiff = cgi.escape(wdiff) 710 wdiff = wdiff.replace(self._WDIFF_DEL, "<span class=del>") 711 wdiff = wdiff.replace(self._WDIFF_ADD, "<span class=add>") 712 wdiff = wdiff.replace(self._WDIFF_END, "</span>") 713 html = "<head><style>.del { background: #faa; } " 714 html += ".add { background: #afa; }</style></head>" 715 html += "<pre>%s</pre>" % wdiff 716 return html 717 718 def _wdiff_command(self, actual_filename, expected_filename): 719 executable = self._path_to_wdiff() 720 return [executable, 721 "--start-delete=%s" % self._WDIFF_DEL, 722 "--end-delete=%s" % self._WDIFF_END, 723 "--start-insert=%s" % self._WDIFF_ADD, 724 "--end-insert=%s" % self._WDIFF_END, 725 actual_filename, 726 expected_filename] 727 728 @staticmethod 729 def _handle_wdiff_error(script_error): 730 # Exit 1 means the files differed, any other exit code is an error. 731 if script_error.exit_code != 1: 732 raise script_error 733 734 def _run_wdiff(self, actual_filename, expected_filename): 735 """Runs wdiff and may throw exceptions. 736 This is mostly a hook for unit testing.""" 737 # Diffs are treated as binary as they may include multiple files 738 # with conflicting encodings. Thus we do not decode the output. 739 command = self._wdiff_command(actual_filename, expected_filename) 740 wdiff = self._executive.run_command(command, decode_output=False, 741 error_handler=self._handle_wdiff_error) 742 return self._format_wdiff_output_as_html(wdiff) 743 744 def wdiff_text(self, actual_filename, expected_filename): 745 """Returns a string of HTML indicating the word-level diff of the 746 contents of the two filenames. Returns an empty string if word-level 747 diffing isn't available.""" 748 if not self._wdiff_available: 749 return "" 750 try: 751 # It's possible to raise a ScriptError we pass wdiff invalid paths. 752 return self._run_wdiff(actual_filename, expected_filename) 753 except OSError, e: 754 if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]: 755 # Silently ignore cases where wdiff is missing. 756 self._wdiff_available = False 757 return "" 758 raise 759 760 # This is a class variable so we can test error output easily. 761 _pretty_patch_error_html = "Failed to run PrettyPatch, see error log." 762 763 def pretty_patch_text(self, diff_path): 764 if self._pretty_patch_available is None: 765 self._pretty_patch_available = self.check_pretty_patch(logging=False) 766 if not self._pretty_patch_available: 767 return self._pretty_patch_error_html 768 command = ("ruby", "-I", self._filesystem.dirname(self._pretty_patch_path), 769 self._pretty_patch_path, diff_path) 770 try: 771 # Diffs are treated as binary (we pass decode_output=False) as they 772 # may contain multiple files of conflicting encodings. 773 return self._executive.run_command(command, decode_output=False) 774 except OSError, e: 775 # If the system is missing ruby log the error and stop trying. 776 self._pretty_patch_available = False 777 _log.error("Failed to run PrettyPatch (%s): %s" % (command, e)) 778 return self._pretty_patch_error_html 779 except ScriptError, e: 780 # If ruby failed to run for some reason, log the command 781 # output and stop trying. 782 self._pretty_patch_available = False 783 _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, 784 e.message_with_output())) 785 return self._pretty_patch_error_html 786 787 def default_configuration(self): 788 return self._config.default_configuration() 789 790 # 791 # PROTECTED ROUTINES 792 # 793 # The routines below should only be called by routines in this class 794 # or any of its subclasses. 795 # 796 def _webkit_build_directory(self, args): 797 return self._config.build_directory(args[0]) 798 799 def _path_to_apache(self): 800 """Returns the full path to the apache binary. 801 802 This is needed only by ports that use the apache_http_server module.""" 803 raise NotImplementedError('Port.path_to_apache') 804 805 def _path_to_apache_config_file(self): 806 """Returns the full path to the apache binary. 807 808 This is needed only by ports that use the apache_http_server module.""" 809 raise NotImplementedError('Port.path_to_apache_config_file') 810 811 def _path_to_driver(self, configuration=None): 812 """Returns the full path to the test driver (DumpRenderTree).""" 813 raise NotImplementedError('Port._path_to_driver') 814 815 def _path_to_webcore_library(self): 816 """Returns the full path to a built copy of WebCore.""" 817 raise NotImplementedError('Port.path_to_webcore_library') 818 819 def _path_to_helper(self): 820 """Returns the full path to the layout_test_helper binary, which 821 is used to help configure the system for the test run, or None 822 if no helper is needed. 823 824 This is likely only used by start/stop_helper().""" 825 raise NotImplementedError('Port._path_to_helper') 826 827 def _path_to_image_diff(self): 828 """Returns the full path to the image_diff binary, or None if it 829 is not available. 830 831 This is likely used only by diff_image()""" 832 raise NotImplementedError('Port.path_to_image_diff') 833 834 def _path_to_lighttpd(self): 835 """Returns the path to the LigHTTPd binary. 836 837 This is needed only by ports that use the http_server.py module.""" 838 raise NotImplementedError('Port._path_to_lighttpd') 839 840 def _path_to_lighttpd_modules(self): 841 """Returns the path to the LigHTTPd modules directory. 842 843 This is needed only by ports that use the http_server.py module.""" 844 raise NotImplementedError('Port._path_to_lighttpd_modules') 845 846 def _path_to_lighttpd_php(self): 847 """Returns the path to the LigHTTPd PHP executable. 848 849 This is needed only by ports that use the http_server.py module.""" 850 raise NotImplementedError('Port._path_to_lighttpd_php') 851 852 def _path_to_wdiff(self): 853 """Returns the full path to the wdiff binary, or None if it is 854 not available. 855 856 This is likely used only by wdiff_text()""" 857 raise NotImplementedError('Port._path_to_wdiff') 858 859 def _shut_down_http_server(self, pid): 860 """Forcefully and synchronously kills the web server. 861 862 This routine should only be called from http_server.py or its 863 subclasses.""" 864 raise NotImplementedError('Port._shut_down_http_server') 865 866 def _webkit_baseline_path(self, platform): 867 """Return the full path to the top of the baseline tree for a 868 given platform.""" 869 return self._filesystem.join(self.layout_tests_dir(), 'platform', 870 platform) 871 872 873 class DriverInput(object): 874 """Holds the input parameters for a driver.""" 875 876 def __init__(self, filename, timeout, image_hash): 877 """Initializes a DriverInput object. 878 879 Args: 880 filename: Full path to the test. 881 timeout: Timeout in msecs the driver should use while running the test 882 image_hash: A image checksum which is used to avoid doing an image dump if 883 the checksums match. 884 """ 885 self.filename = filename 886 self.timeout = timeout 887 self.image_hash = image_hash 888 889 890 class DriverOutput(object): 891 """Groups information about a output from driver for easy passing of data.""" 892 893 def __init__(self, text, image, image_hash, audio, 894 crash=False, test_time=0, timeout=False, error=''): 895 """Initializes a TestOutput object. 896 897 Args: 898 text: a text output 899 image: an image output 900 image_hash: a string containing the checksum of the image 901 audio: contents of an audio stream, if any (in WAV format) 902 crash: a boolean indicating whether the driver crashed on the test 903 test_time: the time the test took to execute 904 timeout: a boolean indicating whehter the test timed out 905 error: any unexpected or additional (or error) text output 906 """ 907 self.text = text 908 self.image = image 909 self.image_hash = image_hash 910 self.audio = audio 911 self.crash = crash 912 self.test_time = test_time 913 self.timeout = timeout 914 self.error = error 915 916 917 class Driver: 918 """Abstract interface for the DumpRenderTree interface.""" 919 920 def __init__(self, port, worker_number): 921 """Initialize a Driver to subsequently run tests. 922 923 Typically this routine will spawn DumpRenderTree in a config 924 ready for subsequent input. 925 926 port - reference back to the port object. 927 worker_number - identifier for a particular worker/driver instance 928 """ 929 raise NotImplementedError('Driver.__init__') 930 931 def run_test(self, driver_input): 932 """Run a single test and return the results. 933 934 Note that it is okay if a test times out or crashes and leaves 935 the driver in an indeterminate state. The upper layers of the program 936 are responsible for cleaning up and ensuring things are okay. 937 938 Args: 939 driver_input: a DriverInput object 940 941 Returns a DriverOutput object. 942 Note that DriverOutput.image will be '' (empty string) if a test crashes. 943 """ 944 raise NotImplementedError('Driver.run_test') 945 946 # FIXME: This is static so we can test it w/o creating a Base instance. 947 @classmethod 948 def _command_wrapper(cls, wrapper_option): 949 # Hook for injecting valgrind or other runtime instrumentation, 950 # used by e.g. tools/valgrind/valgrind_tests.py. 951 wrapper = [] 952 browser_wrapper = os.environ.get("BROWSER_WRAPPER", None) 953 if browser_wrapper: 954 # FIXME: There seems to be no reason to use BROWSER_WRAPPER over --wrapper. 955 # Remove this code any time after the date listed below. 956 _log.error("BROWSER_WRAPPER is deprecated, please use --wrapper instead.") 957 _log.error("BROWSER_WRAPPER will be removed any time after June 1st 2010 and your scripts will break.") 958 wrapper += [browser_wrapper] 959 960 if wrapper_option: 961 wrapper += shlex.split(wrapper_option) 962 return wrapper 963 964 def poll(self): 965 """Returns None if the Driver is still running. Returns the returncode 966 if it has exited.""" 967 raise NotImplementedError('Driver.poll') 968 969 def stop(self): 970 raise NotImplementedError('Driver.stop') 971 972 973 class TestConfiguration(object): 974 def __init__(self, port=None, os=None, version=None, architecture=None, 975 build_type=None, graphics_type=None): 976 self.os = os or port.operating_system() 977 self.version = version or port.version() 978 self.architecture = architecture or port.architecture() 979 self.build_type = build_type or port._options.configuration.lower() 980 self.graphics_type = graphics_type or port.graphics_type() 981 982 def items(self): 983 return self.__dict__.items() 984 985 def keys(self): 986 return self.__dict__.keys() 987 988 def __str__(self): 989 return ("<%(os)s, %(version)s, %(architecture)s, %(build_type)s, %(graphics_type)s>" % 990 self.__dict__) 991 992 def __repr__(self): 993 return "TestConfig(os='%(os)s', version='%(version)s', architecture='%(architecture)s', build_type='%(build_type)s', graphics_type='%(graphics_type)s')" % self.__dict__ 994 995 def values(self): 996 """Returns the configuration values of this instance as a tuple.""" 997 return self.__dict__.values() 998 999 def all_test_configurations(self): 1000 """Returns a sequence of the TestConfigurations the port supports.""" 1001 # By default, we assume we want to test every graphics type in 1002 # every configuration on every system. 1003 test_configurations = [] 1004 for system in self.all_systems(): 1005 for build_type in self.all_build_types(): 1006 for graphics_type in self.all_graphics_types(): 1007 test_configurations.append(TestConfiguration( 1008 os=system[0], 1009 version=system[1], 1010 architecture=system[2], 1011 build_type=build_type, 1012 graphics_type=graphics_type)) 1013 return test_configurations 1014 1015 def all_systems(self): 1016 return (('mac', 'leopard', 'x86'), 1017 ('mac', 'snowleopard', 'x86'), 1018 ('win', 'xp', 'x86'), 1019 ('win', 'vista', 'x86'), 1020 ('win', 'win7', 'x86'), 1021 ('linux', 'hardy', 'x86'), 1022 ('linux', 'hardy', 'x86_64')) 1023 1024 def all_build_types(self): 1025 return ('debug', 'release') 1026 1027 def all_graphics_types(self): 1028 return ('cpu', 'gpu') 1029