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