4c0472d5d035f2ef69d562733be98b42e8ba1ec9
[WebKit.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 codecs
35 import difflib
36 import errno
37 import os
38 import shlex
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, tolerance=0):
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         |tolerance| should be a percentage value (0.0 - 100.0).
140         If it is omitted, the port default tolerance value is used.
141
142         While this is a generic routine, we include it in the Port
143         interface so that it can be overriden for testing purposes."""
144         executable = self._path_to_image_diff()
145
146         if diff_filename:
147             cmd = [executable, '--diff', expected_filename, actual_filename,
148                    diff_filename]
149         else:
150             cmd = [executable, expected_filename, actual_filename]
151
152         result = True
153         try:
154             if self._executive.run_command(cmd, return_exit_code=True) == 0:
155                 return False
156         except OSError, e:
157             if e.errno == errno.ENOENT or e.errno == errno.EACCES:
158                 _compare_available = False
159             else:
160                 raise e
161         return result
162
163     def diff_text(self, expected_text, actual_text,
164                   expected_filename, actual_filename):
165         """Returns a string containing the diff of the two text strings
166         in 'unified diff' format.
167
168         While this is a generic routine, we include it in the Port
169         interface so that it can be overriden for testing purposes."""
170         diff = difflib.unified_diff(expected_text.splitlines(True),
171                                     actual_text.splitlines(True),
172                                     expected_filename,
173                                     actual_filename)
174         return ''.join(diff)
175
176     def driver_name(self):
177         """Returns the name of the actual binary that is performing the test,
178         so that it can be referred to in log messages. In most cases this
179         will be DumpRenderTree, but if a port uses a binary with a different
180         name, it can be overridden here."""
181         return "DumpRenderTree"
182
183     def expected_baselines(self, filename, suffix, all_baselines=False):
184         """Given a test name, finds where the baseline results are located.
185
186         Args:
187         filename: absolute filename to test file
188         suffix: file suffix of the expected results, including dot; e.g.
189             '.txt' or '.png'.  This should not be None, but may be an empty
190             string.
191         all_baselines: If True, return an ordered list of all baseline paths
192             for the given platform. If False, return only the first one.
193         Returns
194         a list of ( platform_dir, results_filename ), where
195             platform_dir - abs path to the top of the results tree (or test
196                 tree)
197             results_filename - relative path from top of tree to the results
198                 file
199             (os.path.join of the two gives you the full path to the file,
200                 unless None was returned.)
201         Return values will be in the format appropriate for the current
202         platform (e.g., "\\" for path separators on Windows). If the results
203         file is not found, then None will be returned for the directory,
204         but the expected relative pathname will still be returned.
205
206         This routine is generic but lives here since it is used in
207         conjunction with the other baseline and filename routines that are
208         platform specific.
209         """
210         testname = os.path.splitext(self.relative_test_filename(filename))[0]
211
212         baseline_filename = testname + '-expected' + suffix
213
214         baseline_search_path = self.baseline_search_path()
215
216         baselines = []
217         for platform_dir in baseline_search_path:
218             if os.path.exists(os.path.join(platform_dir, baseline_filename)):
219                 baselines.append((platform_dir, baseline_filename))
220
221             if not all_baselines and baselines:
222                 return baselines
223
224         # If it wasn't found in a platform directory, return the expected
225         # result in the test directory, even if no such file actually exists.
226         platform_dir = self.layout_tests_dir()
227         if os.path.exists(os.path.join(platform_dir, baseline_filename)):
228             baselines.append((platform_dir, baseline_filename))
229
230         if baselines:
231             return baselines
232
233         return [(None, baseline_filename)]
234
235     def expected_filename(self, filename, suffix):
236         """Given a test name, returns an absolute path to its expected results.
237
238         If no expected results are found in any of the searched directories,
239         the directory in which the test itself is located will be returned.
240         The return value is in the format appropriate for the platform
241         (e.g., "\\" for path separators on windows).
242
243         Args:
244         filename: absolute filename to test file
245         suffix: file suffix of the expected results, including dot; e.g. '.txt'
246             or '.png'.  This should not be None, but may be an empty string.
247         platform: the most-specific directory name to use to build the
248             search list of directories, e.g., 'chromium-win', or
249             'chromium-mac-leopard' (we follow the WebKit format)
250
251         This routine is generic but is implemented here to live alongside
252         the other baseline and filename manipulation routines.
253         """
254         platform_dir, baseline_filename = self.expected_baselines(
255             filename, suffix)[0]
256         if platform_dir:
257             return os.path.join(platform_dir, baseline_filename)
258         return os.path.join(self.layout_tests_dir(), baseline_filename)
259
260     def filename_to_uri(self, filename):
261         """Convert a test file to a URI."""
262         LAYOUTTEST_HTTP_DIR = "http/tests/"
263         LAYOUTTEST_WEBSOCKET_DIR = "websocket/tests/"
264
265         relative_path = self.relative_test_filename(filename)
266         port = None
267         use_ssl = False
268
269         if relative_path.startswith(LAYOUTTEST_HTTP_DIR):
270             # http/tests/ run off port 8000 and ssl/ off 8443
271             relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):]
272             port = 8000
273         elif relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR):
274             # websocket/tests/ run off port 8880 and 9323
275             # Note: the root is /, not websocket/tests/
276             port = 8880
277
278         # Make http/tests/local run as local files. This is to mimic the
279         # logic in run-webkit-tests.
280         #
281         # TODO(dpranke): remove the media reference and the SSL reference?
282         if (port and not relative_path.startswith("local/") and
283             not relative_path.startswith("media/")):
284             if relative_path.startswith("ssl/"):
285                 port += 443
286                 protocol = "https"
287             else:
288                 protocol = "http"
289             return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path)
290
291         if sys.platform in ('cygwin', 'win32'):
292             return "file:///" + self.get_absolute_path(filename)
293         return "file://" + self.get_absolute_path(filename)
294
295     def get_absolute_path(self, filename):
296         """Return the absolute path in unix format for the given filename.
297
298         This routine exists so that platforms that don't use unix filenames
299         can convert accordingly."""
300         return os.path.abspath(filename)
301
302     def layout_tests_dir(self):
303         """Return the absolute path to the top of the LayoutTests directory."""
304         return self.path_from_webkit_base('LayoutTests')
305
306     def maybe_make_directory(self, *path):
307         """Creates the specified directory if it doesn't already exist."""
308         try:
309             os.makedirs(os.path.join(*path))
310         except OSError, e:
311             if e.errno != errno.EEXIST:
312                 raise
313
314     def name(self):
315         """Return the name of the port (e.g., 'mac', 'chromium-win-xp').
316
317         Note that this is different from the test_platform_name(), which
318         may be different (e.g., 'win-xp' instead of 'chromium-win-xp'."""
319         return self._name
320
321     # FIXME: This could be replaced by functions in webkitpy.common.checkout.scm.
322     def path_from_webkit_base(self, *comps):
323         """Returns the full path to path made by joining the top of the
324         WebKit source tree and the list of path components in |*comps|."""
325         if not self._webkit_base_dir:
326             abspath = os.path.abspath(__file__)
327             self._webkit_base_dir = abspath[0:abspath.find('WebKitTools')]
328             _log.debug("Using WebKit root: %s" % self._webkit_base_dir)
329
330         return os.path.join(self._webkit_base_dir, *comps)
331
332     # FIXME: Callers should eventually move to scm.script_path.
333     def script_path(self, script_name):
334         return self.path_from_webkit_base("WebKitTools", "Scripts", script_name)
335
336     def path_to_test_expectations_file(self):
337         """Update the test expectations to the passed-in string.
338
339         This is used by the rebaselining tool. Raises NotImplementedError
340         if the port does not use expectations files."""
341         raise NotImplementedError('Port.path_to_test_expectations_file')
342
343     def remove_directory(self, *path):
344         """Recursively removes a directory, even if it's marked read-only.
345
346         Remove the directory located at *path, if it exists.
347
348         shutil.rmtree() doesn't work on Windows if any of the files
349         or directories are read-only, which svn repositories and
350         some .svn files are.  We need to be able to force the files
351         to be writable (i.e., deletable) as we traverse the tree.
352
353         Even with all this, Windows still sometimes fails to delete a file,
354         citing a permission error (maybe something to do with antivirus
355         scans or disk indexing).  The best suggestion any of the user
356         forums had was to wait a bit and try again, so we do that too.
357         It's hand-waving, but sometimes it works. :/
358         """
359         file_path = os.path.join(*path)
360         if not os.path.exists(file_path):
361             return
362
363         win32 = False
364         if sys.platform == 'win32':
365             win32 = True
366             # Some people don't have the APIs installed. In that case we'll do
367             # without.
368             try:
369                 win32api = __import__('win32api')
370                 win32con = __import__('win32con')
371             except ImportError:
372                 win32 = False
373
374             def remove_with_retry(rmfunc, path):
375                 os.chmod(path, os.stat.S_IWRITE)
376                 if win32:
377                     win32api.SetFileAttributes(path,
378                                               win32con.FILE_ATTRIBUTE_NORMAL)
379                 try:
380                     return rmfunc(path)
381                 except EnvironmentError, e:
382                     if e.errno != errno.EACCES:
383                         raise
384                     print 'Failed to delete %s: trying again' % repr(path)
385                     time.sleep(0.1)
386                     return rmfunc(path)
387         else:
388
389             def remove_with_retry(rmfunc, path):
390                 if os.path.islink(path):
391                     return os.remove(path)
392                 else:
393                     return rmfunc(path)
394
395         for root, dirs, files in os.walk(file_path, topdown=False):
396             # For POSIX:  making the directory writable guarantees
397             # removability. Windows will ignore the non-read-only
398             # bits in the chmod value.
399             os.chmod(root, 0770)
400             for name in files:
401                 remove_with_retry(os.remove, os.path.join(root, name))
402             for name in dirs:
403                 remove_with_retry(os.rmdir, os.path.join(root, name))
404
405         remove_with_retry(os.rmdir, file_path)
406
407     def test_platform_name(self):
408         return self._name
409
410     def relative_test_filename(self, filename):
411         """Relative unix-style path for a filename under the LayoutTests
412         directory. Filenames outside the LayoutTests directory should raise
413         an error."""
414         # FIXME This should assert() here but cannot due to printing_unittest.Testprinter
415         # assert(filename.startswith(self.layout_tests_dir()))
416         return filename[len(self.layout_tests_dir()) + 1:]
417
418     def results_directory(self):
419         """Absolute path to the place to store the test results."""
420         raise NotImplemented('Port.results_directory')
421
422     def setup_test_run(self):
423         """Perform port-specific work at the beginning of a test run."""
424         pass
425
426     def setup_environ_for_server(self):
427         """Perform port-specific work at the beginning of a server launch.
428
429         Returns:
430            Operating-system's environment.
431         """
432         return os.environ.copy()
433
434     def show_html_results_file(self, results_filename):
435         """This routine should display the HTML file pointed at by
436         results_filename in a users' browser."""
437         raise NotImplementedError('Port.show_html_results_file')
438
439     def create_driver(self, png_path, options):
440         """Return a newly created base.Driver subclass for starting/stopping
441         the test driver."""
442         raise NotImplementedError('Port.create_driver')
443
444     def start_helper(self):
445         """If a port needs to reconfigure graphics settings or do other
446         things to ensure a known test configuration, it should override this
447         method."""
448         pass
449
450     def start_http_server(self):
451         """Start a web server if it is available. Do nothing if
452         it isn't. This routine is allowed to (and may) fail if a server
453         is already running."""
454         if self._options.use_apache:
455             self._http_server = apache_http_server.LayoutTestApacheHttpd(self,
456                 self._options.results_directory)
457         else:
458             self._http_server = http_server.Lighttpd(self,
459                 self._options.results_directory)
460         self._http_server.start()
461
462     def start_websocket_server(self):
463         """Start a websocket server if it is available. Do nothing if
464         it isn't. This routine is allowed to (and may) fail if a server
465         is already running."""
466         self._websocket_server = websocket_server.PyWebSocket(self,
467             self._options.results_directory)
468         self._websocket_server.start()
469
470     def stop_helper(self):
471         """Shut down the test helper if it is running. Do nothing if
472         it isn't, or it isn't available. If a port overrides start_helper()
473         it must override this routine as well."""
474         pass
475
476     def stop_http_server(self):
477         """Shut down the http server if it is running. Do nothing if
478         it isn't, or it isn't available."""
479         if self._http_server:
480             self._http_server.stop()
481
482     def stop_websocket_server(self):
483         """Shut down the websocket server if it is running. Do nothing if
484         it isn't, or it isn't available."""
485         if self._websocket_server:
486             self._websocket_server.stop()
487
488     def test_expectations(self):
489         """Returns the test expectations for this port.
490
491         Basically this string should contain the equivalent of a
492         test_expectations file. See test_expectations.py for more details."""
493         raise NotImplementedError('Port.test_expectations')
494
495     def test_expectations_overrides(self):
496         """Returns an optional set of overrides for the test_expectations.
497
498         This is used by ports that have code in two repositories, and where
499         it is possible that you might need "downstream" expectations that
500         temporarily override the "upstream" expectations until the port can
501         sync up the two repos."""
502         return None
503
504     def test_base_platform_names(self):
505         """Return a list of the 'base' platforms on your port. The base
506         platforms represent different architectures, operating systems,
507         or implementations (as opposed to different versions of a single
508         platform). For example, 'mac' and 'win' might be different base
509         platforms, wherease 'mac-tiger' and 'mac-leopard' might be
510         different platforms. This routine is used by the rebaselining tool
511         and the dashboards, and the strings correspond to the identifiers
512         in your test expectations (*not* necessarily the platform names
513         themselves)."""
514         raise NotImplementedError('Port.base_test_platforms')
515
516     def test_platform_name(self):
517         """Returns the string that corresponds to the given platform name
518         in the test expectations. This may be the same as name(), or it
519         may be different. For example, chromium returns 'mac' for
520         'chromium-mac'."""
521         raise NotImplementedError('Port.test_platform_name')
522
523     def test_platforms(self):
524         """Returns the list of test platform identifiers as used in the
525         test_expectations and on dashboards, the rebaselining tool, etc.
526
527         Note that this is not necessarily the same as the list of ports,
528         which must be globally unique (e.g., both 'chromium-mac' and 'mac'
529         might return 'mac' as a test_platform name'."""
530         raise NotImplementedError('Port.platforms')
531
532     def test_platform_name_to_name(self, test_platform_name):
533         """Returns the Port platform name that corresponds to the name as
534         referenced in the expectations file. E.g., "mac" returns
535         "chromium-mac" on the Chromium ports."""
536         raise NotImplementedError('Port.test_platform_name_to_name')
537
538     def version(self):
539         """Returns a string indicating the version of a given platform, e.g.
540         '-leopard' or '-xp'.
541
542         This is used to help identify the exact port when parsing test
543         expectations, determining search paths, and logging information."""
544         raise NotImplementedError('Port.version')
545
546     def test_repository_paths(self):
547         """Returns a list of (repository_name, repository_path) tuples
548         of its depending code base.  By default it returns a list that only
549         contains a ('webkit', <webkitRepossitoryPath>) tuple.
550         """
551         return [('webkit', self.layout_tests_dir())]
552
553
554     _WDIFF_DEL = '##WDIFF_DEL##'
555     _WDIFF_ADD = '##WDIFF_ADD##'
556     _WDIFF_END = '##WDIFF_END##'
557
558     def _format_wdiff_output_as_html(self, wdiff):
559         wdiff = cgi.escape(wdiff)
560         wdiff = wdiff.replace(self._WDIFF_DEL, "<span class=del>")
561         wdiff = wdiff.replace(self._WDIFF_ADD, "<span class=add>")
562         wdiff = wdiff.replace(self._WDIFF_END, "</span>")
563         html = "<head><style>.del { background: #faa; } "
564         html += ".add { background: #afa; }</style></head>"
565         html += "<pre>%s</pre>" % wdiff
566         return html
567
568     def _wdiff_command(self, actual_filename, expected_filename):
569         executable = self._path_to_wdiff()
570         return [executable,
571                 "--start-delete=%s" % self._WDIFF_DEL,
572                 "--end-delete=%s" % self._WDIFF_END,
573                 "--start-insert=%s" % self._WDIFF_ADD,
574                 "--end-insert=%s" % self._WDIFF_END,
575                 actual_filename,
576                 expected_filename]
577
578     @staticmethod
579     def _handle_wdiff_error(script_error):
580         # Exit 1 means the files differed, any other exit code is an error.
581         if script_error.exit_code != 1:
582             raise script_error
583
584     def _run_wdiff(self, actual_filename, expected_filename):
585         """Runs wdiff and may throw exceptions.
586         This is mostly a hook for unit testing."""
587         # Diffs are treated as binary as they may include multiple files
588         # with conflicting encodings.  Thus we do not decode the output.
589         command = self._wdiff_command(actual_filename, expected_filename)
590         wdiff = self._executive.run_command(command, decode_output=False,
591             error_handler=self._handle_wdiff_error)
592         return self._format_wdiff_output_as_html(wdiff)
593
594     def wdiff_text(self, actual_filename, expected_filename):
595         """Returns a string of HTML indicating the word-level diff of the
596         contents of the two filenames. Returns an empty string if word-level
597         diffing isn't available."""
598         global _wdiff_available  # See explaination at top of file.
599         if not _wdiff_available:
600             return ""
601         try:
602             # It's possible to raise a ScriptError we pass wdiff invalid paths.
603             return self._run_wdiff(actual_filename, expected_filename)
604         except OSError, e:
605             if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
606                 # Silently ignore cases where wdiff is missing.
607                 _wdiff_available = False
608                 return ""
609             raise
610         assert(False)  # Should never be reached.
611
612     _pretty_patch_error_html = "Failed to run PrettyPatch, see error console."
613
614     def pretty_patch_text(self, diff_path):
615         # FIXME: Much of this function could move to prettypatch.rb
616         global _pretty_patch_available
617         if not _pretty_patch_available:
618             return self._pretty_patch_error_html
619         pretty_patch_path = self.path_from_webkit_base("BugsSite", "PrettyPatch")
620         prettify_path = os.path.join(pretty_patch_path, "prettify.rb")
621         command = ["ruby", "-I", pretty_patch_path, prettify_path, diff_path]
622         try:
623             # Diffs are treated as binary (we pass decode_output=False) as they
624             # may contain multiple files of conflicting encodings.
625             return self._executive.run_command(command, decode_output=False)
626         except OSError, e:
627             # If the system is missing ruby log the error and stop trying.
628             _pretty_patch_available = False
629             _log.error("Failed to run PrettyPatch (%s): %s" % (command, e))
630             return self._pretty_patch_error_html
631         except ScriptError, e:
632             # If ruby failed to run for some reason, log the command output and stop trying.
633             _pretty_patch_available = False
634             _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, e.message_with_output()))
635             return self._pretty_patch_error_html
636
637     def _webkit_build_directory(self, args):
638         args = [self.script_path("webkit-build-directory")] + args
639         return self._executive.run_command(args).rstrip()
640
641     def _configuration_file_path(self):
642         build_root = self._webkit_build_directory(["--top-level"])
643         return os.path.join(build_root, "Configuration")
644
645     # Easy override for unit tests
646     def _open_configuration_file(self):
647         configuration_path = self._configuration_file_path()
648         return codecs.open(configuration_path, "r", "utf-8")
649
650     def _read_configuration(self):
651         try:
652             with self._open_configuration_file() as file:
653                 return file.readline().rstrip()
654         except IOError, e:
655             return None
656
657     # FIXME: This list may be incomplete as Apple has some sekret configs.
658     _RECOGNIZED_CONFIGURATIONS = ("Debug", "Release")
659
660     def default_configuration(self):
661         # FIXME: Unify this with webkitdir.pm configuration reading code.
662         configuration = self._read_configuration()
663         if not configuration:
664             configuration = "Release"
665         if configuration not in self._RECOGNIZED_CONFIGURATIONS:
666             _log.warn("Configuration \"%s\" found in %s is not a recognized value.\n" % (configuration, self._configuration_file_path()))
667             _log.warn("Scripts may fail.  See 'set-webkit-configuration --help'.")
668         return configuration
669
670     #
671     # PROTECTED ROUTINES
672     #
673     # The routines below should only be called by routines in this class
674     # or any of its subclasses.
675     #
676
677     def _path_to_apache(self):
678         """Returns the full path to the apache binary.
679
680         This is needed only by ports that use the apache_http_server module."""
681         raise NotImplementedError('Port.path_to_apache')
682
683     def _path_to_apache_config_file(self):
684         """Returns the full path to the apache binary.
685
686         This is needed only by ports that use the apache_http_server module."""
687         raise NotImplementedError('Port.path_to_apache_config_file')
688
689     def _path_to_driver(self, configuration=None):
690         """Returns the full path to the test driver (DumpRenderTree)."""
691         raise NotImplementedError('Port.path_to_driver')
692
693     def _path_to_helper(self):
694         """Returns the full path to the layout_test_helper binary, which
695         is used to help configure the system for the test run, or None
696         if no helper is needed.
697
698         This is likely only used by start/stop_helper()."""
699         raise NotImplementedError('Port._path_to_helper')
700
701     def _path_to_image_diff(self):
702         """Returns the full path to the image_diff binary, or None if it
703         is not available.
704
705         This is likely used only by diff_image()"""
706         raise NotImplementedError('Port.path_to_image_diff')
707
708     def _path_to_lighttpd(self):
709         """Returns the path to the LigHTTPd binary.
710
711         This is needed only by ports that use the http_server.py module."""
712         raise NotImplementedError('Port._path_to_lighttpd')
713
714     def _path_to_lighttpd_modules(self):
715         """Returns the path to the LigHTTPd modules directory.
716
717         This is needed only by ports that use the http_server.py module."""
718         raise NotImplementedError('Port._path_to_lighttpd_modules')
719
720     def _path_to_lighttpd_php(self):
721         """Returns the path to the LigHTTPd PHP executable.
722
723         This is needed only by ports that use the http_server.py module."""
724         raise NotImplementedError('Port._path_to_lighttpd_php')
725
726     def _path_to_wdiff(self):
727         """Returns the full path to the wdiff binary, or None if it is
728         not available.
729
730         This is likely used only by wdiff_text()"""
731         raise NotImplementedError('Port._path_to_wdiff')
732
733     def _shut_down_http_server(self, pid):
734         """Forcefully and synchronously kills the web server.
735
736         This routine should only be called from http_server.py or its
737         subclasses."""
738         raise NotImplementedError('Port._shut_down_http_server')
739
740     def _webkit_baseline_path(self, platform):
741         """Return the  full path to the top of the baseline tree for a
742         given platform."""
743         return os.path.join(self.layout_tests_dir(), 'platform',
744                             platform)
745
746
747 class Driver:
748     """Abstract interface for the DumpRenderTree interface."""
749
750     def __init__(self, port, png_path, options):
751         """Initialize a Driver to subsequently run tests.
752
753         Typically this routine will spawn DumpRenderTree in a config
754         ready for subsequent input.
755
756         port - reference back to the port object.
757         png_path - an absolute path for the driver to write any image
758             data for a test (as a PNG). If no path is provided, that
759             indicates that pixel test results will not be checked.
760         options - any port-specific driver options."""
761         raise NotImplementedError('Driver.__init__')
762
763     def run_test(self, uri, timeout, checksum):
764         """Run a single test and return the results.
765
766         Note that it is okay if a test times out or crashes and leaves
767         the driver in an indeterminate state. The upper layers of the program
768         are responsible for cleaning up and ensuring things are okay.
769
770         uri - a full URI for the given test
771         timeout - number of milliseconds to wait before aborting this test.
772         checksum - if present, the expected checksum for the image for this
773             test
774
775         Returns a tuple of the following:
776             crash - a boolean indicating whether the driver crashed on the test
777             timeout - a boolean indicating whehter the test timed out
778             checksum - a string containing the checksum of the image, if
779                 present
780             output - any text output
781             error - any unexpected or additional (or error) text output
782
783         Note that the image itself should be written to the path that was
784         specified in the __init__() call."""
785         raise NotImplementedError('Driver.run_test')
786
787     # FIXME: This is static so we can test it w/o creating a Base instance.
788     @classmethod
789     def _command_wrapper(cls, wrapper_option):
790         # Hook for injecting valgrind or other runtime instrumentation,
791         # used by e.g. tools/valgrind/valgrind_tests.py.
792         wrapper = []
793         browser_wrapper = os.environ.get("BROWSER_WRAPPER", None)
794         if browser_wrapper:
795             # FIXME: There seems to be no reason to use BROWSER_WRAPPER over --wrapper.
796             # Remove this code any time after the date listed below.
797             _log.error("BROWSER_WRAPPER is deprecated, please use --wrapper instead.")
798             _log.error("BROWSER_WRAPPER will be removed any time after June 1st 2010 and your scripts will break.")
799             wrapper += [browser_wrapper]
800
801         if wrapper_option:
802             wrapper += shlex.split(wrapper_option)
803         return wrapper
804
805     def poll(self):
806         """Returns None if the Driver is still running. Returns the returncode
807         if it has exited."""
808         raise NotImplementedError('Driver.poll')
809
810     def returncode(self):
811         """Returns the system-specific returncode if the Driver has stopped or
812         exited."""
813         raise NotImplementedError('Driver.returncode')
814
815     def stop(self):
816         raise NotImplementedError('Driver.stop')