1ca465c316a78d0e44a9e126ab1d9ccdfa40b02e
[WebKit-https.git] / WebKitTools / 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 import cgi
34 import difflib
35 import errno
36 import os
37 import shlex
38 import subprocess
39 import sys
40 import time
41
42 import apache_http_server
43 import http_server
44 import websocket_server
45
46 from webkitpy.common.system import logutils
47 from webkitpy.common.system.executive import Executive, ScriptError
48
49
50 _log = logutils.get_logger(__file__)
51
52
53 # Python's Popen has a bug that causes any pipes opened to a
54 # process that can't be executed to be leaked.  Since this
55 # code is specifically designed to tolerate exec failures
56 # to gracefully handle cases where wdiff is not installed,
57 # the bug results in a massive file descriptor leak. As a
58 # workaround, if an exec failure is ever experienced for
59 # wdiff, assume it's not available.  This will leak one
60 # file descriptor but that's better than leaking each time
61 # wdiff would be run.
62 #
63 # http://mail.python.org/pipermail/python-list/
64 #    2008-August/505753.html
65 # http://bugs.python.org/issue3210
66 _wdiff_available = True
67 _pretty_patch_available = True
68
69 # FIXME: This class should merge with webkitpy.webkit_port at some point.
70 class Port(object):
71     """Abstract class for Port-specific hooks for the layout_test package.
72     """
73
74     @staticmethod
75     def flag_from_configuration(configuration):
76         flags_by_configuration = {
77             "Debug": "--debug",
78             "Release": "--release",
79         }
80         return flags_by_configuration[configuration]
81
82     def __init__(self, port_name=None, options=None, executive=Executive()):
83         self._name = port_name
84         self._options = options
85         self._helper = None
86         self._http_server = None
87         self._webkit_base_dir = None
88         self._websocket_server = None
89         self._executive = executive
90
91     def default_child_processes(self):
92         """Return the number of DumpRenderTree instances to use for this
93         port."""
94         return self._executive.cpu_count()
95
96     def baseline_path(self):
97         """Return the absolute path to the directory to store new baselines
98         in for this port."""
99         raise NotImplementedError('Port.baseline_path')
100
101     def baseline_search_path(self):
102         """Return a list of absolute paths to directories to search under for
103         baselines. The directories are searched in order."""
104         raise NotImplementedError('Port.baseline_search_path')
105
106     def check_build(self, needs_http):
107         """This routine is used to ensure that the build is up to date
108         and all the needed binaries are present."""
109         raise NotImplementedError('Port.check_build')
110
111     def check_sys_deps(self, needs_http):
112         """If the port needs to do some runtime checks to ensure that the
113         tests can be run successfully, it should override this routine.
114         This step can be skipped with --nocheck-sys-deps.
115
116         Returns whether the system is properly configured."""
117         return True
118
119     def check_image_diff(self, override_step=None, logging=True):
120         """This routine is used to check whether image_diff binary exists."""
121         raise NotImplemented('Port.check_image_diff')
122
123     def compare_text(self, expected_text, actual_text):
124         """Return whether or not the two strings are *not* equal. This
125         routine is used to diff text output.
126
127         While this is a generic routine, we include it in the Port
128         interface so that it can be overriden for testing purposes."""
129         return expected_text != actual_text
130
131     def diff_image(self, expected_filename, actual_filename,
132                    diff_filename=None):
133         """Compare two image files and produce a delta image file.
134
135         Return True if the two files are different, False if they are the same.
136         Also produce a delta image of the two images and write that into
137         |diff_filename| if it is not None.
138
139         While this is a generic routine, we include it in the Port
140         interface so that it can be overriden for testing purposes."""
141         executable = self._path_to_image_diff()
142
143         if diff_filename:
144             cmd = [executable, '--diff', expected_filename, actual_filename,
145                    diff_filename]
146         else:
147             cmd = [executable, expected_filename, actual_filename]
148
149         result = True
150         try:
151             if subprocess.call(cmd) == 0:
152                 return False
153         except OSError, e:
154             if e.errno == errno.ENOENT or e.errno == errno.EACCES:
155                 _compare_available = False
156             else:
157                 raise e
158         except ValueError:
159             # work around a race condition in Python 2.4's implementation
160             # of subprocess.Popen. See http://bugs.python.org/issue1199282 .
161             pass
162         return result
163
164     def diff_text(self, expected_text, actual_text,
165                   expected_filename, actual_filename):
166         """Returns a string containing the diff of the two text strings
167         in 'unified diff' format.
168
169         While this is a generic routine, we include it in the Port
170         interface so that it can be overriden for testing purposes."""
171         diff = difflib.unified_diff(expected_text.splitlines(True),
172                                     actual_text.splitlines(True),
173                                     expected_filename,
174                                     actual_filename)
175         return ''.join(diff)
176
177     def driver_name(self):
178         """Returns the name of the actual binary that is performing the test,
179         so that it can be referred to in log messages. In most cases this
180         will be DumpRenderTree, but if a port uses a binary with a different
181         name, it can be overridden here."""
182         return "DumpRenderTree"
183
184     def expected_baselines(self, filename, suffix, all_baselines=False):
185         """Given a test name, finds where the baseline results are located.
186
187         Args:
188         filename: absolute filename to test file
189         suffix: file suffix of the expected results, including dot; e.g.
190             '.txt' or '.png'.  This should not be None, but may be an empty
191             string.
192         all_baselines: If True, return an ordered list of all baseline paths
193             for the given platform. If False, return only the first one.
194         Returns
195         a list of ( platform_dir, results_filename ), where
196             platform_dir - abs path to the top of the results tree (or test
197                 tree)
198             results_filename - relative path from top of tree to the results
199                 file
200             (os.path.join of the two gives you the full path to the file,
201                 unless None was returned.)
202         Return values will be in the format appropriate for the current
203         platform (e.g., "\\" for path separators on Windows). If the results
204         file is not found, then None will be returned for the directory,
205         but the expected relative pathname will still be returned.
206
207         This routine is generic but lives here since it is used in
208         conjunction with the other baseline and filename routines that are
209         platform specific.
210         """
211         testname = os.path.splitext(self.relative_test_filename(filename))[0]
212
213         baseline_filename = testname + '-expected' + suffix
214
215         baseline_search_path = self.baseline_search_path()
216
217         baselines = []
218         for platform_dir in baseline_search_path:
219             if os.path.exists(os.path.join(platform_dir, baseline_filename)):
220                 baselines.append((platform_dir, baseline_filename))
221
222             if not all_baselines and baselines:
223                 return baselines
224
225         # If it wasn't found in a platform directory, return the expected
226         # result in the test directory, even if no such file actually exists.
227         platform_dir = self.layout_tests_dir()
228         if os.path.exists(os.path.join(platform_dir, baseline_filename)):
229             baselines.append((platform_dir, baseline_filename))
230
231         if baselines:
232             return baselines
233
234         return [(None, baseline_filename)]
235
236     def expected_filename(self, filename, suffix):
237         """Given a test name, returns an absolute path to its expected results.
238
239         If no expected results are found in any of the searched directories,
240         the directory in which the test itself is located will be returned.
241         The return value is in the format appropriate for the platform
242         (e.g., "\\" for path separators on windows).
243
244         Args:
245         filename: absolute filename to test file
246         suffix: file suffix of the expected results, including dot; e.g. '.txt'
247             or '.png'.  This should not be None, but may be an empty string.
248         platform: the most-specific directory name to use to build the
249             search list of directories, e.g., 'chromium-win', or
250             'chromium-mac-leopard' (we follow the WebKit format)
251
252         This routine is generic but is implemented here to live alongside
253         the other baseline and filename manipulation routines.
254         """
255         platform_dir, baseline_filename = self.expected_baselines(
256             filename, suffix)[0]
257         if platform_dir:
258             return os.path.join(platform_dir, baseline_filename)
259         return os.path.join(self.layout_tests_dir(), baseline_filename)
260
261     def filename_to_uri(self, filename):
262         """Convert a test file to a URI."""
263         LAYOUTTEST_HTTP_DIR = "http/tests/"
264         LAYOUTTEST_WEBSOCKET_DIR = "websocket/tests/"
265
266         relative_path = self.relative_test_filename(filename)
267         port = None
268         use_ssl = False
269
270         if relative_path.startswith(LAYOUTTEST_HTTP_DIR):
271             # http/tests/ run off port 8000 and ssl/ off 8443
272             relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):]
273             port = 8000
274         elif relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR):
275             # websocket/tests/ run off port 8880 and 9323
276             # Note: the root is /, not websocket/tests/
277             port = 8880
278
279         # Make http/tests/local run as local files. This is to mimic the
280         # logic in run-webkit-tests.
281         #
282         # TODO(dpranke): remove the media reference and the SSL reference?
283         if (port and not relative_path.startswith("local/") and
284             not relative_path.startswith("media/")):
285             if relative_path.startswith("ssl/"):
286                 port += 443
287                 protocol = "https"
288             else:
289                 protocol = "http"
290             return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path)
291
292         if sys.platform in ('cygwin', 'win32'):
293             return "file:///" + self.get_absolute_path(filename)
294         return "file://" + self.get_absolute_path(filename)
295
296     def get_absolute_path(self, filename):
297         """Return the absolute path in unix format for the given filename.
298
299         This routine exists so that platforms that don't use unix filenames
300         can convert accordingly."""
301         return os.path.abspath(filename)
302
303     def layout_tests_dir(self):
304         """Return the absolute path to the top of the LayoutTests directory."""
305         return self.path_from_webkit_base('LayoutTests')
306
307     def maybe_make_directory(self, *path):
308         """Creates the specified directory if it doesn't already exist."""
309         try:
310             os.makedirs(os.path.join(*path))
311         except OSError, e:
312             if e.errno != errno.EEXIST:
313                 raise
314
315     def name(self):
316         """Return the name of the port (e.g., 'mac', 'chromium-win-xp').
317
318         Note that this is different from the test_platform_name(), which
319         may be different (e.g., 'win-xp' instead of 'chromium-win-xp'."""
320         return self._name
321
322     # FIXME: This could be replaced by functions in webkitpy.common.checkout.scm.
323     def path_from_webkit_base(self, *comps):
324         """Returns the full path to path made by joining the top of the
325         WebKit source tree and the list of path components in |*comps|."""
326         if not self._webkit_base_dir:
327             abspath = os.path.abspath(__file__)
328             self._webkit_base_dir = abspath[0:abspath.find('WebKitTools')]
329             _log.debug("Using WebKit root: %s" % self._webkit_base_dir)
330
331         return os.path.join(self._webkit_base_dir, *comps)
332
333     # FIXME: Callers should eventually move to scm.script_path.
334     def script_path(self, script_name):
335         return self.path_from_webkit_base("WebKitTools", "Scripts", script_name)
336
337     def path_to_test_expectations_file(self):
338         """Update the test expectations to the passed-in string.
339
340         This is used by the rebaselining tool. Raises NotImplementedError
341         if the port does not use expectations files."""
342         raise NotImplementedError('Port.path_to_test_expectations_file')
343
344     def remove_directory(self, *path):
345         """Recursively removes a directory, even if it's marked read-only.
346
347         Remove the directory located at *path, if it exists.
348
349         shutil.rmtree() doesn't work on Windows if any of the files
350         or directories are read-only, which svn repositories and
351         some .svn files are.  We need to be able to force the files
352         to be writable (i.e., deletable) as we traverse the tree.
353
354         Even with all this, Windows still sometimes fails to delete a file,
355         citing a permission error (maybe something to do with antivirus
356         scans or disk indexing).  The best suggestion any of the user
357         forums had was to wait a bit and try again, so we do that too.
358         It's hand-waving, but sometimes it works. :/
359         """
360         file_path = os.path.join(*path)
361         if not os.path.exists(file_path):
362             return
363
364         win32 = False
365         if sys.platform == 'win32':
366             win32 = True
367             # Some people don't have the APIs installed. In that case we'll do
368             # without.
369             try:
370                 win32api = __import__('win32api')
371                 win32con = __import__('win32con')
372             except ImportError:
373                 win32 = False
374
375             def remove_with_retry(rmfunc, path):
376                 os.chmod(path, os.stat.S_IWRITE)
377                 if win32:
378                     win32api.SetFileAttributes(path,
379                                               win32con.FILE_ATTRIBUTE_NORMAL)
380                 try:
381                     return rmfunc(path)
382                 except EnvironmentError, e:
383                     if e.errno != errno.EACCES:
384                         raise
385                     print 'Failed to delete %s: trying again' % repr(path)
386                     time.sleep(0.1)
387                     return rmfunc(path)
388         else:
389
390             def remove_with_retry(rmfunc, path):
391                 if os.path.islink(path):
392                     return os.remove(path)
393                 else:
394                     return rmfunc(path)
395
396         for root, dirs, files in os.walk(file_path, topdown=False):
397             # For POSIX:  making the directory writable guarantees
398             # removability. Windows will ignore the non-read-only
399             # bits in the chmod value.
400             os.chmod(root, 0770)
401             for name in files:
402                 remove_with_retry(os.remove, os.path.join(root, name))
403             for name in dirs:
404                 remove_with_retry(os.rmdir, os.path.join(root, name))
405
406         remove_with_retry(os.rmdir, file_path)
407
408     def test_platform_name(self):
409         return self._name
410
411     def relative_test_filename(self, filename):
412         """Relative unix-style path for a filename under the LayoutTests
413         directory. Filenames outside the LayoutTests directory should raise
414         an error."""
415         return filename[len(self.layout_tests_dir()) + 1:]
416
417     def results_directory(self):
418         """Absolute path to the place to store the test results."""
419         raise NotImplemented('Port.results_directory')
420
421     def setup_test_run(self):
422         """This routine can be overridden to perform any port-specific
423         work that shouuld be done at the beginning of a test run."""
424         pass
425
426     def show_html_results_file(self, results_filename):
427         """This routine should display the HTML file pointed at by
428         results_filename in a users' browser."""
429         raise NotImplementedError('Port.show_html_results_file')
430
431     def create_driver(self, png_path, options):
432         """Return a newly created base.Driver subclass for starting/stopping
433         the test driver."""
434         raise NotImplementedError('Port.create_driver')
435
436     def start_helper(self):
437         """If a port needs to reconfigure graphics settings or do other
438         things to ensure a known test configuration, it should override this
439         method."""
440         pass
441
442     def start_http_server(self):
443         """Start a web server if it is available. Do nothing if
444         it isn't. This routine is allowed to (and may) fail if a server
445         is already running."""
446         if self._options.use_apache:
447             self._http_server = apache_http_server.LayoutTestApacheHttpd(self,
448                 self._options.results_directory)
449         else:
450             self._http_server = http_server.Lighttpd(self,
451                 self._options.results_directory)
452         self._http_server.start()
453
454     def start_websocket_server(self):
455         """Start a websocket server if it is available. Do nothing if
456         it isn't. This routine is allowed to (and may) fail if a server
457         is already running."""
458         self._websocket_server = websocket_server.PyWebSocket(self,
459             self._options.results_directory)
460         self._websocket_server.start()
461
462     def stop_helper(self):
463         """Shut down the test helper if it is running. Do nothing if
464         it isn't, or it isn't available. If a port overrides start_helper()
465         it must override this routine as well."""
466         pass
467
468     def stop_http_server(self):
469         """Shut down the http server if it is running. Do nothing if
470         it isn't, or it isn't available."""
471         if self._http_server:
472             self._http_server.stop()
473
474     def stop_websocket_server(self):
475         """Shut down the websocket server if it is running. Do nothing if
476         it isn't, or it isn't available."""
477         if self._websocket_server:
478             self._websocket_server.stop()
479
480     def test_expectations(self):
481         """Returns the test expectations for this port.
482
483         Basically this string should contain the equivalent of a
484         test_expectations file. See test_expectations.py for more details."""
485         raise NotImplementedError('Port.test_expectations')
486
487     def test_expectations_overrides(self):
488         """Returns an optional set of overrides for the test_expectations.
489
490         This is used by ports that have code in two repositories, and where
491         it is possible that you might need "downstream" expectations that
492         temporarily override the "upstream" expectations until the port can
493         sync up the two repos."""
494         return None
495
496     def test_base_platform_names(self):
497         """Return a list of the 'base' platforms on your port. The base
498         platforms represent different architectures, operating systems,
499         or implementations (as opposed to different versions of a single
500         platform). For example, 'mac' and 'win' might be different base
501         platforms, wherease 'mac-tiger' and 'mac-leopard' might be
502         different platforms. This routine is used by the rebaselining tool
503         and the dashboards, and the strings correspond to the identifiers
504         in your test expectations (*not* necessarily the platform names
505         themselves)."""
506         raise NotImplementedError('Port.base_test_platforms')
507
508     def test_platform_name(self):
509         """Returns the string that corresponds to the given platform name
510         in the test expectations. This may be the same as name(), or it
511         may be different. For example, chromium returns 'mac' for
512         'chromium-mac'."""
513         raise NotImplementedError('Port.test_platform_name')
514
515     def test_platforms(self):
516         """Returns the list of test platform identifiers as used in the
517         test_expectations and on dashboards, the rebaselining tool, etc.
518
519         Note that this is not necessarily the same as the list of ports,
520         which must be globally unique (e.g., both 'chromium-mac' and 'mac'
521         might return 'mac' as a test_platform name'."""
522         raise NotImplementedError('Port.platforms')
523
524     def test_platform_name_to_name(self, test_platform_name):
525         """Returns the Port platform name that corresponds to the name as
526         referenced in the expectations file. E.g., "mac" returns
527         "chromium-mac" on the Chromium ports."""
528         raise NotImplementedError('Port.test_platform_name_to_name')
529
530     def version(self):
531         """Returns a string indicating the version of a given platform, e.g.
532         '-leopard' or '-xp'.
533
534         This is used to help identify the exact port when parsing test
535         expectations, determining search paths, and logging information."""
536         raise NotImplementedError('Port.version')
537
538     def wdiff_text(self, actual_filename, expected_filename):
539         """Returns a string of HTML indicating the word-level diff of the
540         contents of the two filenames. Returns an empty string if word-level
541         diffing isn't available."""
542         executable = self._path_to_wdiff()
543         cmd = [executable,
544                '--start-delete=##WDIFF_DEL##',
545                '--end-delete=##WDIFF_END##',
546                '--start-insert=##WDIFF_ADD##',
547                '--end-insert=##WDIFF_END##',
548                actual_filename,
549                expected_filename]
550         global _wdiff_available  # See explaination at top of file.
551         result = ''
552         try:
553             if _wdiff_available:
554                 wdiff = self._executive.run_command(cmd, decode_output=False)
555                 wdiff = cgi.escape(wdiff)
556                 wdiff = wdiff.replace('##WDIFF_DEL##', '<span class=del>')
557                 wdiff = wdiff.replace('##WDIFF_ADD##', '<span class=add>')
558                 wdiff = wdiff.replace('##WDIFF_END##', '</span>')
559                 result = '<head><style>.del { background: #faa; } '
560                 result += '.add { background: #afa; }</style></head>'
561                 result += '<pre>' + wdiff + '</pre>'
562         except OSError, e:
563             if (e.errno == errno.ENOENT or e.errno == errno.EACCES or
564                 e.errno == errno.ECHILD):
565                 _wdiff_available = False
566             else:
567                 raise e
568         # Diffs are treated as binary as they may include multiple files
569         # with conflicting encodings.  Thus we do not decode the output here.
570         return result
571
572     _pretty_patch_error_html = "Failed to run PrettyPatch, see error console."
573
574     def pretty_patch_text(self, diff_path):
575         global _pretty_patch_available
576         if not _pretty_patch_available:
577             return self._pretty_patch_error_html
578         pretty_patch_path = self.path_from_webkit_base("BugsSite", "PrettyPatch")
579         prettify_path = os.path.join(pretty_patch_path, "prettify.rb")
580         command = ["ruby", "-I", pretty_patch_path, prettify_path, diff_path]
581         try:
582             # Diffs are treated as binary (we pass decode_output=False) as they
583             # may contain multiple files of conflicting encodings.
584             return self._executive.run_command(command, decode_output=False)
585         except OSError, e:
586             # If the system is missing ruby log the error and stop trying.
587             _pretty_patch_available = False
588             _log.error("Failed to run PrettyPatch (%s): %s" % (command, e))
589             return self._pretty_patch_error_html
590         except ScriptError, e:
591             # If ruby failed to run for some reason, log the command output and stop trying.
592             _pretty_patch_available = False
593             _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, e.message_with_output()))
594             return self._pretty_patch_error_html
595
596     def default_configuration(self):
597         return "Release"
598
599     #
600     # PROTECTED ROUTINES
601     #
602     # The routines below should only be called by routines in this class
603     # or any of its subclasses.
604     #
605
606     def _path_to_apache(self):
607         """Returns the full path to the apache binary.
608
609         This is needed only by ports that use the apache_http_server module."""
610         raise NotImplementedError('Port.path_to_apache')
611
612     def _path_to_apache_config_file(self):
613         """Returns the full path to the apache binary.
614
615         This is needed only by ports that use the apache_http_server module."""
616         raise NotImplementedError('Port.path_to_apache_config_file')
617
618     def _path_to_driver(self, configuration=None):
619         """Returns the full path to the test driver (DumpRenderTree)."""
620         raise NotImplementedError('Port.path_to_driver')
621
622     def _path_to_helper(self):
623         """Returns the full path to the layout_test_helper binary, which
624         is used to help configure the system for the test run, or None
625         if no helper is needed.
626
627         This is likely only used by start/stop_helper()."""
628         raise NotImplementedError('Port._path_to_helper')
629
630     def _path_to_image_diff(self):
631         """Returns the full path to the image_diff binary, or None if it
632         is not available.
633
634         This is likely used only by diff_image()"""
635         raise NotImplementedError('Port.path_to_image_diff')
636
637     def _path_to_lighttpd(self):
638         """Returns the path to the LigHTTPd binary.
639
640         This is needed only by ports that use the http_server.py module."""
641         raise NotImplementedError('Port._path_to_lighttpd')
642
643     def _path_to_lighttpd_modules(self):
644         """Returns the path to the LigHTTPd modules directory.
645
646         This is needed only by ports that use the http_server.py module."""
647         raise NotImplementedError('Port._path_to_lighttpd_modules')
648
649     def _path_to_lighttpd_php(self):
650         """Returns the path to the LigHTTPd PHP executable.
651
652         This is needed only by ports that use the http_server.py module."""
653         raise NotImplementedError('Port._path_to_lighttpd_php')
654
655     def _path_to_wdiff(self):
656         """Returns the full path to the wdiff binary, or None if it is
657         not available.
658
659         This is likely used only by wdiff_text()"""
660         raise NotImplementedError('Port._path_to_wdiff')
661
662     def _shut_down_http_server(self, pid):
663         """Forcefully and synchronously kills the web server.
664
665         This routine should only be called from http_server.py or its
666         subclasses."""
667         raise NotImplementedError('Port._shut_down_http_server')
668
669     def _webkit_baseline_path(self, platform):
670         """Return the  full path to the top of the baseline tree for a
671         given platform."""
672         return os.path.join(self.layout_tests_dir(), 'platform',
673                             platform)
674
675
676 class Driver:
677     """Abstract interface for the DumpRenderTree interface."""
678
679     def __init__(self, port, png_path, options):
680         """Initialize a Driver to subsequently run tests.
681
682         Typically this routine will spawn DumpRenderTree in a config
683         ready for subsequent input.
684
685         port - reference back to the port object.
686         png_path - an absolute path for the driver to write any image
687             data for a test (as a PNG). If no path is provided, that
688             indicates that pixel test results will not be checked.
689         options - any port-specific driver options."""
690         raise NotImplementedError('Driver.__init__')
691
692     def run_test(self, uri, timeout, checksum):
693         """Run a single test and return the results.
694
695         Note that it is okay if a test times out or crashes and leaves
696         the driver in an indeterminate state. The upper layers of the program
697         are responsible for cleaning up and ensuring things are okay.
698
699         uri - a full URI for the given test
700         timeout - number of milliseconds to wait before aborting this test.
701         checksum - if present, the expected checksum for the image for this
702             test
703
704         Returns a tuple of the following:
705             crash - a boolean indicating whether the driver crashed on the test
706             timeout - a boolean indicating whehter the test timed out
707             checksum - a string containing the checksum of the image, if
708                 present
709             output - any text output
710             error - any unexpected or additional (or error) text output
711
712         Note that the image itself should be written to the path that was
713         specified in the __init__() call."""
714         raise NotImplementedError('Driver.run_test')
715
716     # FIXME: This is static so we can test it w/o creating a Base instance.
717     @classmethod
718     def _command_wrapper(cls, wrapper_option):
719         # Hook for injecting valgrind or other runtime instrumentation,
720         # used by e.g. tools/valgrind/valgrind_tests.py.
721         wrapper = []
722         browser_wrapper = os.environ.get("BROWSER_WRAPPER", None)
723         if browser_wrapper:
724             # FIXME: There seems to be no reason to use BROWSER_WRAPPER over --wrapper.
725             # Remove this code any time after the date listed below.
726             _log.error("BROWSER_WRAPPER is deprecated, please use --wrapper instead.")
727             _log.error("BROWSER_WRAPPER will be removed any time after June 1st 2010 and your scripts will break.")
728             wrapper += [browser_wrapper]
729
730         if wrapper_option:
731             wrapper += shlex.split(wrapper_option)
732         return wrapper
733
734     def poll(self):
735         """Returns None if the Driver is still running. Returns the returncode
736         if it has exited."""
737         raise NotImplementedError('Driver.poll')
738
739     def returncode(self):
740         """Returns the system-specific returncode if the Driver has stopped or
741         exited."""
742         raise NotImplementedError('Driver.returncode')
743
744     def stop(self):
745         raise NotImplementedError('Driver.stop')