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