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