DRT/WTR python interface handles about:blank incorrectly
[WebKit-https.git] / Tools / Scripts / webkitpy / layout_tests / port / webkit.py
1 #!/usr/bin/env python
2 # Copyright (C) 2010 Google Inc. All rights reserved.
3 # Copyright (C) 2010 Gabor Rapcsanyi <rgabor@inf.u-szeged.hu>, University of Szeged
4 # Copyright (C) 2011, 2012 Apple Inc. All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions are
8 # met:
9 #
10 #     * Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 #     * Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following disclaimer
14 # in the documentation and/or other materials provided with the
15 # distribution.
16 #     * Neither the Google name nor the names of its
17 # contributors may be used to endorse or promote products derived from
18 # this software without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32 """WebKit implementations of the Port interface."""
33
34 import base64
35 import itertools
36 import logging
37 import operator
38 import re
39 import sys
40 import time
41
42 from webkitpy.common.memoized import memoized
43 from webkitpy.common.net.buildbot import BuildBot
44 from webkitpy.common.system.environment import Environment
45 from webkitpy.common.system.executive import Executive, ScriptError
46 from webkitpy.common.system.path import cygpath
47 from webkitpy.layout_tests.port import builders, server_process, Port, Driver, DriverOutput
48
49
50 _log = logging.getLogger(__name__)
51
52
53 class WebKitPort(Port):
54     def __init__(self, host, port_name=None, **kwargs):
55         Port.__init__(self, host, port_name=port_name, **kwargs)
56
57         # FIXME: Disable pixel tests until they are run by default on build.webkit.org.
58         self.set_option_default("pixel_tests", False)
59         # WebKit ports expect a 35s timeout, or 350s timeout when running with -g/--guard-malloc.
60         # FIXME: --guard-malloc is only supported on Mac, so this logic should be in mac.py.
61         default_time_out_seconds = 350 if self.get_option('guard_malloc') else 35
62         self.set_option_default("time_out_ms", default_time_out_seconds * 1000)
63
64     def driver_name(self):
65         if self.get_option('webkit_test_runner'):
66             return "WebKitTestRunner"
67         return "DumpRenderTree"
68
69     # FIXME: Eventually we should standarize port naming, and make this method smart enough
70     # to use for all port configurations (including architectures, graphics types, etc).
71     def baseline_search_path(self):
72         search_paths = []
73         if self.get_option('webkit_test_runner'):
74             search_paths.append(self._wk2_port_name())
75         search_paths.append(self.name())
76         if self.name() != self.port_name:
77             search_paths.append(self.port_name)
78         return map(self._webkit_baseline_path, search_paths)
79
80     def _port_flag_for_scripts(self):
81         # This is overrriden by ports which need a flag passed to scripts to distinguish the use of that port.
82         # For example --qt on linux, since a user might have both Gtk and Qt libraries installed.
83         # FIXME: Chromium should override this once ChromiumPort is a WebKitPort.
84         return None
85
86     # This is modeled after webkitdirs.pm argumentsForConfiguration() from old-run-webkit-tests
87     def _arguments_for_configuration(self):
88         config_args = []
89         config_args.append(self._config.flag_for_configuration(self.get_option('configuration')))
90         # FIXME: We may need to add support for passing --32-bit like old-run-webkit-tests had.
91         port_flag = self._port_flag_for_scripts()
92         if port_flag:
93             config_args.append(port_flag)
94         return config_args
95
96     def _run_script(self, script_name, args=None, include_configuration_arguments=True, decode_output=True, env=None):
97         run_script_command = [self._config.script_path(script_name)]
98         if include_configuration_arguments:
99             run_script_command.extend(self._arguments_for_configuration())
100         if args:
101             run_script_command.extend(args)
102         output = self._executive.run_command(run_script_command, cwd=self._config.webkit_base_dir(), decode_output=decode_output, env=env)
103         _log.debug('Output of %s:\n%s' % (run_script_command, output))
104         return output
105
106     def _build_driver(self):
107         environment = self.host.copy_current_environment()
108         environment.disable_gcc_smartquotes()
109         env = environment.to_dictionary()
110
111         # FIXME: We build both DumpRenderTree and WebKitTestRunner for
112         # WebKitTestRunner runs because DumpRenderTree still includes
113         # the DumpRenderTreeSupport module and the TestNetscapePlugin.
114         # These two projects should be factored out into their own
115         # projects.
116         try:
117             self._run_script("build-dumprendertree", args=self._build_driver_flags(), env=env)
118             if self.get_option('webkit_test_runner'):
119                 self._run_script("build-webkittestrunner", args=self._build_driver_flags(), env=env)
120         except ScriptError, e:
121             _log.error(e.message_with_output(output_limit=None))
122             return False
123         return True
124
125     def _build_driver_flags(self):
126         return []
127
128     def _check_driver(self):
129         driver_path = self._path_to_driver()
130         if not self._filesystem.exists(driver_path):
131             _log.error("%s was not found at %s" % (self.driver_name(), driver_path))
132             return False
133         return True
134
135     def check_build(self, needs_http):
136         # If we're using a pre-built copy of WebKit (--root), we assume it also includes a build of DRT.
137         if not self.get_option('root') and self.get_option('build') and not self._build_driver():
138             return False
139         if not self._check_driver():
140             return False
141         if self.get_option('pixel_tests'):
142             if not self.check_image_diff():
143                 return False
144         if not self._check_port_build():
145             return False
146         return True
147
148     def _check_port_build(self):
149         # Ports can override this method to do additional checks.
150         return True
151
152     def check_image_diff(self, override_step=None, logging=True):
153         image_diff_path = self._path_to_image_diff()
154         if not self._filesystem.exists(image_diff_path):
155             _log.error("ImageDiff was not found at %s" % image_diff_path)
156             return False
157         return True
158
159     def diff_image(self, expected_contents, actual_contents, tolerance=None):
160         # Handle the case where the test didn't actually generate an image.
161         # FIXME: need unit tests for this.
162         if not actual_contents and not expected_contents:
163             return (None, 0)
164         if not actual_contents or not expected_contents:
165             # FIXME: It's not clear what we should return in this case.
166             # Maybe we should throw an exception?
167             return (True, 0)
168
169         process = self._start_image_diff_process(expected_contents, actual_contents, tolerance=tolerance)
170         return self._read_image_diff(process)
171
172     def _image_diff_command(self, tolerance=None):
173         # FIXME: There needs to be a more sane way of handling default
174         # values for options so that you can distinguish between a default
175         # value of None and a default value that wasn't set.
176         if tolerance is None:
177             if self.get_option('tolerance') is not None:
178                 tolerance = self.get_option('tolerance')
179             else:
180                 tolerance = 0.1
181
182         command = [self._path_to_image_diff(), '--tolerance', str(tolerance)]
183         return command
184
185     def _start_image_diff_process(self, expected_contents, actual_contents, tolerance=None):
186         command = self._image_diff_command(tolerance)
187         environment = self.setup_environ_for_server('ImageDiff')
188         process = server_process.ServerProcess(self, 'ImageDiff', command, environment)
189
190         process.write('Content-Length: %d\n%sContent-Length: %d\n%s' % (
191             len(actual_contents), actual_contents,
192             len(expected_contents), expected_contents))
193         return process
194
195     def _read_image_diff(self, sp):
196         deadline = time.time() + 2.0
197         output = None
198         output_image = ""
199
200         while True:
201             output = sp.read_stdout_line(deadline)
202             if sp.timed_out or sp.has_crashed() or not output:
203                 break
204
205             if output.startswith('diff'):  # This is the last line ImageDiff prints.
206                 break
207
208             if output.startswith('Content-Length'):
209                 m = re.match('Content-Length: (\d+)', output)
210                 content_length = int(m.group(1))
211                 output_image = sp.read_stdout(deadline, content_length)
212                 output = sp.read_stdout_line(deadline)
213                 break
214
215         stderr = sp.pop_all_buffered_stderr()
216         if stderr:
217             _log.warn("ImageDiff produced stderr output:\n" + stderr)
218         if sp.timed_out:
219             _log.error("ImageDiff timed out")
220         if sp.has_crashed():
221             _log.error("ImageDiff crashed")
222         # FIXME: There is no need to shut down the ImageDiff server after every diff.
223         sp.stop()
224
225         diff_percent = 0
226         if output and output.startswith('diff'):
227             m = re.match('diff: (.+)% (passed|failed)', output)
228             if m.group(2) == 'passed':
229                 return [None, 0]
230             diff_percent = float(m.group(1))
231
232         return (output_image, diff_percent)
233
234     def setup_environ_for_server(self, server_name=None):
235         clean_env = super(WebKitPort, self).setup_environ_for_server(server_name)
236         self._copy_value_from_environ_if_set(clean_env, 'WEBKIT_TESTFONTS')
237         return clean_env
238
239     def default_results_directory(self):
240         # Results are store relative to the built products to make it easy
241         # to have multiple copies of webkit checked out and built.
242         return self._build_path('layout-test-results')
243
244     def _driver_class(self):
245         return WebKitDriver
246
247     def _tests_for_other_platforms(self):
248         # By default we will skip any directory under LayoutTests/platform
249         # that isn't in our baseline search path (this mirrors what
250         # old-run-webkit-tests does in findTestsToRun()).
251         # Note this returns LayoutTests/platform/*, not platform/*/*.
252         entries = self._filesystem.glob(self._webkit_baseline_path('*'))
253         dirs_to_skip = []
254         for entry in entries:
255             if self._filesystem.isdir(entry) and entry not in self.baseline_search_path():
256                 basename = self._filesystem.basename(entry)
257                 dirs_to_skip.append('platform/%s' % basename)
258         return dirs_to_skip
259
260     def _runtime_feature_list(self):
261         """If a port makes certain features available only through runtime flags, it can override this routine to indicate which ones are available."""
262         return None
263
264     def nm_command(self):
265         return 'nm'
266
267     def _webcore_symbols_string(self):
268         webcore_library_path = self._path_to_webcore_library()
269         if not webcore_library_path:
270             return None
271         try:
272             return self._executive.run_command([self.nm_command(), webcore_library_path], error_handler=Executive.ignore_error)
273         except OSError, e:
274             _log.warn("Failed to run nm: %s.  Can't determine WebCore supported features." % e)
275         return None
276
277     # Ports which use run-time feature detection should define this method and return
278     # a dictionary mapping from Feature Names to skipped directoires.  NRWT will
279     # run DumpRenderTree --print-supported-features and parse the output.
280     # If the Feature Names are not found in the output, the corresponding directories
281     # will be skipped.
282     def _missing_feature_to_skipped_tests(self):
283         """Return the supported feature dictionary. Keys are feature names and values
284         are the lists of directories to skip if the feature name is not matched."""
285         # FIXME: This list matches WebKitWin and should be moved onto the Win port.
286         return {
287             "Accelerated Compositing": ["compositing"],
288             "3D Rendering": ["animations/3d", "transforms/3d"],
289         }
290
291     # Ports which use compile-time feature detection should define this method and return
292     # a dictionary mapping from symbol substrings to possibly disabled test directories.
293     # When the symbol substrings are not matched, the directories will be skipped.
294     # If ports don't ever enable certain features, then those directories can just be
295     # in the Skipped list instead of compile-time-checked here.
296     def _missing_symbol_to_skipped_tests(self):
297         """Return the supported feature dictionary. The keys are symbol-substrings
298         and the values are the lists of directories to skip if that symbol is missing."""
299         return {
300             "MathMLElement": ["mathml"],
301             "GraphicsLayer": ["compositing"],
302             "WebCoreHas3DRendering": ["animations/3d", "transforms/3d"],
303             "WebGLShader": ["fast/canvas/webgl", "compositing/webgl", "http/tests/canvas/webgl"],
304             "MHTMLArchive": ["mhtml"],
305             "CSSVariableValue": ["fast/css/variables"],
306         }
307
308     def _has_test_in_directories(self, directory_lists, test_list):
309         if not test_list:
310             return False
311
312         directories = itertools.chain.from_iterable(directory_lists)
313         for directory, test in itertools.product(directories, test_list):
314             if test.startswith(directory):
315                 return True
316         return False
317
318     def _skipped_tests_for_unsupported_features(self, test_list):
319         # Only check the runtime feature list of there are tests in the test_list that might get skipped.
320         # This is a performance optimization to avoid the subprocess call to DRT.
321         if self._has_test_in_directories(self._missing_feature_to_skipped_tests().values(), test_list):
322             # If the port supports runtime feature detection, disable any tests
323             # for features missing from the runtime feature list.
324             supported_feature_list = self._runtime_feature_list()
325             # If _runtime_feature_list returns a non-None value, then prefer
326             # runtime feature detection over static feature detection.
327             if supported_feature_list is not None:
328                 return reduce(operator.add, [directories for feature, directories in self._missing_feature_to_skipped_tests().items() if feature not in supported_feature_list])
329
330         # Only check the symbols of there are tests in the test_list that might get skipped.
331         # This is a performance optimization to avoid the calling nm.
332         if self._has_test_in_directories(self._missing_symbol_to_skipped_tests().values(), test_list):
333             # Runtime feature detection not supported, fallback to static dectection:
334             # Disable any tests for symbols missing from the webcore symbol string.
335             webcore_symbols_string = self._webcore_symbols_string()
336             if webcore_symbols_string is not None:
337                 return reduce(operator.add, [directories for symbol_substring, directories in self._missing_symbol_to_skipped_tests().items() if symbol_substring not in webcore_symbols_string], [])
338
339         # Failed to get any runtime or symbol information, don't skip any tests.
340         return []
341
342     def _wk2_port_name(self):
343         # By current convention, the WebKit2 name is always mac-wk2, win-wk2, not mac-leopard-wk2, etc,
344         # except for Qt because WebKit2 is only supported by Qt 5.0 (therefore: qt-5.0-wk2).
345         return "%s-wk2" % self.port_name
346
347     def _skipped_file_search_paths(self):
348         # Unlike baseline_search_path, we only want to search [WK2-PORT, PORT-VERSION, PORT] and any directories
349         # included via --additional-platform-directory, not the full casade.
350         # Note order doesn't matter since the Skipped file contents are all combined.
351         search_paths = set([self.port_name])
352         if 'future' not in self.name():
353             search_paths.add(self.name())
354         if self.get_option('webkit_test_runner'):
355             # Because nearly all of the skipped tests for WebKit 2 are due to cross-platform
356             # issues, all wk2 ports share a skipped list under platform/wk2.
357             search_paths.update([self._wk2_port_name(), "wk2"])
358         search_paths.update(self.get_option("additional_platform_directory", []))
359
360         return search_paths
361
362     def skipped_layout_tests(self, test_list):
363         tests_to_skip = set(self._expectations_from_skipped_files(self._skipped_file_search_paths()))
364         tests_to_skip.update(self._tests_for_other_platforms())
365         tests_to_skip.update(self._skipped_tests_for_unsupported_features(test_list))
366         return tests_to_skip
367
368     def _build_path(self, *comps):
369         # --root is used for running with a pre-built root (like from a nightly zip).
370         build_directory = self.get_option('root') or self.get_option('build_directory')
371         if not build_directory:
372             build_directory = self._config.build_directory(self.get_option('configuration'))
373             # Set --build-directory here Since this modifies the options object used by the worker subprocesses,
374             # it avoids the slow call out to build_directory in each subprocess.
375             self.set_option_default('build_directory', build_directory)
376         return self._filesystem.join(self._filesystem.abspath(build_directory), *comps)
377
378     def _path_to_driver(self):
379         return self._build_path(self.driver_name())
380
381     def _path_to_webcore_library(self):
382         return None
383
384     def _path_to_helper(self):
385         return None
386
387     def _path_to_image_diff(self):
388         return self._build_path('ImageDiff')
389
390     def _path_to_wdiff(self):
391         # FIXME: This does not exist on a default Mac OS X Leopard install.
392         return 'wdiff'
393
394     # FIXME: This does not belong on the port object.
395     @memoized
396     def _path_to_apache(self):
397         # The Apache binary path can vary depending on OS and distribution
398         # See http://wiki.apache.org/httpd/DistrosDefaultLayout
399         for path in ["/usr/sbin/httpd", "/usr/sbin/apache2"]:
400             if self._filesystem.exists(path):
401                 return path
402         _log.error("Could not find apache. Not installed or unknown path.")
403         return None
404
405     # FIXME: This belongs on some platform abstraction instead of Port.
406     def _is_redhat_based(self):
407         return self._filesystem.exists('/etc/redhat-release')
408
409     def _is_debian_based(self):
410         return self._filesystem.exists('/etc/debian_version')
411
412     # We pass sys_platform into this method to make it easy to unit test.
413     def _apache_config_file_name_for_platform(self, sys_platform):
414         if sys_platform == 'cygwin':
415             return 'cygwin-httpd.conf'  # CYGWIN is the only platform to still use Apache 1.3.
416         if sys_platform.startswith('linux'):
417             if self._is_redhat_based():
418                 return 'fedora-httpd.conf'  # This is an Apache 2.x config file despite the naming.
419             if self._is_debian_based():
420                 return 'apache2-debian-httpd.conf'
421         # All platforms use apache2 except for CYGWIN (and Mac OS X Tiger and prior, which we no longer support).
422         return "apache2-httpd.conf"
423
424     def _path_to_apache_config_file(self):
425         config_file_name = self._apache_config_file_name_for_platform(sys.platform)
426         return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', config_file_name)
427
428
429 class WebKitDriver(Driver):
430     """WebKit implementation of the DumpRenderTree/WebKitTestRunner interface."""
431
432     def __init__(self, port, worker_number, pixel_tests, no_timeout=False):
433         Driver.__init__(self, port, worker_number, pixel_tests, no_timeout)
434         self._driver_tempdir = None
435         # WebKitTestRunner can report back subprocess crashes by printing
436         # "#CRASHED - PROCESSNAME".  Since those can happen at any time
437         # and ServerProcess won't be aware of them (since the actual tool
438         # didn't crash, just a subprocess) we record the crashed subprocess name here.
439         self._crashed_process_name = None
440         self._crashed_pid = None
441
442         # WebKitTestRunner can report back subprocesses that became unresponsive
443         # This could mean they crashed.
444         self._subprocess_was_unresponsive = False
445
446         # stderr reading is scoped on a per-test (not per-block) basis, so we store the accumulated
447         # stderr output, as well as if we've seen #EOF on this driver instance.
448         # FIXME: We should probably remove _read_first_block and _read_optional_image_block and
449         # instead scope these locally in run_test.
450         self.error_from_test = str()
451         self.err_seen_eof = False
452         self._server_process = None
453
454     def __del__(self):
455         self.stop()
456
457     def cmd_line(self, pixel_tests, per_test_args):
458         cmd = self._command_wrapper(self._port.get_option('wrapper'))
459         cmd.append(self._port._path_to_driver())
460         if self._port.get_option('gc_between_tests'):
461             cmd.append('--gc-between-tests')
462         if self._port.get_option('complex_text'):
463             cmd.append('--complex-text')
464         if self._port.get_option('threaded'):
465             cmd.append('--threaded')
466         if self._no_timeout:
467             cmd.append('--no-timeout')
468         # FIXME: We need to pass --timeout=SECONDS to WebKitTestRunner for WebKit2.
469
470         cmd.extend(self._port.get_option('additional_drt_flag', []))
471
472         if pixel_tests:
473             cmd.append('--pixel-tests')
474         cmd.extend(per_test_args)
475
476         cmd.append('-')
477         return cmd
478
479     def _start(self, pixel_tests, per_test_args):
480         self.stop()
481         self._driver_tempdir = self._port._filesystem.mkdtemp(prefix='%s-' % self._port.driver_name())
482         server_name = self._port.driver_name()
483         environment = self._port.setup_environ_for_server(server_name)
484         environment['DYLD_LIBRARY_PATH'] = self._port._build_path()
485         environment['DYLD_FRAMEWORK_PATH'] = self._port._build_path()
486         # FIXME: We're assuming that WebKitTestRunner checks this DumpRenderTree-named environment variable.
487         environment['DUMPRENDERTREE_TEMP'] = str(self._driver_tempdir)
488         environment['LOCAL_RESOURCE_ROOT'] = self._port.layout_tests_dir()
489         self._crashed_process_name = None
490         self._crashed_pid = None
491         self._server_process = server_process.ServerProcess(self._port, server_name, self.cmd_line(pixel_tests, per_test_args), environment)
492
493     def has_crashed(self):
494         if self._server_process is None:
495             return False
496         if self._crashed_process_name:
497             return True
498         if self._server_process.has_crashed():
499             self._crashed_process_name = self._server_process.name()
500             self._crashed_pid = self._server_process.pid()
501             return True
502         return False
503
504     def _check_for_driver_crash(self, error_line):
505         if error_line == "#CRASHED\n":
506             # This is used on Windows to report that the process has crashed
507             # See http://trac.webkit.org/changeset/65537.
508             self._crashed_process_name = self._server_process.name()
509             self._crashed_pid = self._server_process.pid()
510         elif (error_line.startswith("#CRASHED - WebProcess")
511             or error_line.startswith("#PROCESS UNRESPONSIVE - WebProcess")):
512             # WebKitTestRunner uses this to report that the WebProcess subprocess crashed.
513             pid = None
514             m = re.search('pid (\d+)', error_line)
515             if m:
516                 pid = int(m.group(1))
517             self._crashed_process_name = 'WebProcess'
518             self._crashed_pid = pid
519             # FIXME: delete this after we're sure this code is working :)
520             _log.debug('WebProcess crash, pid = %s, error_line = %s' % (str(pid), error_line))
521             if error_line.startswith("#PROCESS UNRESPONSIVE - WebProcess"):
522                 self._subprocess_was_unresponsive = True
523             return True
524         return self.has_crashed()
525
526     def _command_from_driver_input(self, driver_input):
527         # FIXME: performance tests pass in full URLs instead of test names.
528         if driver_input.test_name.startswith('http://') or driver_input.test_name.startswith('https://')  or driver_input.test_name == ('about:blank'):
529             command = driver_input.test_name
530         elif self.is_http_test(driver_input.test_name):
531             command = self.test_to_uri(driver_input.test_name)
532         else:
533             command = self._port.abspath_for_test(driver_input.test_name)
534             if sys.platform == 'cygwin':
535                 command = cygpath(command)
536
537         if driver_input.image_hash:
538             # FIXME: Why the leading quote?
539             command += "'" + driver_input.image_hash
540         return command + "\n"
541
542     def _read_first_block(self, deadline):
543         # returns (text_content, audio_content)
544         block = self._read_block(deadline)
545         if block.content_type == 'audio/wav':
546             return (None, block.decoded_content)
547         return (block.decoded_content, None)
548
549     def _read_optional_image_block(self, deadline):
550         # returns (image, actual_image_hash)
551         block = self._read_block(deadline, wait_for_stderr_eof=True)
552         if block.content and block.content_type == 'image/png':
553             return (block.decoded_content, block.content_hash)
554         return (None, block.content_hash)
555
556     def run_test(self, driver_input):
557         start_time = time.time()
558         self.start(driver_input.should_run_pixel_test, driver_input.args)
559         self.error_from_test = str()
560         self.err_seen_eof = False
561
562         command = self._command_from_driver_input(driver_input)
563         deadline = start_time + int(driver_input.timeout) / 1000.0
564
565         self._server_process.write(command)
566         text, audio = self._read_first_block(deadline)  # First block is either text or audio
567         image, actual_image_hash = self._read_optional_image_block(deadline)  # The second (optional) block is image data.
568
569         # We may not have read all of the output if an error (crash) occured.
570         # Since some platforms output the stacktrace over error, we should
571         # dump any buffered error into self.error_from_test.
572         # FIXME: We may need to also read stderr until the process dies?
573         self.error_from_test += self._server_process.pop_all_buffered_stderr()
574
575         crash_log = None
576         if self.has_crashed():
577             crash_log = self._port._get_crash_log(self._crashed_process_name, self._crashed_pid, text, self.error_from_test,
578                                                   newer_than=start_time)
579
580             # If we don't find a crash log use a placeholder error message instead.
581             if not crash_log:
582                 crash_log = 'no crash log found for %s:%d.' % (self._crashed_process_name, self._crashed_pid)
583                 # If we were unresponsive append a message informing there may not have been a crash.
584                 if self._subprocess_was_unresponsive:
585                     crash_log += '  Process failed to become responsive before timing out.'
586
587         timeout = self._server_process.timed_out
588         if timeout:
589             # DRT doesn't have a built in timer to abort the test, so we might as well
590             # kill the process directly and not wait for it to shut down cleanly (since it may not).
591             self._server_process.kill()
592
593         return DriverOutput(text, image, actual_image_hash, audio,
594             crash=self.has_crashed(), test_time=time.time() - start_time,
595             timeout=timeout, error=self.error_from_test,
596             crashed_process_name=self._crashed_process_name,
597             crashed_pid=self._crashed_pid, crash_log=crash_log)
598
599     def _read_header(self, block, line, header_text, header_attr, header_filter=None):
600         if line.startswith(header_text) and getattr(block, header_attr) is None:
601             value = line.split()[1]
602             if header_filter:
603                 value = header_filter(value)
604             setattr(block, header_attr, value)
605             return True
606         return False
607
608     def _process_stdout_line(self, block, line):
609         if (self._read_header(block, line, 'Content-Type: ', 'content_type')
610             or self._read_header(block, line, 'Content-Transfer-Encoding: ', 'encoding')
611             or self._read_header(block, line, 'Content-Length: ', '_content_length', int)
612             or self._read_header(block, line, 'ActualHash: ', 'content_hash')):
613             return
614         # Note, we're not reading ExpectedHash: here, but we could.
615         # If the line wasn't a header, we just append it to the content.
616         block.content += line
617
618     def _strip_eof(self, line):
619         if line and line.endswith("#EOF\n"):
620             return line[:-5], True
621         return line, False
622
623     def _read_block(self, deadline, wait_for_stderr_eof=False):
624         block = ContentBlock()
625         out_seen_eof = False
626
627         while not self.has_crashed():
628             if out_seen_eof and (self.err_seen_eof or not wait_for_stderr_eof):
629                 break
630
631             if self.err_seen_eof:
632                 out_line = self._server_process.read_stdout_line(deadline)
633                 err_line = None
634             elif out_seen_eof:
635                 out_line = None
636                 err_line = self._server_process.read_stderr_line(deadline)
637             else:
638                 out_line, err_line = self._server_process.read_either_stdout_or_stderr_line(deadline)
639
640             if self._server_process.timed_out or self.has_crashed():
641                 break
642
643             if out_line:
644                 assert not out_seen_eof
645                 out_line, out_seen_eof = self._strip_eof(out_line)
646             if err_line:
647                 assert not self.err_seen_eof
648                 err_line, self.err_seen_eof = self._strip_eof(err_line)
649
650             if out_line:
651                 if out_line[-1] != "\n":
652                     _log.error("Last character read from DRT stdout line was not a newline!  This indicates either a NRWT or DRT bug.")
653                 content_length_before_header_check = block._content_length
654                 self._process_stdout_line(block, out_line)
655                 # FIXME: Unlike HTTP, DRT dumps the content right after printing a Content-Length header.
656                 # Don't wait until we're done with headers, just read the binary blob right now.
657                 if content_length_before_header_check != block._content_length:
658                     block.content = self._server_process.read_stdout(deadline, block._content_length)
659
660             if err_line:
661                 if self._check_for_driver_crash(err_line):
662                     break
663                 self.error_from_test += err_line
664
665         block.decode_content()
666         return block
667
668     def start(self, pixel_tests, per_test_args):
669         if not self._server_process:
670             self._start(pixel_tests, per_test_args)
671
672     def stop(self):
673         if self._server_process:
674             self._server_process.stop()
675             self._server_process = None
676
677         if self._driver_tempdir:
678             self._port._filesystem.rmtree(str(self._driver_tempdir))
679             self._driver_tempdir = None
680
681
682 class ContentBlock(object):
683     def __init__(self):
684         self.content_type = None
685         self.encoding = None
686         self.content_hash = None
687         self._content_length = None
688         # Content is treated as binary data even though the text output is usually UTF-8.
689         self.content = str()  # FIXME: Should be bytearray() once we require Python 2.6.
690         self.decoded_content = None
691
692     def decode_content(self):
693         if self.encoding == 'base64':
694             self.decoded_content = base64.b64decode(self.content)
695         else:
696             self.decoded_content = self.content