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