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 39 from webkitpy.common.system.profiler import ProfilerFactory 40 41 42 _log = logging.getLogger(__name__) 43 44 45 DRIVER_START_TIMEOUT_SECS = 30 46 47 48 class DriverInput(object): 49 def __init__(self, test_name, timeout, image_hash, should_run_pixel_test, args): 50 self.test_name = test_name 51 self.timeout = timeout # in ms 52 self.image_hash = image_hash 53 self.should_run_pixel_test = should_run_pixel_test 54 self.args = args 55 56 57 class DriverOutput(object): 58 """Groups information about a output from driver for easy passing 59 and post-processing of data.""" 60 61 def __init__(self, text, image, image_hash, audio, crash=False, 62 test_time=0, measurements=None, timeout=False, error='', crashed_process_name='??', 63 crashed_pid=None, crash_log=None, leak=False, leak_log=None, pid=None): 64 # FIXME: Args could be renamed to better clarify what they do. 65 self.text = text 66 self.image = image # May be empty-string if the test crashes. 67 self.image_hash = image_hash 68 self.image_diff = None # image_diff gets filled in after construction. 69 self.audio = audio # Binary format is port-dependent. 70 self.crash = crash 71 self.crashed_process_name = crashed_process_name 72 self.crashed_pid = crashed_pid 73 self.crash_log = crash_log 74 self.leak = leak 75 self.leak_log = leak_log 76 self.test_time = test_time 77 self.measurements = measurements 78 self.timeout = timeout 79 self.error = error # stderr output 80 self.pid = pid 81 82 def has_stderr(self): 83 return bool(self.error) 84 85 86 class DeviceFailure(Exception): 87 pass 88 89 90 class Driver(object): 91 """object for running test(s) using content_shell or other driver.""" 92 93 def __init__(self, port, worker_number, pixel_tests, no_timeout=False): 94 """Initialize a Driver to subsequently run tests. 95 96 Typically this routine will spawn content_shell in a config 97 ready for subsequent input. 98 99 port - reference back to the port object. 100 worker_number - identifier for a particular worker/driver instance 101 """ 102 self._port = port 103 self._worker_number = worker_number 104 self._no_timeout = no_timeout 105 106 self._driver_tempdir = None 107 # content_shell can report back subprocess crashes by printing 108 # "#CRASHED - PROCESSNAME". Since those can happen at any time 109 # and ServerProcess won't be aware of them (since the actual tool 110 # didn't crash, just a subprocess) we record the crashed subprocess name here. 111 self._crashed_process_name = None 112 self._crashed_pid = None 113 114 # content_shell can report back subprocesses that became unresponsive 115 # This could mean they crashed. 116 self._subprocess_was_unresponsive = False 117 118 # content_shell can report back subprocess DOM-object leaks by printing 119 # "#LEAK". This leak detection is enabled only when the flag 120 # --enable-leak-detection is passed to content_shell. 121 self._leaked = False 122 123 # stderr reading is scoped on a per-test (not per-block) basis, so we store the accumulated 124 # stderr output, as well as if we've seen #EOF on this driver instance. 125 # FIXME: We should probably remove _read_first_block and _read_optional_image_block and 126 # instead scope these locally in run_test. 127 self.error_from_test = str() 128 self.err_seen_eof = False 129 self._server_process = None 130 self._current_cmd_line = None 131 132 self._measurements = {} 133 if self._port.get_option("profile"): 134 profiler_name = self._port.get_option("profiler") 135 self._profiler = ProfilerFactory.create_profiler(self._port.host, 136 self._port._path_to_driver(), self._port.results_directory(), profiler_name) 137 else: 138 self._profiler = None 139 140 def __del__(self): 141 self.stop() 142 143 def run_test(self, driver_input, stop_when_done): 144 """Run a single test and return the results. 145 146 Note that it is okay if a test times out or crashes and leaves 147 the driver in an indeterminate state. The upper layers of the program 148 are responsible for cleaning up and ensuring things are okay. 149 150 Returns a DriverOutput object. 151 """ 152 start_time = time.time() 153 self.start(driver_input.should_run_pixel_test, driver_input.args) 154 test_begin_time = time.time() 155 self.error_from_test = str() 156 self.err_seen_eof = False 157 158 command = self._command_from_driver_input(driver_input) 159 deadline = test_begin_time + int(driver_input.timeout) / 1000.0 160 161 self._server_process.write(command) 162 text, audio = self._read_first_block(deadline) # First block is either text or audio 163 image, actual_image_hash = self._read_optional_image_block(deadline) # The second (optional) block is image data. 164 165 crashed = self.has_crashed() 166 timed_out = self._server_process.timed_out 167 pid = self._server_process.pid() 168 leaked = self._leaked 169 170 if not crashed and 'AddressSanitizer' in self.error_from_test: 171 self.error_from_test = 'OUTPUT CONTAINS "AddressSanitizer", so we are treating this test as if it crashed, even though it did not.\n\n' + self.error_from_test 172 crashed = True 173 self._crashed_process_name = "unknown process name" 174 self._crashed_pid = 0 175 176 if stop_when_done or crashed or timed_out or leaked: 177 # We call stop() even if we crashed or timed out in order to get any remaining stdout/stderr output. 178 # In the timeout case, we kill the hung process as well. 179 out, err = self._server_process.stop(self._port.driver_stop_timeout() if stop_when_done else 0.0) 180 if out: 181 text += out 182 if err: 183 self.error_from_test += err 184 self._server_process = None 185 186 crash_log = None 187 if crashed: 188 self.error_from_test, crash_log = self._get_crash_log(text, self.error_from_test, newer_than=start_time) 189 190 # If we don't find a crash log use a placeholder error message instead. 191 if not crash_log: 192 pid_str = str(self._crashed_pid) if self._crashed_pid else "unknown pid" 193 crash_log = 'No crash log found for %s:%s.\n' % (self._crashed_process_name, pid_str) 194 # If we were unresponsive append a message informing there may not have been a crash. 195 if self._subprocess_was_unresponsive: 196 crash_log += 'Process failed to become responsive before timing out.\n' 197 198 # Print stdout and stderr to the placeholder crash log; we want as much context as possible. 199 if self.error_from_test: 200 crash_log += '\nstdout:\n%s\nstderr:\n%s\n' % (text, self.error_from_test) 201 202 return DriverOutput(text, image, actual_image_hash, audio, 203 crash=crashed, test_time=time.time() - test_begin_time, measurements=self._measurements, 204 timeout=timed_out, error=self.error_from_test, 205 crashed_process_name=self._crashed_process_name, 206 crashed_pid=self._crashed_pid, crash_log=crash_log, 207 leak=leaked, leak_log=self._leak_log, 208 pid=pid) 209 210 def _get_crash_log(self, stdout, stderr, newer_than): 211 return self._port._get_crash_log(self._crashed_process_name, self._crashed_pid, stdout, stderr, newer_than) 212 213 # FIXME: Seems this could just be inlined into callers. 214 @classmethod 215 def _command_wrapper(cls, wrapper_option): 216 # Hook for injecting valgrind or other runtime instrumentation, 217 # used by e.g. tools/valgrind/valgrind_tests.py. 218 return shlex.split(wrapper_option) if wrapper_option else [] 219 220 HTTP_DIR = "http/tests/" 221 HTTP_LOCAL_DIR = "http/tests/local/" 222 223 def is_http_test(self, test_name): 224 return test_name.startswith(self.HTTP_DIR) and not test_name.startswith(self.HTTP_LOCAL_DIR) 225 226 def test_to_uri(self, test_name): 227 """Convert a test name to a URI.""" 228 if not self.is_http_test(test_name): 229 return path.abspath_to_uri(self._port.host.platform, self._port.abspath_for_test(test_name)) 230 231 relative_path = test_name[len(self.HTTP_DIR):] 232 233 # TODO(dpranke): remove the SSL reference? 234 if relative_path.startswith("ssl/"): 235 return "https://127.0.0.1:8443/" + relative_path 236 return "http://127.0.0.1:8000/" + relative_path 237 238 def uri_to_test(self, uri): 239 """Return the base layout test name for a given URI. 240 241 This returns the test name for a given URI, e.g., if you passed in 242 "file:///src/LayoutTests/fast/html/keygen.html" it would return 243 "fast/html/keygen.html". 244 245 """ 246 if uri.startswith("file:///"): 247 prefix = path.abspath_to_uri(self._port.host.platform, self._port.layout_tests_dir()) 248 if not prefix.endswith('/'): 249 prefix += '/' 250 return uri[len(prefix):] 251 if uri.startswith("http://"): 252 return uri.replace('http://127.0.0.1:8000/', self.HTTP_DIR) 253 if uri.startswith("https://"): 254 return uri.replace('https://127.0.0.1:8443/', self.HTTP_DIR) 255 raise NotImplementedError('unknown url type: %s' % uri) 256 257 def has_crashed(self): 258 if self._server_process is None: 259 return False 260 if self._crashed_process_name: 261 return True 262 if self._server_process.has_crashed(): 263 self._crashed_process_name = self._server_process.name() 264 self._crashed_pid = self._server_process.pid() 265 return True 266 return False 267 268 def start(self, pixel_tests, per_test_args): 269 new_cmd_line = self.cmd_line(pixel_tests, per_test_args) 270 if not self._server_process or new_cmd_line != self._current_cmd_line: 271 self._start(pixel_tests, per_test_args) 272 self._run_post_start_tasks() 273 274 def _setup_environ_for_driver(self, environment): 275 if self._profiler: 276 environment = self._profiler.adjusted_environment(environment) 277 return environment 278 279 def _start(self, pixel_tests, per_test_args, wait_for_ready=True): 280 self.stop() 281 self._driver_tempdir = self._port._filesystem.mkdtemp(prefix='%s-' % self._port.driver_name()) 282 server_name = self._port.driver_name() 283 environment = self._port.setup_environ_for_server(server_name) 284 environment = self._setup_environ_for_driver(environment) 285 self._crashed_process_name = None 286 self._crashed_pid = None 287 self._leaked = False 288 self._leak_log = None 289 cmd_line = self.cmd_line(pixel_tests, per_test_args) 290 self._server_process = self._port._server_process_constructor(self._port, server_name, cmd_line, environment, logging=self._port.get_option("driver_logging")) 291 self._server_process.start() 292 self._current_cmd_line = cmd_line 293 294 if wait_for_ready: 295 deadline = time.time() + DRIVER_START_TIMEOUT_SECS 296 if not self._wait_for_server_process_output(self._server_process, deadline, '#READY'): 297 _log.error("content_shell took too long to startup.") 298 299 def _wait_for_server_process_output(self, server_process, deadline, text): 300 output = '' 301 line = server_process.read_stdout_line(deadline) 302 while not server_process.timed_out and not server_process.has_crashed() and not text in line.rstrip(): 303 output += line 304 line = server_process.read_stdout_line(deadline) 305 306 if server_process.timed_out or server_process.has_crashed(): 307 _log.error('Failed to start the %s process: \n%s' % (server_process.name(), output)) 308 return False 309 310 return True 311 312 def _run_post_start_tasks(self): 313 # Remote drivers may override this to delay post-start tasks until the server has ack'd. 314 if self._profiler: 315 self._profiler.attach_to_pid(self._pid_on_target()) 316 317 def _pid_on_target(self): 318 # Remote drivers will override this method to return the pid on the device. 319 return self._server_process.pid() 320 321 def stop(self): 322 if self._server_process: 323 self._server_process.stop() 324 self._server_process = None 325 if self._profiler: 326 self._profiler.profile_after_exit() 327 328 if self._driver_tempdir: 329 self._port._filesystem.rmtree(str(self._driver_tempdir)) 330 self._driver_tempdir = None 331 332 self._current_cmd_line = None 333 334 def cmd_line(self, pixel_tests, per_test_args): 335 cmd = self._command_wrapper(self._port.get_option('wrapper')) 336 cmd.append(self._port._path_to_driver()) 337 if self._no_timeout: 338 cmd.append('--no-timeout') 339 cmd.extend(self._port.get_option('additional_drt_flag', [])) 340 cmd.extend(self._port.additional_drt_flag()) 341 if self._port.get_option('enable_leak_detection'): 342 cmd.append('--enable-leak-detection') 343 cmd.extend(per_test_args) 344 cmd.append('-') 345 return cmd 346 347 def _check_for_driver_crash(self, error_line): 348 if error_line == "#CRASHED\n": 349 # This is used on Windows to report that the process has crashed 350 # See http://trac.webkit.org/changeset/65537. 351 self._crashed_process_name = self._server_process.name() 352 self._crashed_pid = self._server_process.pid() 353 elif (error_line.startswith("#CRASHED - ") 354 or error_line.startswith("#PROCESS UNRESPONSIVE - ")): 355 # WebKitTestRunner uses this to report that the WebProcess subprocess crashed. 356 match = re.match('#(?:CRASHED|PROCESS UNRESPONSIVE) - (\S+)', error_line) 357 self._crashed_process_name = match.group(1) if match else 'WebProcess' 358 match = re.search('pid (\d+)', error_line) 359 pid = int(match.group(1)) if match else None 360 self._crashed_pid = pid 361 # FIXME: delete this after we're sure this code is working :) 362 _log.debug('%s crash, pid = %s, error_line = %s' % (self._crashed_process_name, str(pid), error_line)) 363 if error_line.startswith("#PROCESS UNRESPONSIVE - "): 364 self._subprocess_was_unresponsive = True 365 self._port.sample_process(self._crashed_process_name, self._crashed_pid) 366 # We want to show this since it's not a regular crash and probably we don't have a crash log. 367 self.error_from_test += error_line 368 return True 369 return self.has_crashed() 370 371 def _check_for_leak(self, error_line): 372 if error_line.startswith("#LEAK - "): 373 self._leaked = True 374 match = re.match('#LEAK - (\S+) pid (\d+) (.+)\n', error_line) 375 self._leak_log = match.group(3) 376 return self._leaked 377 378 def _command_from_driver_input(self, driver_input): 379 # FIXME: performance tests pass in full URLs instead of test names. 380 if driver_input.test_name.startswith('http://') or driver_input.test_name.startswith('https://') or driver_input.test_name == ('about:blank'): 381 command = driver_input.test_name 382 elif self.is_http_test(driver_input.test_name): 383 command = self.test_to_uri(driver_input.test_name) 384 else: 385 command = self._port.abspath_for_test(driver_input.test_name) 386 if sys.platform == 'cygwin': 387 command = path.cygpath(command) 388 389 assert not driver_input.image_hash or driver_input.should_run_pixel_test 390 391 # ' is the separator between arguments. 392 if self._port.supports_per_test_timeout(): 393 command += "'--timeout'%s" % driver_input.timeout 394 if driver_input.should_run_pixel_test: 395 command += "'--pixel-test" 396 if driver_input.image_hash: 397 command += "'" + driver_input.image_hash 398 return command + "\n" 399 400 def _read_first_block(self, deadline): 401 # returns (text_content, audio_content) 402 block = self._read_block(deadline) 403 if block.malloc: 404 self._measurements['Malloc'] = float(block.malloc) 405 if block.js_heap: 406 self._measurements['JSHeap'] = float(block.js_heap) 407 if block.content_type == 'audio/wav': 408 return (None, block.decoded_content) 409 return (block.decoded_content, None) 410 411 def _read_optional_image_block(self, deadline): 412 # returns (image, actual_image_hash) 413 block = self._read_block(deadline, wait_for_stderr_eof=True) 414 if block.content and block.content_type == 'image/png': 415 return (block.decoded_content, block.content_hash) 416 return (None, block.content_hash) 417 418 def _read_header(self, block, line, header_text, header_attr, header_filter=None): 419 if line.startswith(header_text) and getattr(block, header_attr) is None: 420 value = line.split()[1] 421 if header_filter: 422 value = header_filter(value) 423 setattr(block, header_attr, value) 424 return True 425 return False 426 427 def _process_stdout_line(self, block, line): 428 if (self._read_header(block, line, 'Content-Type: ', 'content_type') 429 or self._read_header(block, line, 'Content-Transfer-Encoding: ', 'encoding') 430 or self._read_header(block, line, 'Content-Length: ', '_content_length', int) 431 or self._read_header(block, line, 'ActualHash: ', 'content_hash') 432 or self._read_header(block, line, 'DumpMalloc: ', 'malloc') 433 or self._read_header(block, line, 'DumpJSHeap: ', 'js_heap')): 434 return 435 # Note, we're not reading ExpectedHash: here, but we could. 436 # If the line wasn't a header, we just append it to the content. 437 block.content += line 438 439 def _strip_eof(self, line): 440 if line and line.endswith("#EOF\n"): 441 return line[:-5], True 442 if line and line.endswith("#EOF\r\n"): 443 _log.error("Got a CRLF-terminated #EOF - this is a driver bug.") 444 return line[:-6], True 445 return line, False 446 447 def _read_block(self, deadline, wait_for_stderr_eof=False): 448 block = ContentBlock() 449 out_seen_eof = False 450 451 while not self.has_crashed(): 452 if out_seen_eof and (self.err_seen_eof or not wait_for_stderr_eof): 453 break 454 455 if self.err_seen_eof: 456 out_line = self._server_process.read_stdout_line(deadline) 457 err_line = None 458 elif out_seen_eof: 459 out_line = None 460 err_line = self._server_process.read_stderr_line(deadline) 461 else: 462 out_line, err_line = self._server_process.read_either_stdout_or_stderr_line(deadline) 463 464 if self._server_process.timed_out or self.has_crashed(): 465 break 466 467 if out_line: 468 assert not out_seen_eof 469 out_line, out_seen_eof = self._strip_eof(out_line) 470 if err_line: 471 assert not self.err_seen_eof 472 err_line, self.err_seen_eof = self._strip_eof(err_line) 473 474 if out_line: 475 if out_line[-1] != "\n": 476 _log.error("Last character read from DRT stdout line was not a newline! This indicates either a NRWT or DRT bug.") 477 content_length_before_header_check = block._content_length 478 self._process_stdout_line(block, out_line) 479 # FIXME: Unlike HTTP, DRT dumps the content right after printing a Content-Length header. 480 # Don't wait until we're done with headers, just read the binary blob right now. 481 if content_length_before_header_check != block._content_length: 482 if block._content_length > 0: 483 block.content = self._server_process.read_stdout(deadline, block._content_length) 484 else: 485 _log.error("Received content of type %s with Content-Length of 0! This indicates a bug in %s.", 486 block.content_type, self._server_process.name()) 487 488 if err_line: 489 if self._check_for_driver_crash(err_line): 490 break 491 if self._check_for_leak(err_line): 492 break 493 self.error_from_test += err_line 494 495 block.decode_content() 496 return block 497 498 499 class ContentBlock(object): 500 def __init__(self): 501 self.content_type = None 502 self.encoding = None 503 self.content_hash = None 504 self._content_length = None 505 # Content is treated as binary data even though the text output is usually UTF-8. 506 self.content = str() # FIXME: Should be bytearray() once we require Python 2.6. 507 self.decoded_content = None 508 self.malloc = None 509 self.js_heap = None 510 511 def decode_content(self): 512 if self.encoding == 'base64' and self.content is not None: 513 self.decoded_content = base64.b64decode(self.content) 514 else: 515 self.decoded_content = self.content 516