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