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