Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 1 | # Copyright (C) 2011 Google Inc. All rights reserved. |
| 2 | # |
| 3 | # Redistribution and use in source and binary forms, with or without |
| 4 | # modification, are permitted provided that the following conditions are |
| 5 | # met: |
| 6 | # |
| 7 | # * Redistributions of source code must retain the above copyright |
| 8 | # notice, this list of conditions and the following disclaimer. |
| 9 | # * Redistributions in binary form must reproduce the above |
| 10 | # copyright notice, this list of conditions and the following disclaimer |
| 11 | # in the documentation and/or other materials provided with the |
| 12 | # distribution. |
| 13 | # * Neither the Google name nor the names of its |
| 14 | # contributors may be used to endorse or promote products derived from |
| 15 | # this software without specific prior written permission. |
| 16 | # |
| 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 18 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 19 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 20 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 21 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 22 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 23 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 24 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 25 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 27 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 28 | |
| 29 | import base64 |
| 30 | import copy |
| 31 | import logging |
| 32 | import re |
| 33 | import shlex |
| 34 | import sys |
| 35 | import time |
| 36 | import os |
| 37 | |
| 38 | from webkitpy.common.system import path |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 39 | from webkitpy.common.system.profiler import ProfilerFactory |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 40 | |
| 41 | |
| 42 | _log = logging.getLogger(__name__) |
| 43 | |
| 44 | |
| 45 | class DriverInput(object): |
| 46 | def __init__(self, test_name, timeout, image_hash, should_run_pixel_test, args=None): |
| 47 | self.test_name = test_name |
| 48 | self.timeout = timeout # in ms |
| 49 | self.image_hash = image_hash |
| 50 | self.should_run_pixel_test = should_run_pixel_test |
| 51 | self.args = args or [] |
| 52 | |
| 53 | |
| 54 | class DriverOutput(object): |
| 55 | """Groups information about a output from driver for easy passing |
| 56 | and post-processing of data.""" |
| 57 | |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 58 | def __init__(self, text, image, image_hash, audio, crash=False, |
| 59 | test_time=0, measurements=None, timeout=False, error='', crashed_process_name='??', |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 60 | crashed_pid=None, crash_log=None, pid=None): |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 61 | # FIXME: Args could be renamed to better clarify what they do. |
| 62 | self.text = text |
| 63 | self.image = image # May be empty-string if the test crashes. |
| 64 | self.image_hash = image_hash |
| 65 | self.image_diff = None # image_diff gets filled in after construction. |
| 66 | self.audio = audio # Binary format is port-dependent. |
| 67 | self.crash = crash |
| 68 | self.crashed_process_name = crashed_process_name |
| 69 | self.crashed_pid = crashed_pid |
| 70 | self.crash_log = crash_log |
| 71 | self.test_time = test_time |
| 72 | self.measurements = measurements |
| 73 | self.timeout = timeout |
| 74 | self.error = error # stderr output |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 75 | self.pid = pid |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 76 | |
| 77 | def has_stderr(self): |
| 78 | return bool(self.error) |
| 79 | |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 80 | |
| 81 | class Driver(object): |
Ben Murdoch | 591b958 | 2013-07-10 11:41:44 +0100 | [diff] [blame] | 82 | """object for running test(s) using content_shell or other driver.""" |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 83 | |
| 84 | def __init__(self, port, worker_number, pixel_tests, no_timeout=False): |
| 85 | """Initialize a Driver to subsequently run tests. |
| 86 | |
Ben Murdoch | 591b958 | 2013-07-10 11:41:44 +0100 | [diff] [blame] | 87 | Typically this routine will spawn content_shell in a config |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 88 | ready for subsequent input. |
| 89 | |
| 90 | port - reference back to the port object. |
| 91 | worker_number - identifier for a particular worker/driver instance |
| 92 | """ |
| 93 | self._port = port |
| 94 | self._worker_number = worker_number |
| 95 | self._no_timeout = no_timeout |
| 96 | |
| 97 | self._driver_tempdir = None |
| 98 | # WebKitTestRunner can report back subprocess crashes by printing |
| 99 | # "#CRASHED - PROCESSNAME". Since those can happen at any time |
| 100 | # and ServerProcess won't be aware of them (since the actual tool |
| 101 | # didn't crash, just a subprocess) we record the crashed subprocess name here. |
| 102 | self._crashed_process_name = None |
| 103 | self._crashed_pid = None |
| 104 | |
| 105 | # WebKitTestRunner can report back subprocesses that became unresponsive |
| 106 | # This could mean they crashed. |
| 107 | self._subprocess_was_unresponsive = False |
| 108 | |
| 109 | # stderr reading is scoped on a per-test (not per-block) basis, so we store the accumulated |
| 110 | # stderr output, as well as if we've seen #EOF on this driver instance. |
| 111 | # FIXME: We should probably remove _read_first_block and _read_optional_image_block and |
| 112 | # instead scope these locally in run_test. |
| 113 | self.error_from_test = str() |
| 114 | self.err_seen_eof = False |
| 115 | self._server_process = None |
Torne (Richard Coles) | 93ac45c | 2013-05-29 14:40:20 +0100 | [diff] [blame] | 116 | self._current_cmd_line = None |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 117 | |
| 118 | self._measurements = {} |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 119 | if self._port.get_option("profile"): |
| 120 | profiler_name = self._port.get_option("profiler") |
| 121 | self._profiler = ProfilerFactory.create_profiler(self._port.host, |
| 122 | self._port._path_to_driver(), self._port.results_directory(), profiler_name) |
| 123 | else: |
| 124 | self._profiler = None |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 125 | |
| 126 | def __del__(self): |
| 127 | self.stop() |
| 128 | |
| 129 | def run_test(self, driver_input, stop_when_done): |
| 130 | """Run a single test and return the results. |
| 131 | |
| 132 | Note that it is okay if a test times out or crashes and leaves |
| 133 | the driver in an indeterminate state. The upper layers of the program |
| 134 | are responsible for cleaning up and ensuring things are okay. |
| 135 | |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 136 | Returns a DriverOutput object. |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 137 | """ |
Torne (Richard Coles) | 93ac45c | 2013-05-29 14:40:20 +0100 | [diff] [blame] | 138 | base = self._port.lookup_virtual_test_base(driver_input.test_name) |
| 139 | if base: |
| 140 | virtual_driver_input = copy.copy(driver_input) |
| 141 | virtual_driver_input.test_name = base |
| 142 | virtual_driver_input.args = self._port.lookup_virtual_test_args(driver_input.test_name) |
| 143 | return self.run_test(virtual_driver_input, stop_when_done) |
| 144 | |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 145 | start_time = time.time() |
| 146 | self.start(driver_input.should_run_pixel_test, driver_input.args) |
| 147 | test_begin_time = time.time() |
| 148 | self.error_from_test = str() |
| 149 | self.err_seen_eof = False |
| 150 | |
| 151 | command = self._command_from_driver_input(driver_input) |
| 152 | deadline = test_begin_time + int(driver_input.timeout) / 1000.0 |
| 153 | |
| 154 | self._server_process.write(command) |
| 155 | text, audio = self._read_first_block(deadline) # First block is either text or audio |
| 156 | image, actual_image_hash = self._read_optional_image_block(deadline) # The second (optional) block is image data. |
| 157 | |
| 158 | crashed = self.has_crashed() |
| 159 | timed_out = self._server_process.timed_out |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 160 | pid = self._server_process.pid() |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 161 | |
| 162 | if stop_when_done or crashed or timed_out: |
| 163 | # We call stop() even if we crashed or timed out in order to get any remaining stdout/stderr output. |
| 164 | # In the timeout case, we kill the hung process as well. |
| 165 | out, err = self._server_process.stop(self._port.driver_stop_timeout() if stop_when_done else 0.0) |
| 166 | if out: |
| 167 | text += out |
| 168 | if err: |
| 169 | self.error_from_test += err |
| 170 | self._server_process = None |
| 171 | |
| 172 | crash_log = None |
| 173 | if crashed: |
| 174 | self.error_from_test, crash_log = self._get_crash_log(text, self.error_from_test, newer_than=start_time) |
| 175 | |
| 176 | # If we don't find a crash log use a placeholder error message instead. |
| 177 | if not crash_log: |
| 178 | pid_str = str(self._crashed_pid) if self._crashed_pid else "unknown pid" |
| 179 | crash_log = 'No crash log found for %s:%s.\n' % (self._crashed_process_name, pid_str) |
| 180 | # If we were unresponsive append a message informing there may not have been a crash. |
| 181 | if self._subprocess_was_unresponsive: |
| 182 | crash_log += 'Process failed to become responsive before timing out.\n' |
| 183 | |
| 184 | # Print stdout and stderr to the placeholder crash log; we want as much context as possible. |
| 185 | if self.error_from_test: |
| 186 | crash_log += '\nstdout:\n%s\nstderr:\n%s\n' % (text, self.error_from_test) |
| 187 | |
| 188 | return DriverOutput(text, image, actual_image_hash, audio, |
| 189 | crash=crashed, test_time=time.time() - test_begin_time, measurements=self._measurements, |
| 190 | timeout=timed_out, error=self.error_from_test, |
| 191 | crashed_process_name=self._crashed_process_name, |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 192 | crashed_pid=self._crashed_pid, crash_log=crash_log, pid=pid) |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 193 | |
| 194 | def _get_crash_log(self, stdout, stderr, newer_than): |
| 195 | return self._port._get_crash_log(self._crashed_process_name, self._crashed_pid, stdout, stderr, newer_than) |
| 196 | |
| 197 | # FIXME: Seems this could just be inlined into callers. |
| 198 | @classmethod |
| 199 | def _command_wrapper(cls, wrapper_option): |
| 200 | # Hook for injecting valgrind or other runtime instrumentation, |
| 201 | # used by e.g. tools/valgrind/valgrind_tests.py. |
| 202 | return shlex.split(wrapper_option) if wrapper_option else [] |
| 203 | |
| 204 | HTTP_DIR = "http/tests/" |
| 205 | HTTP_LOCAL_DIR = "http/tests/local/" |
| 206 | |
| 207 | def is_http_test(self, test_name): |
| 208 | return test_name.startswith(self.HTTP_DIR) and not test_name.startswith(self.HTTP_LOCAL_DIR) |
| 209 | |
| 210 | def test_to_uri(self, test_name): |
| 211 | """Convert a test name to a URI.""" |
| 212 | if not self.is_http_test(test_name): |
| 213 | return path.abspath_to_uri(self._port.host.platform, self._port.abspath_for_test(test_name)) |
| 214 | |
| 215 | relative_path = test_name[len(self.HTTP_DIR):] |
| 216 | |
| 217 | # TODO(dpranke): remove the SSL reference? |
| 218 | if relative_path.startswith("ssl/"): |
| 219 | return "https://127.0.0.1:8443/" + relative_path |
| 220 | return "http://127.0.0.1:8000/" + relative_path |
| 221 | |
| 222 | def uri_to_test(self, uri): |
| 223 | """Return the base layout test name for a given URI. |
| 224 | |
| 225 | This returns the test name for a given URI, e.g., if you passed in |
| 226 | "file:///src/LayoutTests/fast/html/keygen.html" it would return |
| 227 | "fast/html/keygen.html". |
| 228 | |
| 229 | """ |
| 230 | if uri.startswith("file:///"): |
| 231 | prefix = path.abspath_to_uri(self._port.host.platform, self._port.layout_tests_dir()) |
| 232 | if not prefix.endswith('/'): |
| 233 | prefix += '/' |
| 234 | return uri[len(prefix):] |
| 235 | if uri.startswith("http://"): |
| 236 | return uri.replace('http://127.0.0.1:8000/', self.HTTP_DIR) |
| 237 | if uri.startswith("https://"): |
| 238 | return uri.replace('https://127.0.0.1:8443/', self.HTTP_DIR) |
| 239 | raise NotImplementedError('unknown url type: %s' % uri) |
| 240 | |
| 241 | def has_crashed(self): |
| 242 | if self._server_process is None: |
| 243 | return False |
| 244 | if self._crashed_process_name: |
| 245 | return True |
| 246 | if self._server_process.has_crashed(): |
| 247 | self._crashed_process_name = self._server_process.name() |
| 248 | self._crashed_pid = self._server_process.pid() |
| 249 | return True |
| 250 | return False |
| 251 | |
| 252 | def start(self, pixel_tests, per_test_args): |
Torne (Richard Coles) | 93ac45c | 2013-05-29 14:40:20 +0100 | [diff] [blame] | 253 | new_cmd_line = self.cmd_line(pixel_tests, per_test_args) |
| 254 | if not self._server_process or new_cmd_line != self._current_cmd_line: |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 255 | self._start(pixel_tests, per_test_args) |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 256 | self._run_post_start_tasks() |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 257 | |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 258 | def _setup_environ_for_driver(self, environment): |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 259 | environment['DYLD_LIBRARY_PATH'] = self._port._build_path() |
| 260 | environment['DYLD_FRAMEWORK_PATH'] = self._port._build_path() |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 261 | environment['LOCAL_RESOURCE_ROOT'] = self._port.layout_tests_dir() |
| 262 | if 'WEBKITOUTPUTDIR' in os.environ: |
| 263 | environment['WEBKITOUTPUTDIR'] = os.environ['WEBKITOUTPUTDIR'] |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 264 | if self._profiler: |
| 265 | environment = self._profiler.adjusted_environment(environment) |
| 266 | return environment |
| 267 | |
| 268 | def _start(self, pixel_tests, per_test_args): |
| 269 | self.stop() |
| 270 | self._driver_tempdir = self._port._filesystem.mkdtemp(prefix='%s-' % self._port.driver_name()) |
| 271 | server_name = self._port.driver_name() |
| 272 | environment = self._port.setup_environ_for_server(server_name) |
| 273 | environment = self._setup_environ_for_driver(environment) |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 274 | self._crashed_process_name = None |
| 275 | self._crashed_pid = None |
Torne (Richard Coles) | 93ac45c | 2013-05-29 14:40:20 +0100 | [diff] [blame] | 276 | cmd_line = self.cmd_line(pixel_tests, per_test_args) |
| 277 | self._server_process = self._port._server_process_constructor(self._port, server_name, cmd_line, environment) |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 278 | self._server_process.start() |
Torne (Richard Coles) | 93ac45c | 2013-05-29 14:40:20 +0100 | [diff] [blame] | 279 | self._current_cmd_line = cmd_line |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 280 | |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 281 | def _run_post_start_tasks(self): |
| 282 | # Remote drivers may override this to delay post-start tasks until the server has ack'd. |
| 283 | if self._profiler: |
| 284 | self._profiler.attach_to_pid(self._pid_on_target()) |
| 285 | |
| 286 | def _pid_on_target(self): |
| 287 | # Remote drivers will override this method to return the pid on the device. |
| 288 | return self._server_process.pid() |
| 289 | |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 290 | def stop(self): |
| 291 | if self._server_process: |
| 292 | self._server_process.stop(self._port.driver_stop_timeout()) |
| 293 | self._server_process = None |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 294 | if self._profiler: |
| 295 | self._profiler.profile_after_exit() |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 296 | |
| 297 | if self._driver_tempdir: |
| 298 | self._port._filesystem.rmtree(str(self._driver_tempdir)) |
| 299 | self._driver_tempdir = None |
| 300 | |
Torne (Richard Coles) | 93ac45c | 2013-05-29 14:40:20 +0100 | [diff] [blame] | 301 | self._current_cmd_line = None |
| 302 | |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 303 | def cmd_line(self, pixel_tests, per_test_args): |
| 304 | cmd = self._command_wrapper(self._port.get_option('wrapper')) |
| 305 | cmd.append(self._port._path_to_driver()) |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 306 | if self._no_timeout: |
| 307 | cmd.append('--no-timeout') |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 308 | cmd.extend(self._port.get_option('additional_drt_flag', [])) |
| 309 | cmd.extend(self._port.additional_drt_flag()) |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 310 | cmd.extend(per_test_args) |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 311 | cmd.append('-') |
| 312 | return cmd |
| 313 | |
| 314 | def _check_for_driver_crash(self, error_line): |
| 315 | if error_line == "#CRASHED\n": |
| 316 | # This is used on Windows to report that the process has crashed |
| 317 | # See http://trac.webkit.org/changeset/65537. |
| 318 | self._crashed_process_name = self._server_process.name() |
| 319 | self._crashed_pid = self._server_process.pid() |
| 320 | elif (error_line.startswith("#CRASHED - ") |
| 321 | or error_line.startswith("#PROCESS UNRESPONSIVE - ")): |
| 322 | # WebKitTestRunner uses this to report that the WebProcess subprocess crashed. |
| 323 | match = re.match('#(?:CRASHED|PROCESS UNRESPONSIVE) - (\S+)', error_line) |
| 324 | self._crashed_process_name = match.group(1) if match else 'WebProcess' |
| 325 | match = re.search('pid (\d+)', error_line) |
| 326 | pid = int(match.group(1)) if match else None |
| 327 | self._crashed_pid = pid |
| 328 | # FIXME: delete this after we're sure this code is working :) |
| 329 | _log.debug('%s crash, pid = %s, error_line = %s' % (self._crashed_process_name, str(pid), error_line)) |
| 330 | if error_line.startswith("#PROCESS UNRESPONSIVE - "): |
| 331 | self._subprocess_was_unresponsive = True |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 332 | self._port.sample_process(self._crashed_process_name, self._crashed_pid) |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 333 | # We want to show this since it's not a regular crash and probably we don't have a crash log. |
| 334 | self.error_from_test += error_line |
| 335 | return True |
| 336 | return self.has_crashed() |
| 337 | |
| 338 | def _command_from_driver_input(self, driver_input): |
| 339 | # FIXME: performance tests pass in full URLs instead of test names. |
| 340 | if driver_input.test_name.startswith('http://') or driver_input.test_name.startswith('https://') or driver_input.test_name == ('about:blank'): |
| 341 | command = driver_input.test_name |
| 342 | elif self.is_http_test(driver_input.test_name): |
| 343 | command = self.test_to_uri(driver_input.test_name) |
| 344 | else: |
| 345 | command = self._port.abspath_for_test(driver_input.test_name) |
| 346 | if sys.platform == 'cygwin': |
| 347 | command = path.cygpath(command) |
| 348 | |
| 349 | assert not driver_input.image_hash or driver_input.should_run_pixel_test |
| 350 | |
| 351 | # ' is the separator between arguments. |
Torne (Richard Coles) | 926b001 | 2013-03-28 15:32:48 +0000 | [diff] [blame] | 352 | if self._port.supports_per_test_timeout(): |
| 353 | command += "'--timeout'%s" % driver_input.timeout |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 354 | if driver_input.should_run_pixel_test: |
| 355 | command += "'--pixel-test" |
| 356 | if driver_input.image_hash: |
| 357 | command += "'" + driver_input.image_hash |
| 358 | return command + "\n" |
| 359 | |
| 360 | def _read_first_block(self, deadline): |
| 361 | # returns (text_content, audio_content) |
| 362 | block = self._read_block(deadline) |
| 363 | if block.malloc: |
| 364 | self._measurements['Malloc'] = float(block.malloc) |
| 365 | if block.js_heap: |
| 366 | self._measurements['JSHeap'] = float(block.js_heap) |
| 367 | if block.content_type == 'audio/wav': |
| 368 | return (None, block.decoded_content) |
| 369 | return (block.decoded_content, None) |
| 370 | |
| 371 | def _read_optional_image_block(self, deadline): |
| 372 | # returns (image, actual_image_hash) |
| 373 | block = self._read_block(deadline, wait_for_stderr_eof=True) |
| 374 | if block.content and block.content_type == 'image/png': |
| 375 | return (block.decoded_content, block.content_hash) |
| 376 | return (None, block.content_hash) |
| 377 | |
| 378 | def _read_header(self, block, line, header_text, header_attr, header_filter=None): |
| 379 | if line.startswith(header_text) and getattr(block, header_attr) is None: |
| 380 | value = line.split()[1] |
| 381 | if header_filter: |
| 382 | value = header_filter(value) |
| 383 | setattr(block, header_attr, value) |
| 384 | return True |
| 385 | return False |
| 386 | |
| 387 | def _process_stdout_line(self, block, line): |
| 388 | if (self._read_header(block, line, 'Content-Type: ', 'content_type') |
| 389 | or self._read_header(block, line, 'Content-Transfer-Encoding: ', 'encoding') |
| 390 | or self._read_header(block, line, 'Content-Length: ', '_content_length', int) |
| 391 | or self._read_header(block, line, 'ActualHash: ', 'content_hash') |
| 392 | or self._read_header(block, line, 'DumpMalloc: ', 'malloc') |
| 393 | or self._read_header(block, line, 'DumpJSHeap: ', 'js_heap')): |
| 394 | return |
| 395 | # Note, we're not reading ExpectedHash: here, but we could. |
| 396 | # If the line wasn't a header, we just append it to the content. |
| 397 | block.content += line |
| 398 | |
| 399 | def _strip_eof(self, line): |
| 400 | if line and line.endswith("#EOF\n"): |
| 401 | return line[:-5], True |
| 402 | return line, False |
| 403 | |
| 404 | def _read_block(self, deadline, wait_for_stderr_eof=False): |
| 405 | block = ContentBlock() |
| 406 | out_seen_eof = False |
| 407 | |
| 408 | while not self.has_crashed(): |
| 409 | if out_seen_eof and (self.err_seen_eof or not wait_for_stderr_eof): |
| 410 | break |
| 411 | |
| 412 | if self.err_seen_eof: |
| 413 | out_line = self._server_process.read_stdout_line(deadline) |
| 414 | err_line = None |
| 415 | elif out_seen_eof: |
| 416 | out_line = None |
| 417 | err_line = self._server_process.read_stderr_line(deadline) |
| 418 | else: |
| 419 | out_line, err_line = self._server_process.read_either_stdout_or_stderr_line(deadline) |
| 420 | |
| 421 | if self._server_process.timed_out or self.has_crashed(): |
| 422 | break |
| 423 | |
| 424 | if out_line: |
| 425 | assert not out_seen_eof |
| 426 | out_line, out_seen_eof = self._strip_eof(out_line) |
| 427 | if err_line: |
| 428 | assert not self.err_seen_eof |
| 429 | err_line, self.err_seen_eof = self._strip_eof(err_line) |
| 430 | |
| 431 | if out_line: |
| 432 | if out_line[-1] != "\n": |
| 433 | _log.error("Last character read from DRT stdout line was not a newline! This indicates either a NRWT or DRT bug.") |
| 434 | content_length_before_header_check = block._content_length |
| 435 | self._process_stdout_line(block, out_line) |
| 436 | # FIXME: Unlike HTTP, DRT dumps the content right after printing a Content-Length header. |
| 437 | # Don't wait until we're done with headers, just read the binary blob right now. |
| 438 | if content_length_before_header_check != block._content_length: |
Ben Murdoch | 591b958 | 2013-07-10 11:41:44 +0100 | [diff] [blame] | 439 | if block._content_length > 0: |
| 440 | block.content = self._server_process.read_stdout(deadline, block._content_length) |
| 441 | else: |
| 442 | _log.error("Received content of type %s with Content-Length of 0! This indicates a bug in %s.", |
| 443 | block.content_type, self._server_process.name()) |
Torne (Richard Coles) | 5c87bf8 | 2012-11-14 11:46:17 +0000 | [diff] [blame] | 444 | |
| 445 | if err_line: |
| 446 | if self._check_for_driver_crash(err_line): |
| 447 | break |
| 448 | self.error_from_test += err_line |
| 449 | |
| 450 | block.decode_content() |
| 451 | return block |
| 452 | |
| 453 | |
| 454 | class ContentBlock(object): |
| 455 | def __init__(self): |
| 456 | self.content_type = None |
| 457 | self.encoding = None |
| 458 | self.content_hash = None |
| 459 | self._content_length = None |
| 460 | # Content is treated as binary data even though the text output is usually UTF-8. |
| 461 | self.content = str() # FIXME: Should be bytearray() once we require Python 2.6. |
| 462 | self.decoded_content = None |
| 463 | self.malloc = None |
| 464 | self.js_heap = None |
| 465 | |
| 466 | def decode_content(self): |
| 467 | if self.encoding == 'base64' and self.content is not None: |
| 468 | self.decoded_content = base64.b64decode(self.content) |
| 469 | else: |
| 470 | self.decoded_content = self.content |