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