blob: c30d1141eaa22a8647c7addaacfa5b7e0bc18ba4 [file] [log] [blame]
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +00001# 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
29import base64
30import copy
31import logging
32import re
33import shlex
34import sys
35import time
36import os
37
38from webkitpy.common.system import path
Torne (Richard Coles)926b0012013-03-28 15:32:48 +000039from webkitpy.common.system.profiler import ProfilerFactory
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000040
41
42_log = logging.getLogger(__name__)
43
44
45class 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
54class DriverOutput(object):
55 """Groups information about a output from driver for easy passing
56 and post-processing of data."""
57
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000058 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)926b0012013-03-28 15:32:48 +000060 crashed_pid=None, crash_log=None, pid=None):
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000061 # 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)926b0012013-03-28 15:32:48 +000075 self.pid = pid
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000076
77 def has_stderr(self):
78 return bool(self.error)
79
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000080
81class Driver(object):
Ben Murdoch591b9582013-07-10 11:41:44 +010082 """object for running test(s) using content_shell or other driver."""
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000083
84 def __init__(self, port, worker_number, pixel_tests, no_timeout=False):
85 """Initialize a Driver to subsequently run tests.
86
Ben Murdoch591b9582013-07-10 11:41:44 +010087 Typically this routine will spawn content_shell in a config
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +000088 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)93ac45c2013-05-29 14:40:20 +0100116 self._current_cmd_line = None
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000117
118 self._measurements = {}
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000119 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)5c87bf82012-11-14 11:46:17 +0000125
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)926b0012013-03-28 15:32:48 +0000136 Returns a DriverOutput object.
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000137 """
Torne (Richard Coles)93ac45c2013-05-29 14:40:20 +0100138 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)5c87bf82012-11-14 11:46:17 +0000145 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)926b0012013-03-28 15:32:48 +0000160 pid = self._server_process.pid()
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000161
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)926b0012013-03-28 15:32:48 +0000192 crashed_pid=self._crashed_pid, crash_log=crash_log, pid=pid)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000193
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)93ac45c2013-05-29 14:40:20 +0100253 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)5c87bf82012-11-14 11:46:17 +0000255 self._start(pixel_tests, per_test_args)
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000256 self._run_post_start_tasks()
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000257
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000258 def _setup_environ_for_driver(self, environment):
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000259 environment['DYLD_LIBRARY_PATH'] = self._port._build_path()
260 environment['DYLD_FRAMEWORK_PATH'] = self._port._build_path()
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000261 environment['LOCAL_RESOURCE_ROOT'] = self._port.layout_tests_dir()
262 if 'WEBKITOUTPUTDIR' in os.environ:
263 environment['WEBKITOUTPUTDIR'] = os.environ['WEBKITOUTPUTDIR']
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000264 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)5c87bf82012-11-14 11:46:17 +0000274 self._crashed_process_name = None
275 self._crashed_pid = None
Torne (Richard Coles)93ac45c2013-05-29 14:40:20 +0100276 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)5c87bf82012-11-14 11:46:17 +0000278 self._server_process.start()
Torne (Richard Coles)93ac45c2013-05-29 14:40:20 +0100279 self._current_cmd_line = cmd_line
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000280
Torne (Richard Coles)926b0012013-03-28 15:32:48 +0000281 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)5c87bf82012-11-14 11:46:17 +0000290 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)926b0012013-03-28 15:32:48 +0000294 if self._profiler:
295 self._profiler.profile_after_exit()
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000296
297 if self._driver_tempdir:
298 self._port._filesystem.rmtree(str(self._driver_tempdir))
299 self._driver_tempdir = None
300
Torne (Richard Coles)93ac45c2013-05-29 14:40:20 +0100301 self._current_cmd_line = None
302
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000303 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)5c87bf82012-11-14 11:46:17 +0000306 if self._no_timeout:
307 cmd.append('--no-timeout')
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000308 cmd.extend(self._port.get_option('additional_drt_flag', []))
309 cmd.extend(self._port.additional_drt_flag())
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000310 cmd.extend(per_test_args)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000311 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)926b0012013-03-28 15:32:48 +0000332 self._port.sample_process(self._crashed_process_name, self._crashed_pid)
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000333 # 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)926b0012013-03-28 15:32:48 +0000352 if self._port.supports_per_test_timeout():
353 command += "'--timeout'%s" % driver_input.timeout
Torne (Richard Coles)5c87bf82012-11-14 11:46:17 +0000354 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 Murdoch591b9582013-07-10 11:41:44 +0100439 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)5c87bf82012-11-14 11:46:17 +0000444
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
454class 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