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