f46632f5f9abee7b311f45a8783ae6addf52cc6b
[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] = _parse_reftest_list(self._filesystem, dirname)
433         return self._reftest_list[dirname]
434
435     def _reference_file_for(self, test_name, expectation):
436         reftest_list = self._get_reftest_list(test_name)
437         if not reftest_list:
438             if expectation == '==':
439                 return self.expected_filename(test_name, '.html')
440             else:
441                 return self.expected_filename(test_name, '-mismatch.html')
442
443         filename = self._filesystem.join(self.layout_tests_dir(), test_name)
444         if filename not in reftest_list or reftest_list[filename][0] != expectation:
445             return None
446         return reftest_list[filename][1]
447
448     def is_reftest(self, test_name):
449         reftest_list = self._get_reftest_list(test_name)
450         if not reftest_list:
451             has_expected = self._filesystem.exists(self.expected_filename(test_name, '.html'))
452             return has_expected or self._filesystem.exists(self.expected_filename(test_name, '-mismatch.html'))
453         filename = self._filesystem.join(self.layout_tests_dir(), test_name)
454         return filename in reftest_list
455
456     def reftest_expected_filename(self, test_name):
457         """Return the filename of reference we expect the test matches."""
458         return self._reference_file_for(test_name, '==')
459
460     def reftest_expected_mismatch_filename(self, test_name):
461         """Return the filename of reference we don't expect the test matches."""
462         return self._reference_file_for(test_name, '!=')
463
464     def test_to_uri(self, test_name):
465         """Convert a test name to a URI."""
466         LAYOUTTEST_HTTP_DIR = "http/tests/"
467         LAYOUTTEST_WEBSOCKET_DIR = "http/tests/websocket/tests/"
468
469         port = None
470
471         relative_path = test_name
472         if (relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR)
473             or relative_path.startswith(LAYOUTTEST_HTTP_DIR)):
474             relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):]
475             port = 8000
476
477         # Make http/tests/local run as local files. This is to mimic the
478         # logic in run-webkit-tests.
479         #
480         # TODO(dpranke): remove the SSL reference?
481         if (port and not relative_path.startswith("local/")):
482             if relative_path.startswith("ssl/"):
483                 port += 443
484                 protocol = "https"
485             else:
486                 protocol = "http"
487             return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path)
488
489         return path.abspath_to_uri(self.abspath_for_test(test_name))
490
491     def tests(self, paths):
492         """Return the list of tests found."""
493         # FIXME: Should port.find_test_files() return normalized, relative test names?
494         return set([self.relative_test_filename(f) for f in self.find_test_files(paths)])
495
496     def find_test_files(self, paths):
497         # When collecting test cases, skip these directories
498         skipped_directories = set(['.svn', '_svn', 'resources', 'script-tests', 'reference', 'reftest'])
499         return find_files.find(self.filesystem, self.layout_tests_dir(), paths, skipped_directories, _is_test_file)
500
501     def test_dirs(self):
502         """Returns the list of top-level test directories."""
503         layout_tests_dir = self.layout_tests_dir()
504         return filter(lambda x: self._filesystem.isdir(self._filesystem.join(layout_tests_dir, x)),
505                       self._filesystem.listdir(layout_tests_dir))
506
507     def test_isdir(self, test_name):
508         """Return True if the test name refers to a directory of tests."""
509         # Used by test_expectations.py to apply rules to whole directories.
510         test_path = self.abspath_for_test(test_name)
511         return self._filesystem.isdir(test_path)
512
513     def test_exists(self, test_name):
514         """Return True if the test name refers to an existing test or baseline."""
515         # Used by test_expectations.py to determine if an entry refers to a
516         # valid test and by printing.py to determine if baselines exist.
517         test_path = self.abspath_for_test(test_name)
518         return self._filesystem.exists(test_path)
519
520     def split_test(self, test_name):
521         """Splits a test name into the 'directory' part and the 'basename' part."""
522         index = test_name.rfind(self.TEST_PATH_SEPARATOR)
523         if index < 1:
524             return ('', test_name)
525         return (test_name[0:index], test_name[index:])
526
527     def normalize_test_name(self, test_name):
528         """Returns a normalized version of the test name or test directory."""
529         if self.test_isdir(test_name) and not test_name.endswith('/'):
530             return test_name + '/'
531         return test_name
532
533     def driver_cmd_line(self):
534         """Prints the DRT command line that will be used."""
535         driver = self.create_driver(0)
536         return driver.cmd_line()
537
538     def update_baseline(self, baseline_path, data):
539         """Updates the baseline for a test.
540
541         Args:
542             baseline_path: the actual path to use for baseline, not the path to
543               the test. This function is used to update either generic or
544               platform-specific baselines, but we can't infer which here.
545             data: contents of the baseline.
546         """
547         self._filesystem.write_binary_file(baseline_path, data)
548
549     def uri_to_test_name(self, uri):
550         """Return the base layout test name for a given URI.
551
552         This returns the test name for a given URI, e.g., if you passed in
553         "file:///src/LayoutTests/fast/html/keygen.html" it would return
554         "fast/html/keygen.html".
555
556         """
557         test = uri
558         if uri.startswith("file:///"):
559             prefix = path.abspath_to_uri(self.layout_tests_dir()) + "/"
560             return test[len(prefix):]
561
562         if uri.startswith("http://127.0.0.1:8880/"):
563             # websocket tests
564             return test.replace('http://127.0.0.1:8880/', '')
565
566         if uri.startswith("http://"):
567             # regular HTTP test
568             return test.replace('http://127.0.0.1:8000/', 'http/tests/')
569
570         if uri.startswith("https://"):
571             return test.replace('https://127.0.0.1:8443/', 'http/tests/')
572
573         raise NotImplementedError('unknown url type: %s' % uri)
574
575     def layout_tests_dir(self):
576         """Return the absolute path to the top of the LayoutTests directory."""
577         return self.path_from_webkit_base('LayoutTests')
578
579     def webkit_base(self):
580         return self._filesystem.abspath(self.path_from_webkit_base('.'))
581
582     def skipped_layout_tests(self):
583         return []
584
585     def skips_layout_test(self, test_name):
586         """Figures out if the givent test is being skipped or not.
587
588         Test categories are handled as well."""
589         for test_or_category in self.skipped_layout_tests():
590             if test_or_category == test_name:
591                 return True
592             category = self._filesystem.join(self.layout_tests_dir(), test_or_category)
593             if self._filesystem.isdir(category) and test_name.startswith(test_or_category):
594                 return True
595         return False
596
597     def maybe_make_directory(self, *comps):
598         """Creates the specified directory if it doesn't already exist."""
599         self._filesystem.maybe_make_directory(*comps)
600
601     def name(self):
602         """Returns a name that uniquely identifies this particular type of port
603         (e.g., "mac-snowleopard" or "chromium-gpu-linux-x86_x64" and can be passed
604         to factory.get() to instantiate the port."""
605         return self._name
606
607     def real_name(self):
608         # FIXME: Seems this is only used for MockDRT and should be removed.
609         """Returns the name of the port as passed to the --platform command line argument."""
610         return self.name()
611
612     def operating_system(self):
613         return self._operating_system
614
615     def version(self):
616         """Returns a string indicating the version of a given platform, e.g.
617         'leopard' or 'xp'.
618
619         This is used to help identify the exact port when parsing test
620         expectations, determining search paths, and logging information."""
621         return self._version
622
623     def graphics_type(self):
624         """Returns whether the port uses accelerated graphics ('gpu') or not ('cpu')."""
625         return self._graphics_type
626
627     def architecture(self):
628         return self._architecture
629
630     def get_option(self, name, default_value=None):
631         # FIXME: Eventually we should not have to do a test for
632         # hasattr(), and we should be able to just do
633         # self.options.value. See additional FIXME in the constructor.
634         if hasattr(self._options, name):
635             return getattr(self._options, name)
636         return default_value
637
638     def set_option_default(self, name, default_value):
639         # FIXME: Callers could also use optparse_parser.Values.ensure_value,
640         # since this should always be a optparse_parser.Values object.
641         if not hasattr(self._options, name) or getattr(self._options, name) is None:
642             return setattr(self._options, name, default_value)
643
644     def path_from_webkit_base(self, *comps):
645         """Returns the full path to path made by joining the top of the
646         WebKit source tree and the list of path components in |*comps|."""
647         return self._config.path_from_webkit_base(*comps)
648
649     def path_to_test_expectations_file(self):
650         """Update the test expectations to the passed-in string.
651
652         This is used by the rebaselining tool. Raises NotImplementedError
653         if the port does not use expectations files."""
654         raise NotImplementedError('Port.path_to_test_expectations_file')
655
656     def relative_test_filename(self, filename):
657         """Returns a test_name a realtive unix-style path for a filename under the LayoutTests
658         directory. Filenames outside the LayoutTests directory should raise
659         an error."""
660         # Ports that run on windows need to override this method to deal with
661         # filenames with backslashes in them.
662         assert filename.startswith(self.layout_tests_dir()), "%s did not start with %s" % (filename, self.layout_tests_dir())
663         return filename[len(self.layout_tests_dir()) + 1:]
664
665     def abspath_for_test(self, test_name):
666         """Returns the full path to the file for a given test name. This is the
667         inverse of relative_test_filename()."""
668         return self._filesystem.normpath(self._filesystem.join(self.layout_tests_dir(), test_name))
669
670     def results_directory(self):
671         """Absolute path to the place to store the test results (uses --results-directory)."""
672         if not self._results_directory:
673             option_val = self.get_option('results_directory') or self.default_results_directory()
674             self._results_directory = self._filesystem.abspath(option_val)
675         return self._results_directory
676
677     def default_results_directory(self):
678         """Absolute path to the default place to store the test results."""
679         raise NotImplementedError()
680
681     def setup_test_run(self):
682         """Perform port-specific work at the beginning of a test run."""
683         pass
684
685     # FIXME: os.environ access should be moved to onto a common/system class to be more easily mockable.
686     def _value_or_default_from_environ(self, name, default=None):
687         if name in os.environ:
688             return os.environ[name]
689         return default
690
691     def _copy_value_from_environ_if_set(self, clean_env, name):
692         if name in os.environ:
693             clean_env[name] = os.environ[name]
694
695     def setup_environ_for_server(self, server_name=None):
696         # We intentionally copy only a subset of os.environ when
697         # launching subprocesses to ensure consistent test results.
698         clean_env = {}
699         variables_to_copy = [
700             # For Linux:
701             'XAUTHORITY',
702             'HOME',
703             'LANG',
704             'LD_LIBRARY_PATH',
705             'DBUS_SESSION_BUS_ADDRESS',
706
707             # Darwin:
708             'DYLD_LIBRARY_PATH',
709             'HOME',
710
711             # CYGWIN:
712             'HOMEDRIVE',
713             'HOMEPATH',
714             '_NT_SYMBOL_PATH',
715
716             # Windows:
717             'PATH',
718         ]
719         for variable in variables_to_copy:
720             self._copy_value_from_environ_if_set(clean_env, variable)
721
722         # For Linux:
723         clean_env['DISPLAY'] = self._value_or_default_from_environ('DISPLAY', ':1')
724         return clean_env
725
726     def show_results_html_file(self, results_filename):
727         """This routine should display the HTML file pointed at by
728         results_filename in a users' browser."""
729         return self._user.open_url(results_filename)
730
731     def create_driver(self, worker_number):
732         """Return a newly created Driver subclass for starting/stopping the test driver."""
733         return driver.DriverProxy(self, worker_number, self._driver_class(), pixel_tests=self.get_option('pixel_tests'))
734
735     def start_helper(self):
736         """If a port needs to reconfigure graphics settings or do other
737         things to ensure a known test configuration, it should override this
738         method."""
739         pass
740
741     def start_http_server(self):
742         """Start a web server. Raise an error if it can't start or is already running.
743
744         Ports can stub this out if they don't need a web server to be running."""
745         assert not self._http_server, 'Already running an http server.'
746
747         if self._uses_apache():
748             server = apache_http_server.LayoutTestApacheHttpd(self, self.results_directory())
749         else:
750             server = http_server.Lighttpd(self, self.results_directory())
751
752         server.start()
753         self._http_server = server
754
755     def start_websocket_server(self):
756         """Start a web server. Raise an error if it can't start or is already running.
757
758         Ports can stub this out if they don't need a websocket server to be running."""
759         assert not self._websocket_server, 'Already running a websocket server.'
760
761         server = websocket_server.PyWebSocket(self, self.results_directory())
762         server.start()
763         self._websocket_server = server
764
765     def acquire_http_lock(self):
766         self._http_lock = http_lock.HttpLock(None, filesystem=self._filesystem, executive=self._executive)
767         self._http_lock.wait_for_httpd_lock()
768
769     def stop_helper(self):
770         """Shut down the test helper if it is running. Do nothing if
771         it isn't, or it isn't available. If a port overrides start_helper()
772         it must override this routine as well."""
773         pass
774
775     def stop_http_server(self):
776         """Shut down the http server if it is running. Do nothing if it isn't."""
777         if self._http_server:
778             self._http_server.stop()
779             self._http_server = None
780
781     def stop_websocket_server(self):
782         """Shut down the websocket server if it is running. Do nothing if it isn't."""
783         if self._websocket_server:
784             self._websocket_server.stop()
785             self._websocket_server = None
786
787     def release_http_lock(self):
788         if self._http_lock:
789             self._http_lock.cleanup_http_lock()
790
791     def exit_code_from_summarized_results(self, unexpected_results):
792         """Given summarized results, compute the exit code to be returned by new-run-webkit-tests.
793         Bots turn red when this function returns a non-zero value. By default, return the number of regressions
794         to avoid turning bots red by flaky failures, unexpected passes, and missing results"""
795         # Don't turn bots red for flaky failures, unexpected passes, and missing results.
796         return unexpected_results['num_regressions']
797
798     #
799     # TEST EXPECTATION-RELATED METHODS
800     #
801
802     def test_configuration(self):
803         """Returns the current TestConfiguration for the port."""
804         if not self._test_configuration:
805             self._test_configuration = TestConfiguration.from_port(self)
806         return self._test_configuration
807
808     # FIXME: Belongs on a Platform object.
809     @memoized
810     def all_test_configurations(self):
811         """Returns a list of TestConfiguration instances, representing all available
812         test configurations for this port."""
813         return self._generate_all_test_configurations()
814
815     # FIXME: Belongs on a Platform object.
816     def configuration_specifier_macros(self):
817         """Ports may provide a way to abbreviate configuration specifiers to conveniently
818         refer to them as one term or alias specific values to more generic ones. For example:
819
820         (xp, vista, win7) -> win # Abbreviate all Windows versions into one namesake.
821         (lucid) -> linux  # Change specific name of the Linux distro to a more generic term.
822
823         Returns a dictionary, each key representing a macro term ('win', for example),
824         and value being a list of valid configuration specifiers (such as ['xp', 'vista', 'win7'])."""
825         return {}
826
827     def all_baseline_variants(self):
828         """Returns a list of platform names sufficient to cover all the baselines.
829
830         The list should be sorted so that a later platform  will reuse
831         an earlier platform's baselines if they are the same (e.g.,
832         'snowleopard' should precede 'leopard')."""
833         raise NotImplementedError
834
835     def uses_test_expectations_file(self):
836         # This is different from checking test_expectations() is None, because
837         # some ports have Skipped files which are returned as part of test_expectations().
838         return self._filesystem.exists(self.path_to_test_expectations_file())
839
840     def test_expectations(self):
841         """Returns the test expectations for this port.
842
843         Basically this string should contain the equivalent of a
844         test_expectations file. See test_expectations.py for more details."""
845         return self._filesystem.read_text_file(self.path_to_test_expectations_file())
846
847     def test_expectations_overrides(self):
848         """Returns an optional set of overrides for the test_expectations.
849
850         This is used by ports that have code in two repositories, and where
851         it is possible that you might need "downstream" expectations that
852         temporarily override the "upstream" expectations until the port can
853         sync up the two repos."""
854         return None
855
856     def test_repository_paths(self):
857         """Returns a list of (repository_name, repository_path) tuples
858         of its depending code base.  By default it returns a list that only
859         contains a ('webkit', <webkitRepossitoryPath>) tuple.
860         """
861         return [('webkit', self.layout_tests_dir())]
862
863
864     _WDIFF_DEL = '##WDIFF_DEL##'
865     _WDIFF_ADD = '##WDIFF_ADD##'
866     _WDIFF_END = '##WDIFF_END##'
867
868     def _format_wdiff_output_as_html(self, wdiff):
869         wdiff = cgi.escape(wdiff)
870         wdiff = wdiff.replace(self._WDIFF_DEL, "<span class=del>")
871         wdiff = wdiff.replace(self._WDIFF_ADD, "<span class=add>")
872         wdiff = wdiff.replace(self._WDIFF_END, "</span>")
873         html = "<head><style>.del { background: #faa; } "
874         html += ".add { background: #afa; }</style></head>"
875         html += "<pre>%s</pre>" % wdiff
876         return html
877
878     def _wdiff_command(self, actual_filename, expected_filename):
879         executable = self._path_to_wdiff()
880         return [executable,
881                 "--start-delete=%s" % self._WDIFF_DEL,
882                 "--end-delete=%s" % self._WDIFF_END,
883                 "--start-insert=%s" % self._WDIFF_ADD,
884                 "--end-insert=%s" % self._WDIFF_END,
885                 actual_filename,
886                 expected_filename]
887
888     @staticmethod
889     def _handle_wdiff_error(script_error):
890         # Exit 1 means the files differed, any other exit code is an error.
891         if script_error.exit_code != 1:
892             raise script_error
893
894     def _run_wdiff(self, actual_filename, expected_filename):
895         """Runs wdiff and may throw exceptions.
896         This is mostly a hook for unit testing."""
897         # Diffs are treated as binary as they may include multiple files
898         # with conflicting encodings.  Thus we do not decode the output.
899         command = self._wdiff_command(actual_filename, expected_filename)
900         wdiff = self._executive.run_command(command, decode_output=False,
901             error_handler=self._handle_wdiff_error)
902         return self._format_wdiff_output_as_html(wdiff)
903
904     def wdiff_text(self, actual_filename, expected_filename):
905         """Returns a string of HTML indicating the word-level diff of the
906         contents of the two filenames. Returns an empty string if word-level
907         diffing isn't available."""
908         if not self._wdiff_available:
909             return ""
910         try:
911             # It's possible to raise a ScriptError we pass wdiff invalid paths.
912             return self._run_wdiff(actual_filename, expected_filename)
913         except OSError, e:
914             if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
915                 # Silently ignore cases where wdiff is missing.
916                 self._wdiff_available = False
917                 return ""
918             raise
919
920     # This is a class variable so we can test error output easily.
921     _pretty_patch_error_html = "Failed to run PrettyPatch, see error log."
922
923     def pretty_patch_text(self, diff_path):
924         if self._pretty_patch_available is None:
925             self._pretty_patch_available = self.check_pretty_patch(logging=False)
926         if not self._pretty_patch_available:
927             return self._pretty_patch_error_html
928         command = ("ruby", "-I", self._filesystem.dirname(self._pretty_patch_path),
929                    self._pretty_patch_path, diff_path)
930         try:
931             # Diffs are treated as binary (we pass decode_output=False) as they
932             # may contain multiple files of conflicting encodings.
933             return self._executive.run_command(command, decode_output=False)
934         except OSError, e:
935             # If the system is missing ruby log the error and stop trying.
936             self._pretty_patch_available = False
937             _log.error("Failed to run PrettyPatch (%s): %s" % (command, e))
938             return self._pretty_patch_error_html
939         except ScriptError, e:
940             # If ruby failed to run for some reason, log the command
941             # output and stop trying.
942             self._pretty_patch_available = False
943             _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, e.message_with_output()))
944             return self._pretty_patch_error_html
945
946     def default_configuration(self):
947         return self._config.default_configuration()
948
949     #
950     # PROTECTED ROUTINES
951     #
952     # The routines below should only be called by routines in this class
953     # or any of its subclasses.
954     #
955
956     def _uses_apache(self):
957         return True
958
959     def _path_to_apache(self):
960         """Returns the full path to the apache binary.
961
962         This is needed only by ports that use the apache_http_server module."""
963         raise NotImplementedError('Port.path_to_apache')
964
965     def _path_to_apache_config_file(self):
966         """Returns the full path to the apache binary.
967
968         This is needed only by ports that use the apache_http_server module."""
969         raise NotImplementedError('Port.path_to_apache_config_file')
970
971     def _path_to_driver(self, configuration=None):
972         """Returns the full path to the test driver (DumpRenderTree)."""
973         raise NotImplementedError('Port._path_to_driver')
974
975     def _path_to_webcore_library(self):
976         """Returns the full path to a built copy of WebCore."""
977         raise NotImplementedError('Port.path_to_webcore_library')
978
979     def _path_to_helper(self):
980         """Returns the full path to the layout_test_helper binary, which
981         is used to help configure the system for the test run, or None
982         if no helper is needed.
983
984         This is likely only used by start/stop_helper()."""
985         raise NotImplementedError('Port._path_to_helper')
986
987     def _path_to_image_diff(self):
988         """Returns the full path to the image_diff binary, or None if it is not available.
989
990         This is likely used only by diff_image()"""
991         raise NotImplementedError('Port.path_to_image_diff')
992
993     def _path_to_lighttpd(self):
994         """Returns the path to the LigHTTPd binary.
995
996         This is needed only by ports that use the http_server.py module."""
997         raise NotImplementedError('Port._path_to_lighttpd')
998
999     def _path_to_lighttpd_modules(self):
1000         """Returns the path to the LigHTTPd modules directory.
1001
1002         This is needed only by ports that use the http_server.py module."""
1003         raise NotImplementedError('Port._path_to_lighttpd_modules')
1004
1005     def _path_to_lighttpd_php(self):
1006         """Returns the path to the LigHTTPd PHP executable.
1007
1008         This is needed only by ports that use the http_server.py module."""
1009         raise NotImplementedError('Port._path_to_lighttpd_php')
1010
1011     def _path_to_wdiff(self):
1012         """Returns the full path to the wdiff binary, or None if it is not available.
1013
1014         This is likely used only by wdiff_text()"""
1015         raise NotImplementedError('Port._path_to_wdiff')
1016
1017     def _webkit_baseline_path(self, platform):
1018         """Return the  full path to the top of the baseline tree for a
1019         given platform."""
1020         return self._filesystem.join(self.layout_tests_dir(), 'platform', platform)
1021
1022     # FIXME: Belongs on a Platform object.
1023     def _generate_all_test_configurations(self):
1024         """Generates a list of TestConfiguration instances, representing configurations
1025         for a platform across all OSes, architectures, build and graphics types."""
1026         raise NotImplementedError('Port._generate_test_configurations')
1027
1028     def _driver_class(self):
1029         """Returns the port's driver implementation."""
1030         raise NotImplementedError('Port._driver_class')
1031
1032
1033 # When collecting test cases, we include any file with these extensions.
1034 _supported_file_extensions = set(['.html', '.shtml', '.xml', '.xhtml', '.pl', '.htm', '.php', '.svg', '.mht'])
1035
1036
1037 def is_reference_html_file(filesystem, dirname, filename):
1038     if filename.startswith('ref-') or filename.endswith('notref-'):
1039         return True
1040     filename_wihout_ext, unused = filesystem.splitext(filename)
1041     for suffix in ['-expected', '-expected-mismatch', '-ref', '-notref']:
1042         if filename_wihout_ext.endswith(suffix):
1043             return True
1044     return False
1045
1046
1047 def _has_supported_extension(filesystem, filename):
1048     """Return true if filename is one of the file extensions we want to run a test on."""
1049     extension = filesystem.splitext(filename)[1]
1050     return extension in _supported_file_extensions
1051
1052
1053 def _is_test_file(filesystem, dirname, filename):
1054     return _has_supported_extension(filesystem, filename) and not is_reference_html_file(filesystem, dirname, filename)
1055
1056
1057 def _parse_reftest_list(filesystem, test_dirpath):
1058     reftest_list_path = filesystem.join(test_dirpath, 'reftest.list')
1059     if not filesystem.isfile(reftest_list_path):
1060         return None
1061     reftest_list_file = filesystem.read_text_file(reftest_list_path)
1062
1063     parsed_list = dict()
1064     for line in reftest_list_file.split('\n'):
1065         line = re.sub('#.+$', '', line)
1066         split_line = line.split()
1067         if len(split_line) < 3:
1068             continue
1069         expectation_type, test_file, ref_file = split_line
1070         parsed_list[filesystem.join(test_dirpath, test_file)] = (expectation_type, filesystem.join(test_dirpath, ref_file))
1071     return parsed_list