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