Tools/Scripts: eliminate find_test_files from Port class.
[WebKit-https.git] / Tools / Scripts / webkitpy / layout_tests / port / base.py
1 #!/usr/bin/env python
2 # Copyright (C) 2010 Google 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 entrypoints for the layout tests
31 test infrastructure (the Port and Driver classes)."""
32
33 from __future__ import with_statement
34
35 import cgi
36 import difflib
37 import errno
38 import os
39 import re
40
41 from webkitpy.common.memoized import memoized
42
43
44 # Handle Python < 2.6 where multiprocessing isn't available.
45 try:
46     import multiprocessing
47 except ImportError:
48     multiprocessing = None
49
50 from webkitpy.common import find_files
51 from webkitpy.common.system import logutils
52 from webkitpy.common.system import path
53 from webkitpy.common.system.executive import ScriptError
54 from webkitpy.layout_tests import read_checksum_from_png
55 from webkitpy.layout_tests.models.test_configuration import TestConfiguration
56 from webkitpy.layout_tests.port import config as port_config
57 from webkitpy.layout_tests.port import driver
58 from webkitpy.layout_tests.port import http_lock
59 from webkitpy.layout_tests.servers import apache_http_server
60 from webkitpy.layout_tests.servers import http_server
61 from webkitpy.layout_tests.servers import websocket_server
62
63 _log = logutils.get_logger(__file__)
64
65
66 class DummyOptions(object):
67     """Fake implementation of optparse.Values. Cloned from webkitpy.tool.mocktool.MockOptions."""
68
69     def __init__(self, *args, **kwargs):
70         # The caller can set option values using keyword arguments. We don't
71         # set any values by default because we don't know how this
72         # object will be used. Generally speaking unit tests should
73         # subclass this or provider wrapper functions that set a common
74         # set of options.
75         for key, value in kwargs.items():
76             self.__dict__[key] = value
77
78
79 # FIXME: This class should merge with WebKitPort now that Chromium behaves mostly like other webkit ports.
80 class Port(object):
81     """Abstract class for Port-specific hooks for the layout_test package."""
82
83     port_name = None  # Subclasses override this
84
85     # Test names resemble unix relative paths, and use '/' as a directory separator.
86     TEST_PATH_SEPARATOR = '/'
87
88     ALL_BUILD_TYPES = ('debug', 'release')
89
90     def __init__(self, host,
91                  port_name=None, options=None,
92                  executive=None,
93                  user=None,
94                  filesystem=None,
95                  config=None,
96                  **kwargs):
97
98         # These are default values that should be overridden in a subclasses.
99         self._name = port_name or self.port_name  # Subclasses may append a -VERSION (like mac-leopard) or other qualifiers.
100         self._operating_system = 'mac'
101         self._version = ''
102         self._architecture = 'x86'
103         self._graphics_type = 'cpu'
104
105         # FIXME: Ideally we'd have a package-wide way to get a
106         # well-formed options object that had all of the necessary
107         # options defined on it.
108         self.options = options or DummyOptions()
109
110         self.host = host
111
112         # FIXME: Remove thes accessors once all callers have moved to using self.host.
113         self.executive = executive or self.host.executive
114         self.user = user or self.host.user
115         self.filesystem = filesystem or self.host.filesystem
116         self.config = config or port_config.Config(self.executive, self.filesystem)
117
118         # FIXME: Remove all of the old "protected" versions when we can.
119         self._options = self.options
120         self._executive = self.executive
121         self._filesystem = self.filesystem
122         self._user = self.user
123         self._config = self.config
124
125         self._helper = None
126         self._http_server = None
127         self._websocket_server = None
128         self._http_lock = None  # FIXME: Why does this live on the port object?
129
130         # Python's Popen has a bug that causes any pipes opened to a
131         # process that can't be executed to be leaked.  Since this
132         # code is specifically designed to tolerate exec failures
133         # to gracefully handle cases where wdiff is not installed,
134         # the bug results in a massive file descriptor leak. As a
135         # workaround, if an exec failure is ever experienced for
136         # wdiff, assume it's not available.  This will leak one
137         # file descriptor but that's better than leaking each time
138         # wdiff would be run.
139         #
140         # http://mail.python.org/pipermail/python-list/
141         #    2008-August/505753.html
142         # http://bugs.python.org/issue3210
143         self._wdiff_available = None
144
145         # FIXME: prettypatch.py knows this path, why is it copied here?
146         self._pretty_patch_path = self.path_from_webkit_base("Websites", "bugs.webkit.org", "PrettyPatch", "prettify.rb")
147         self._pretty_patch_available = None
148
149         self.set_option_default('configuration', self.default_configuration())
150         self._test_configuration = None
151         self._reftest_list = {}
152         self._multiprocessing_is_available = (multiprocessing is not None)
153         self._results_directory = None
154
155     def wdiff_available(self):
156         if self._wdiff_available is None:
157             self._wdiff_available = self.check_wdiff(logging=False)
158         return self._wdiff_available
159
160     def pretty_patch_available(self):
161         if self._pretty_patch_available is None:
162             self._pretty_patch_available = self.check_pretty_patch(logging=False)
163         return self._pretty_patch_available
164
165     def default_child_processes(self):
166         """Return the number of DumpRenderTree instances to use for this port."""
167         return self._executive.cpu_count()
168
169     def default_worker_model(self):
170         if self._multiprocessing_is_available:
171             return 'processes'
172         return 'inline'
173
174     def baseline_path(self):
175         """Return the absolute path to the directory to store new baselines in for this port."""
176         baseline_search_paths = self.get_option('additional_platform_directory', []) + self.baseline_search_path()
177         return baseline_search_paths[0]
178
179     def baseline_search_path(self):
180         """Return a list of absolute paths to directories to search under for
181         baselines. The directories are searched in order."""
182         raise NotImplementedError('Port.baseline_search_path')
183
184     def check_build(self, needs_http):
185         """This routine is used to ensure that the build is up to date
186         and all the needed binaries are present."""
187         raise NotImplementedError('Port.check_build')
188
189     def check_sys_deps(self, needs_http):
190         """If the port needs to do some runtime checks to ensure that the
191         tests can be run successfully, it should override this routine.
192         This step can be skipped with --nocheck-sys-deps.
193
194         Returns whether the system is properly configured."""
195         if needs_http:
196             return self.check_httpd()
197         return True
198
199     def check_image_diff(self, override_step=None, logging=True):
200         """This routine is used to check whether image_diff binary exists."""
201         raise NotImplementedError('Port.check_image_diff')
202
203     def check_pretty_patch(self, logging=True):
204         """Checks whether we can use the PrettyPatch ruby script."""
205         try:
206             _ = self._executive.run_command(['ruby', '--version'])
207         except OSError, e:
208             if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
209                 if logging:
210                     _log.error("Ruby is not installed; can't generate pretty patches.")
211                     _log.error('')
212                 return False
213
214         if not self._filesystem.exists(self._pretty_patch_path):
215             if logging:
216                 _log.error("Unable to find %s; can't generate pretty patches." % self._pretty_patch_path)
217                 _log.error('')
218             return False
219
220         return True
221
222     def check_wdiff(self, logging=True):
223         if not self._path_to_wdiff():
224             # Don't need to log here since this is the port choosing not to use wdiff.
225             return False
226
227         try:
228             _ = self._executive.run_command([self._path_to_wdiff(), '--help'])
229         except OSError:
230             if logging:
231                 _log.error("wdiff is not installed.")
232             return False
233
234         return True
235
236     def check_httpd(self):
237         if self._uses_apache():
238             httpd_path = self._path_to_apache()
239         else:
240             httpd_path = self._path_to_lighttpd()
241
242         try:
243             server_name = self._filesystem.basename(httpd_path)
244             env = self.setup_environ_for_server(server_name)
245             return self._executive.run_command([httpd_path, "-v"], env=env, return_exit_code=True) == 0
246         except OSError:
247             _log.error("No httpd found. Cannot run http tests.")
248             return False
249
250     def compare_text(self, expected_text, actual_text):
251         """Return whether or not the two strings are *not* equal. This
252         routine is used to diff text output.
253
254         While this is a generic routine, we include it in the Port
255         interface so that it can be overriden for testing purposes."""
256         return expected_text != actual_text
257
258     def compare_audio(self, expected_audio, actual_audio):
259         # FIXME: If we give this method a better name it won't need this docstring (e.g. are_audio_results_equal()).
260         """Return whether the two audio files are *not* equal."""
261         return expected_audio != actual_audio
262
263     def diff_image(self, expected_contents, actual_contents, tolerance=None):
264         """Compare two images and return a tuple of an image diff, and a percentage difference (0-100).
265
266         |tolerance| should be a percentage value (0.0 - 100.0).
267         If it is omitted, the port default tolerance value is used.
268         """
269         raise NotImplementedError('Port.diff_image')
270
271
272     def diff_text(self, expected_text, actual_text,
273                   expected_filename, actual_filename):
274         """Returns a string containing the diff of the two text strings
275         in 'unified diff' format.
276
277         While this is a generic routine, we include it in the Port
278         interface so that it can be overriden for testing purposes."""
279
280         # The filenames show up in the diff output, make sure they're
281         # raw bytes and not unicode, so that they don't trigger join()
282         # trying to decode the input.
283         def to_raw_bytes(string_value):
284             if isinstance(string_value, unicode):
285                 return string_value.encode('utf-8')
286             return string_value
287         expected_filename = to_raw_bytes(expected_filename)
288         actual_filename = to_raw_bytes(actual_filename)
289         diff = difflib.unified_diff(expected_text.splitlines(True),
290                                     actual_text.splitlines(True),
291                                     expected_filename,
292                                     actual_filename)
293         return ''.join(diff)
294
295     def is_crash_reporter(self, process_name):
296         return False
297
298     def check_for_leaks(self, process_name, process_pid):
299         # Subclasses should check for leaks in the running process
300         # and print any necessary warnings if leaks are found.
301         # FIXME: We should consider moving much of this logic into
302         # Executive and make it platform-specific instead of port-specific.
303         pass
304
305     def print_leaks_summary(self):
306         # Subclasses can override this to print a summary of leaks found
307         # while running the layout tests.
308         pass
309
310     def driver_name(self):
311         """Returns the name of the actual binary that is performing the test,
312         so that it can be referred to in log messages. In most cases this
313         will be DumpRenderTree, but if a port uses a binary with a different
314         name, it can be overridden here."""
315         return "DumpRenderTree"
316
317     def expected_baselines(self, test_name, suffix, all_baselines=False):
318         """Given a test name, finds where the baseline results are located.
319
320         Args:
321         test_name: name of test file (usually a relative path under LayoutTests/)
322         suffix: file suffix of the expected results, including dot; e.g.
323             '.txt' or '.png'.  This should not be None, but may be an empty
324             string.
325         all_baselines: If True, return an ordered list of all baseline paths
326             for the given platform. If False, return only the first one.
327         Returns
328         a list of ( platform_dir, results_filename ), where
329             platform_dir - abs path to the top of the results tree (or test
330                 tree)
331             results_filename - relative path from top of tree to the results
332                 file
333             (port.join() of the two gives you the full path to the file,
334                 unless None was returned.)
335         Return values will be in the format appropriate for the current
336         platform (e.g., "\\" for path separators on Windows). If the results
337         file is not found, then None will be returned for the directory,
338         but the expected relative pathname will still be returned.
339
340         This routine is generic but lives here since it is used in
341         conjunction with the other baseline and filename routines that are
342         platform specific.
343         """
344         baseline_filename = self._filesystem.splitext(test_name)[0] + '-expected' + suffix
345         baseline_search_path = self.get_option('additional_platform_directory', []) + self.baseline_search_path()
346
347         baselines = []
348         for platform_dir in baseline_search_path:
349             if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)):
350                 baselines.append((platform_dir, baseline_filename))
351
352             if not all_baselines and baselines:
353                 return baselines
354
355         # If it wasn't found in a platform directory, return the expected
356         # result in the test directory, even if no such file actually exists.
357         platform_dir = self.layout_tests_dir()
358         if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)):
359             baselines.append((platform_dir, baseline_filename))
360
361         if baselines:
362             return baselines
363
364         return [(None, baseline_filename)]
365
366     def expected_filename(self, test_name, suffix):
367         """Given a test name, returns an absolute path to its expected results.
368
369         If no expected results are found in any of the searched directories,
370         the directory in which the test itself is located will be returned.
371         The return value is in the format appropriate for the platform
372         (e.g., "\\" for path separators on windows).
373
374         Args:
375         test_name: name of test file (usually a relative path under LayoutTests/)
376         suffix: file suffix of the expected results, including dot; e.g. '.txt'
377             or '.png'.  This should not be None, but may be an empty string.
378         platform: the most-specific directory name to use to build the
379             search list of directories, e.g., 'chromium-win', or
380             'chromium-cg-mac-leopard' (we follow the WebKit format)
381
382         This routine is generic but is implemented here to live alongside
383         the other baseline and filename manipulation routines.
384         """
385         # FIXME: The [0] here is very mysterious, as is the destructured return.
386         platform_dir, baseline_filename = self.expected_baselines(test_name, suffix)[0]
387         if platform_dir:
388             return self._filesystem.join(platform_dir, baseline_filename)
389         return self._filesystem.join(self.layout_tests_dir(), baseline_filename)
390
391     def expected_checksum(self, test_name):
392         """Returns the checksum of the image we expect the test to produce, or None if it is a text-only test."""
393         png_path = self.expected_filename(test_name, '.png')
394
395         if self._filesystem.exists(png_path):
396             with self._filesystem.open_binary_file_for_reading(png_path) as filehandle:
397                 return read_checksum_from_png.read_checksum(filehandle)
398
399         return None
400
401     def expected_image(self, test_name):
402         """Returns the image we expect the test to produce."""
403         baseline_path = self.expected_filename(test_name, '.png')
404         if not self._filesystem.exists(baseline_path):
405             return None
406         return self._filesystem.read_binary_file(baseline_path)
407
408     def expected_audio(self, test_name):
409         baseline_path = self.expected_filename(test_name, '.wav')
410         if not self._filesystem.exists(baseline_path):
411             return None
412         return self._filesystem.read_binary_file(baseline_path)
413
414     def expected_text(self, test_name):
415         """Returns the text output we expect the test to produce, or None
416         if we don't expect there to be any text output.
417         End-of-line characters are normalized to '\n'."""
418         # FIXME: DRT output is actually utf-8, but since we don't decode the
419         # output from DRT (instead treating it as a binary string), we read the
420         # baselines as a binary string, too.
421         baseline_path = self.expected_filename(test_name, '.txt')
422         if not self._filesystem.exists(baseline_path):
423             baseline_path = self.expected_filename(test_name, '.webarchive')
424             if not self._filesystem.exists(baseline_path):
425                 return None
426         text = self._filesystem.read_binary_file(baseline_path)
427         return text.replace("\r\n", "\n")
428
429     def _get_reftest_list(self, test_name):
430         dirname = self._filesystem.join(self.layout_tests_dir(), self._filesystem.dirname(test_name))
431         if dirname not in self._reftest_list:
432             self._reftest_list[dirname] = Port._parse_reftest_list(self._filesystem, dirname)
433         return self._reftest_list[dirname]
434
435     @staticmethod
436     def _parse_reftest_list(filesystem, test_dirpath):
437         reftest_list_path = filesystem.join(test_dirpath, 'reftest.list')
438         if not filesystem.isfile(reftest_list_path):
439             return None
440         reftest_list_file = filesystem.read_text_file(reftest_list_path)
441
442         parsed_list = dict()
443         for line in reftest_list_file.split('\n'):
444             line = re.sub('#.+$', '', line)
445             split_line = line.split()
446             if len(split_line) < 3:
447                 continue
448             expectation_type, test_file, ref_file = split_line
449             parsed_list[filesystem.join(test_dirpath, test_file)] = (expectation_type, filesystem.join(test_dirpath, ref_file))
450         return parsed_list
451
452     def _reference_file_for(self, test_name, expectation):
453         reftest_list = self._get_reftest_list(test_name)
454         if not reftest_list:
455             if expectation == '==':
456                 return self.expected_filename(test_name, '.html')
457             else:
458                 return self.expected_filename(test_name, '-mismatch.html')
459
460         filename = self._filesystem.join(self.layout_tests_dir(), test_name)
461         if filename not in reftest_list or reftest_list[filename][0] != expectation:
462             return None
463         return reftest_list[filename][1]
464
465     def is_reftest(self, test_name):
466         reftest_list = self._get_reftest_list(test_name)
467         if not reftest_list:
468             has_expected = self._filesystem.exists(self.expected_filename(test_name, '.html'))
469             return has_expected or self._filesystem.exists(self.expected_filename(test_name, '-mismatch.html'))
470         filename = self._filesystem.join(self.layout_tests_dir(), test_name)
471         return filename in reftest_list
472
473     def reftest_expected_filename(self, test_name):
474         """Return the filename of reference we expect the test matches."""
475         return self._reference_file_for(test_name, '==')
476
477     def reftest_expected_mismatch_filename(self, test_name):
478         """Return the filename of reference we don't expect the test matches."""
479         return self._reference_file_for(test_name, '!=')
480
481     def test_to_uri(self, test_name):
482         """Convert a test name to a URI."""
483         LAYOUTTEST_HTTP_DIR = "http/tests/"
484         LAYOUTTEST_WEBSOCKET_DIR = "http/tests/websocket/tests/"
485
486         port = None
487
488         relative_path = test_name
489         if (relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR)
490             or relative_path.startswith(LAYOUTTEST_HTTP_DIR)):
491             relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):]
492             port = 8000
493
494         # Make http/tests/local run as local files. This is to mimic the
495         # logic in run-webkit-tests.
496         #
497         # TODO(dpranke): remove the SSL reference?
498         if (port and not relative_path.startswith("local/")):
499             if relative_path.startswith("ssl/"):
500                 port += 443
501                 protocol = "https"
502             else:
503                 protocol = "http"
504             return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path)
505
506         return path.abspath_to_uri(self.abspath_for_test(test_name))
507
508     def tests(self, paths):
509         """Return the list of tests found."""
510         # When collecting test cases, skip these directories
511         skipped_directories = set(['.svn', '_svn', 'resources', 'script-tests', 'reference', 'reftest'])
512         files = find_files.find(self.filesystem, self.layout_tests_dir(), paths, skipped_directories, Port._is_test_file)
513         return set([self.relative_test_filename(f) for f in files])
514
515     # When collecting test cases, we include any file with these extensions.
516     _supported_file_extensions = set(['.html', '.shtml', '.xml', '.xhtml', '.pl',
517                                       '.htm', '.php', '.svg', '.mht'])
518
519     @staticmethod
520     def is_reference_html_file(filesystem, dirname, filename):
521         if filename.startswith('ref-') or filename.endswith('notref-'):
522             return True
523         filename_wihout_ext, unused = filesystem.splitext(filename)
524         for suffix in ['-expected', '-expected-mismatch', '-ref', '-notref']:
525             if filename_wihout_ext.endswith(suffix):
526                 return True
527         return False
528
529     @staticmethod
530     def _has_supported_extension(filesystem, filename):
531         """Return true if filename is one of the file extensions we want to run a test on."""
532         extension = filesystem.splitext(filename)[1]
533         return extension in Port._supported_file_extensions
534
535     @staticmethod
536     def _is_test_file(filesystem, dirname, filename):
537         return Port._has_supported_extension(filesystem, filename) and not Port.is_reference_html_file(filesystem, dirname, filename)
538
539     def test_dirs(self):
540         """Returns the list of top-level test directories."""
541         layout_tests_dir = self.layout_tests_dir()
542         return filter(lambda x: self._filesystem.isdir(self._filesystem.join(layout_tests_dir, x)),
543                       self._filesystem.listdir(layout_tests_dir))
544
545     def test_isdir(self, test_name):
546         """Return True if the test name refers to a directory of tests."""
547         # Used by test_expectations.py to apply rules to whole directories.
548         test_path = self.abspath_for_test(test_name)
549         return self._filesystem.isdir(test_path)
550
551     def test_exists(self, test_name):
552         """Return True if the test name refers to an existing test or baseline."""
553         # Used by test_expectations.py to determine if an entry refers to a
554         # valid test and by printing.py to determine if baselines exist.
555         test_path = self.abspath_for_test(test_name)
556         return self._filesystem.exists(test_path)
557
558     def split_test(self, test_name):
559         """Splits a test name into the 'directory' part and the 'basename' part."""
560         index = test_name.rfind(self.TEST_PATH_SEPARATOR)
561         if index < 1:
562             return ('', test_name)
563         return (test_name[0:index], test_name[index:])
564
565     def normalize_test_name(self, test_name):
566         """Returns a normalized version of the test name or test directory."""
567         if self.test_isdir(test_name) and not test_name.endswith('/'):
568             return test_name + '/'
569         return test_name
570
571     def driver_cmd_line(self):
572         """Prints the DRT command line that will be used."""
573         driver = self.create_driver(0)
574         return driver.cmd_line()
575
576     def update_baseline(self, baseline_path, data):
577         """Updates the baseline for a test.
578
579         Args:
580             baseline_path: the actual path to use for baseline, not the path to
581               the test. This function is used to update either generic or
582               platform-specific baselines, but we can't infer which here.
583             data: contents of the baseline.
584         """
585         self._filesystem.write_binary_file(baseline_path, data)
586
587     def uri_to_test_name(self, uri):
588         """Return the base layout test name for a given URI.
589
590         This returns the test name for a given URI, e.g., if you passed in
591         "file:///src/LayoutTests/fast/html/keygen.html" it would return
592         "fast/html/keygen.html".
593
594         """
595         test = uri
596         if uri.startswith("file:///"):
597             prefix = path.abspath_to_uri(self.layout_tests_dir()) + "/"
598             return test[len(prefix):]
599
600         if uri.startswith("http://127.0.0.1:8880/"):
601             # websocket tests
602             return test.replace('http://127.0.0.1:8880/', '')
603
604         if uri.startswith("http://"):
605             # regular HTTP test
606             return test.replace('http://127.0.0.1:8000/', 'http/tests/')
607
608         if uri.startswith("https://"):
609             return test.replace('https://127.0.0.1:8443/', 'http/tests/')
610
611         raise NotImplementedError('unknown url type: %s' % uri)
612
613     def layout_tests_dir(self):
614         """Return the absolute path to the top of the LayoutTests directory."""
615         return self.path_from_webkit_base('LayoutTests')
616
617     def webkit_base(self):
618         return self._filesystem.abspath(self.path_from_webkit_base('.'))
619
620     def skipped_layout_tests(self):
621         return []
622
623     def skips_layout_test(self, test_name):
624         """Figures out if the givent test is being skipped or not.
625
626         Test categories are handled as well."""
627         for test_or_category in self.skipped_layout_tests():
628             if test_or_category == test_name:
629                 return True
630             category = self._filesystem.join(self.layout_tests_dir(), test_or_category)
631             if self._filesystem.isdir(category) and test_name.startswith(test_or_category):
632                 return True
633         return False
634
635     def maybe_make_directory(self, *comps):
636         """Creates the specified directory if it doesn't already exist."""
637         self._filesystem.maybe_make_directory(*comps)
638
639     def name(self):
640         """Returns a name that uniquely identifies this particular type of port
641         (e.g., "mac-snowleopard" or "chromium-gpu-linux-x86_x64" and can be passed
642         to factory.get() to instantiate the port."""
643         return self._name
644
645     def real_name(self):
646         # FIXME: Seems this is only used for MockDRT and should be removed.
647         """Returns the name of the port as passed to the --platform command line argument."""
648         return self.name()
649
650     def operating_system(self):
651         return self._operating_system
652
653     def version(self):
654         """Returns a string indicating the version of a given platform, e.g.
655         'leopard' or 'xp'.
656
657         This is used to help identify the exact port when parsing test
658         expectations, determining search paths, and logging information."""
659         return self._version
660
661     def graphics_type(self):
662         """Returns whether the port uses accelerated graphics ('gpu') or not ('cpu')."""
663         return self._graphics_type
664
665     def architecture(self):
666         return self._architecture
667
668     def get_option(self, name, default_value=None):
669         # FIXME: Eventually we should not have to do a test for
670         # hasattr(), and we should be able to just do
671         # self.options.value. See additional FIXME in the constructor.
672         if hasattr(self._options, name):
673             return getattr(self._options, name)
674         return default_value
675
676     def set_option_default(self, name, default_value):
677         # FIXME: Callers could also use optparse_parser.Values.ensure_value,
678         # since this should always be a optparse_parser.Values object.
679         if not hasattr(self._options, name) or getattr(self._options, name) is None:
680             return setattr(self._options, name, default_value)
681
682     def path_from_webkit_base(self, *comps):
683         """Returns the full path to path made by joining the top of the
684         WebKit source tree and the list of path components in |*comps|."""
685         return self._config.path_from_webkit_base(*comps)
686
687     def path_to_test_expectations_file(self):
688         """Update the test expectations to the passed-in string.
689
690         This is used by the rebaselining tool. Raises NotImplementedError
691         if the port does not use expectations files."""
692         raise NotImplementedError('Port.path_to_test_expectations_file')
693
694     def relative_test_filename(self, filename):
695         """Returns a test_name a realtive unix-style path for a filename under the LayoutTests
696         directory. Filenames outside the LayoutTests directory should raise
697         an error."""
698         # Ports that run on windows need to override this method to deal with
699         # filenames with backslashes in them.
700         assert filename.startswith(self.layout_tests_dir()), "%s did not start with %s" % (filename, self.layout_tests_dir())
701         return filename[len(self.layout_tests_dir()) + 1:]
702
703     def abspath_for_test(self, test_name):
704         """Returns the full path to the file for a given test name. This is the
705         inverse of relative_test_filename()."""
706         return self._filesystem.normpath(self._filesystem.join(self.layout_tests_dir(), test_name))
707
708     def results_directory(self):
709         """Absolute path to the place to store the test results (uses --results-directory)."""
710         if not self._results_directory:
711             option_val = self.get_option('results_directory') or self.default_results_directory()
712             self._results_directory = self._filesystem.abspath(option_val)
713         return self._results_directory
714
715     def default_results_directory(self):
716         """Absolute path to the default place to store the test results."""
717         raise NotImplementedError()
718
719     def setup_test_run(self):
720         """Perform port-specific work at the beginning of a test run."""
721         pass
722
723     # FIXME: os.environ access should be moved to onto a common/system class to be more easily mockable.
724     def _value_or_default_from_environ(self, name, default=None):
725         if name in os.environ:
726             return os.environ[name]
727         return default
728
729     def _copy_value_from_environ_if_set(self, clean_env, name):
730         if name in os.environ:
731             clean_env[name] = os.environ[name]
732
733     def setup_environ_for_server(self, server_name=None):
734         # We intentionally copy only a subset of os.environ when
735         # launching subprocesses to ensure consistent test results.
736         clean_env = {}
737         variables_to_copy = [
738             # For Linux:
739             'XAUTHORITY',
740             'HOME',
741             'LANG',
742             'LD_LIBRARY_PATH',
743             'DBUS_SESSION_BUS_ADDRESS',
744
745             # Darwin:
746             'DYLD_LIBRARY_PATH',
747             'HOME',
748
749             # CYGWIN:
750             'HOMEDRIVE',
751             'HOMEPATH',
752             '_NT_SYMBOL_PATH',
753
754             # Windows:
755             'PATH',
756         ]
757         for variable in variables_to_copy:
758             self._copy_value_from_environ_if_set(clean_env, variable)
759
760         # For Linux:
761         clean_env['DISPLAY'] = self._value_or_default_from_environ('DISPLAY', ':1')
762         return clean_env
763
764     def show_results_html_file(self, results_filename):
765         """This routine should display the HTML file pointed at by
766         results_filename in a users' browser."""
767         return self._user.open_url(results_filename)
768
769     def create_driver(self, worker_number):
770         """Return a newly created Driver subclass for starting/stopping the test driver."""
771         return driver.DriverProxy(self, worker_number, self._driver_class(), pixel_tests=self.get_option('pixel_tests'))
772
773     def start_helper(self):
774         """If a port needs to reconfigure graphics settings or do other
775         things to ensure a known test configuration, it should override this
776         method."""
777         pass
778
779     def start_http_server(self):
780         """Start a web server. Raise an error if it can't start or is already running.
781
782         Ports can stub this out if they don't need a web server to be running."""
783         assert not self._http_server, 'Already running an http server.'
784
785         if self._uses_apache():
786             server = apache_http_server.LayoutTestApacheHttpd(self, self.results_directory())
787         else:
788             server = http_server.Lighttpd(self, self.results_directory())
789
790         server.start()
791         self._http_server = server
792
793     def start_websocket_server(self):
794         """Start a web server. Raise an error if it can't start or is already running.
795
796         Ports can stub this out if they don't need a websocket server to be running."""
797         assert not self._websocket_server, 'Already running a websocket server.'
798
799         server = websocket_server.PyWebSocket(self, self.results_directory())
800         server.start()
801         self._websocket_server = server
802
803     def acquire_http_lock(self):
804         self._http_lock = http_lock.HttpLock(None, filesystem=self._filesystem, executive=self._executive)
805         self._http_lock.wait_for_httpd_lock()
806
807     def stop_helper(self):
808         """Shut down the test helper if it is running. Do nothing if
809         it isn't, or it isn't available. If a port overrides start_helper()
810         it must override this routine as well."""
811         pass
812
813     def stop_http_server(self):
814         """Shut down the http server if it is running. Do nothing if it isn't."""
815         if self._http_server:
816             self._http_server.stop()
817             self._http_server = None
818
819     def stop_websocket_server(self):
820         """Shut down the websocket server if it is running. Do nothing if it isn't."""
821         if self._websocket_server:
822             self._websocket_server.stop()
823             self._websocket_server = None
824
825     def release_http_lock(self):
826         if self._http_lock:
827             self._http_lock.cleanup_http_lock()
828
829     def exit_code_from_summarized_results(self, unexpected_results):
830         """Given summarized results, compute the exit code to be returned by new-run-webkit-tests.
831         Bots turn red when this function returns a non-zero value. By default, return the number of regressions
832         to avoid turning bots red by flaky failures, unexpected passes, and missing results"""
833         # Don't turn bots red for flaky failures, unexpected passes, and missing results.
834         return unexpected_results['num_regressions']
835
836     #
837     # TEST EXPECTATION-RELATED METHODS
838     #
839
840     def test_configuration(self):
841         """Returns the current TestConfiguration for the port."""
842         if not self._test_configuration:
843             self._test_configuration = TestConfiguration.from_port(self)
844         return self._test_configuration
845
846     # FIXME: Belongs on a Platform object.
847     @memoized
848     def all_test_configurations(self):
849         """Returns a list of TestConfiguration instances, representing all available
850         test configurations for this port."""
851         return self._generate_all_test_configurations()
852
853     # FIXME: Belongs on a Platform object.
854     def configuration_specifier_macros(self):
855         """Ports may provide a way to abbreviate configuration specifiers to conveniently
856         refer to them as one term or alias specific values to more generic ones. For example:
857
858         (xp, vista, win7) -> win # Abbreviate all Windows versions into one namesake.
859         (lucid) -> linux  # Change specific name of the Linux distro to a more generic term.
860
861         Returns a dictionary, each key representing a macro term ('win', for example),
862         and value being a list of valid configuration specifiers (such as ['xp', 'vista', 'win7'])."""
863         return {}
864
865     def all_baseline_variants(self):
866         """Returns a list of platform names sufficient to cover all the baselines.
867
868         The list should be sorted so that a later platform  will reuse
869         an earlier platform's baselines if they are the same (e.g.,
870         'snowleopard' should precede 'leopard')."""
871         raise NotImplementedError
872
873     def uses_test_expectations_file(self):
874         # This is different from checking test_expectations() is None, because
875         # some ports have Skipped files which are returned as part of test_expectations().
876         return self._filesystem.exists(self.path_to_test_expectations_file())
877
878     def test_expectations(self):
879         """Returns the test expectations for this port.
880
881         Basically this string should contain the equivalent of a
882         test_expectations file. See test_expectations.py for more details."""
883         return self._filesystem.read_text_file(self.path_to_test_expectations_file())
884
885     def test_expectations_overrides(self):
886         """Returns an optional set of overrides for the test_expectations.
887
888         This is used by ports that have code in two repositories, and where
889         it is possible that you might need "downstream" expectations that
890         temporarily override the "upstream" expectations until the port can
891         sync up the two repos."""
892         return None
893
894     def test_repository_paths(self):
895         """Returns a list of (repository_name, repository_path) tuples
896         of its depending code base.  By default it returns a list that only
897         contains a ('webkit', <webkitRepossitoryPath>) tuple.
898         """
899         return [('webkit', self.layout_tests_dir())]
900
901
902     _WDIFF_DEL = '##WDIFF_DEL##'
903     _WDIFF_ADD = '##WDIFF_ADD##'
904     _WDIFF_END = '##WDIFF_END##'
905
906     def _format_wdiff_output_as_html(self, wdiff):
907         wdiff = cgi.escape(wdiff)
908         wdiff = wdiff.replace(self._WDIFF_DEL, "<span class=del>")
909         wdiff = wdiff.replace(self._WDIFF_ADD, "<span class=add>")
910         wdiff = wdiff.replace(self._WDIFF_END, "</span>")
911         html = "<head><style>.del { background: #faa; } "
912         html += ".add { background: #afa; }</style></head>"
913         html += "<pre>%s</pre>" % wdiff
914         return html
915
916     def _wdiff_command(self, actual_filename, expected_filename):
917         executable = self._path_to_wdiff()
918         return [executable,
919                 "--start-delete=%s" % self._WDIFF_DEL,
920                 "--end-delete=%s" % self._WDIFF_END,
921                 "--start-insert=%s" % self._WDIFF_ADD,
922                 "--end-insert=%s" % self._WDIFF_END,
923                 actual_filename,
924                 expected_filename]
925
926     @staticmethod
927     def _handle_wdiff_error(script_error):
928         # Exit 1 means the files differed, any other exit code is an error.
929         if script_error.exit_code != 1:
930             raise script_error
931
932     def _run_wdiff(self, actual_filename, expected_filename):
933         """Runs wdiff and may throw exceptions.
934         This is mostly a hook for unit testing."""
935         # Diffs are treated as binary as they may include multiple files
936         # with conflicting encodings.  Thus we do not decode the output.
937         command = self._wdiff_command(actual_filename, expected_filename)
938         wdiff = self._executive.run_command(command, decode_output=False,
939             error_handler=self._handle_wdiff_error)
940         return self._format_wdiff_output_as_html(wdiff)
941
942     def wdiff_text(self, actual_filename, expected_filename):
943         """Returns a string of HTML indicating the word-level diff of the
944         contents of the two filenames. Returns an empty string if word-level
945         diffing isn't available."""
946         if not self._wdiff_available:
947             return ""
948         try:
949             # It's possible to raise a ScriptError we pass wdiff invalid paths.
950             return self._run_wdiff(actual_filename, expected_filename)
951         except OSError, e:
952             if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
953                 # Silently ignore cases where wdiff is missing.
954                 self._wdiff_available = False
955                 return ""
956             raise
957
958     # This is a class variable so we can test error output easily.
959     _pretty_patch_error_html = "Failed to run PrettyPatch, see error log."
960
961     def pretty_patch_text(self, diff_path):
962         if self._pretty_patch_available is None:
963             self._pretty_patch_available = self.check_pretty_patch(logging=False)
964         if not self._pretty_patch_available:
965             return self._pretty_patch_error_html
966         command = ("ruby", "-I", self._filesystem.dirname(self._pretty_patch_path),
967                    self._pretty_patch_path, diff_path)
968         try:
969             # Diffs are treated as binary (we pass decode_output=False) as they
970             # may contain multiple files of conflicting encodings.
971             return self._executive.run_command(command, decode_output=False)
972         except OSError, e:
973             # If the system is missing ruby log the error and stop trying.
974             self._pretty_patch_available = False
975             _log.error("Failed to run PrettyPatch (%s): %s" % (command, e))
976             return self._pretty_patch_error_html
977         except ScriptError, e:
978             # If ruby failed to run for some reason, log the command
979             # output and stop trying.
980             self._pretty_patch_available = False
981             _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, e.message_with_output()))
982             return self._pretty_patch_error_html
983
984     def default_configuration(self):
985         return self._config.default_configuration()
986
987     #
988     # PROTECTED ROUTINES
989     #
990     # The routines below should only be called by routines in this class
991     # or any of its subclasses.
992     #
993
994     def _uses_apache(self):
995         return True
996
997     def _path_to_apache(self):
998         """Returns the full path to the apache binary.
999
1000         This is needed only by ports that use the apache_http_server module."""
1001         raise NotImplementedError('Port.path_to_apache')
1002
1003     def _path_to_apache_config_file(self):
1004         """Returns the full path to the apache binary.
1005
1006         This is needed only by ports that use the apache_http_server module."""
1007         raise NotImplementedError('Port.path_to_apache_config_file')
1008
1009     def _path_to_driver(self, configuration=None):
1010         """Returns the full path to the test driver (DumpRenderTree)."""
1011         raise NotImplementedError('Port._path_to_driver')
1012
1013     def _path_to_webcore_library(self):
1014         """Returns the full path to a built copy of WebCore."""
1015         raise NotImplementedError('Port.path_to_webcore_library')
1016
1017     def _path_to_helper(self):
1018         """Returns the full path to the layout_test_helper binary, which
1019         is used to help configure the system for the test run, or None
1020         if no helper is needed.
1021
1022         This is likely only used by start/stop_helper()."""
1023         raise NotImplementedError('Port._path_to_helper')
1024
1025     def _path_to_image_diff(self):
1026         """Returns the full path to the image_diff binary, or None if it is not available.
1027
1028         This is likely used only by diff_image()"""
1029         raise NotImplementedError('Port.path_to_image_diff')
1030
1031     def _path_to_lighttpd(self):
1032         """Returns the path to the LigHTTPd binary.
1033
1034         This is needed only by ports that use the http_server.py module."""
1035         raise NotImplementedError('Port._path_to_lighttpd')
1036
1037     def _path_to_lighttpd_modules(self):
1038         """Returns the path to the LigHTTPd modules directory.
1039
1040         This is needed only by ports that use the http_server.py module."""
1041         raise NotImplementedError('Port._path_to_lighttpd_modules')
1042
1043     def _path_to_lighttpd_php(self):
1044         """Returns the path to the LigHTTPd PHP executable.
1045
1046         This is needed only by ports that use the http_server.py module."""
1047         raise NotImplementedError('Port._path_to_lighttpd_php')
1048
1049     def _path_to_wdiff(self):
1050         """Returns the full path to the wdiff binary, or None if it is not available.
1051
1052         This is likely used only by wdiff_text()"""
1053         raise NotImplementedError('Port._path_to_wdiff')
1054
1055     def _webkit_baseline_path(self, platform):
1056         """Return the  full path to the top of the baseline tree for a
1057         given platform."""
1058         return self._filesystem.join(self.layout_tests_dir(), 'platform', platform)
1059
1060     # FIXME: Belongs on a Platform object.
1061     def _generate_all_test_configurations(self):
1062         """Generates a list of TestConfiguration instances, representing configurations
1063         for a platform across all OSes, architectures, build and graphics types."""
1064         raise NotImplementedError('Port._generate_test_configurations')
1065
1066     def _driver_class(self):
1067         """Returns the port's driver implementation."""
1068         raise NotImplementedError('Port._driver_class')