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