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