browser_tester.py revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
1#!/usr/bin/python 2# Copyright (c) 2012 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import glob 7import optparse 8import os.path 9import socket 10import sys 11import thread 12import time 13import urllib 14 15# Allow the import of third party modules 16script_dir = os.path.dirname(os.path.abspath(__file__)) 17sys.path.append(os.path.join(script_dir, '../../../../third_party/')) 18sys.path.append(os.path.join(script_dir, '../../../../tools/valgrind/')) 19sys.path.append(os.path.join(script_dir, '../../../../testing/')) 20 21import browsertester.browserlauncher 22import browsertester.rpclistener 23import browsertester.server 24 25import memcheck_analyze 26import tsan_analyze 27 28import test_env 29 30def BuildArgParser(): 31 usage = 'usage: %prog [options]' 32 parser = optparse.OptionParser(usage) 33 34 parser.add_option('-p', '--port', dest='port', action='store', type='int', 35 default='0', help='The TCP port the server will bind to. ' 36 'The default is to pick an unused port number.') 37 parser.add_option('--browser_path', dest='browser_path', action='store', 38 type='string', default=None, 39 help='Use the browser located here.') 40 parser.add_option('--map_file', dest='map_files', action='append', 41 type='string', nargs=2, default=[], 42 metavar='DEST SRC', 43 help='Add file SRC to be served from the HTTP server, ' 44 'to be made visible under the path DEST.') 45 parser.add_option('--serving_dir', dest='serving_dirs', action='append', 46 type='string', default=[], 47 metavar='DIRNAME', 48 help='Add directory DIRNAME to be served from the HTTP ' 49 'server to be made visible under the root.') 50 parser.add_option('--output_dir', dest='output_dir', action='store', 51 type='string', default=None, 52 metavar='DIRNAME', 53 help='Set directory DIRNAME to be the output directory ' 54 'when POSTing data to the server. NOTE: if this flag is ' 55 'not set, POSTs will fail.') 56 parser.add_option('--test_arg', dest='test_args', action='append', 57 type='string', nargs=2, default=[], 58 metavar='KEY VALUE', 59 help='Parameterize the test with a key/value pair.') 60 parser.add_option('--redirect_url', dest='map_redirects', action='append', 61 type='string', nargs=2, default=[], 62 metavar='DEST SRC', 63 help='Add a redirect to the HTTP server, ' 64 'requests for SRC will result in a redirect (302) to DEST.') 65 parser.add_option('-f', '--file', dest='files', action='append', 66 type='string', default=[], 67 metavar='FILENAME', 68 help='Add a file to serve from the HTTP server, to be ' 69 'made visible in the root directory. ' 70 '"--file path/to/foo.html" is equivalent to ' 71 '"--map_file foo.html path/to/foo.html"') 72 parser.add_option('--mime_type', dest='mime_types', action='append', 73 type='string', nargs=2, default=[], metavar='DEST SRC', 74 help='Map file extension SRC to MIME type DEST when ' 75 'serving it from the HTTP server.') 76 parser.add_option('-u', '--url', dest='url', action='store', 77 type='string', default=None, 78 help='The webpage to load.') 79 parser.add_option('--ppapi_plugin', dest='ppapi_plugin', action='store', 80 type='string', default=None, 81 help='Use the browser plugin located here.') 82 parser.add_option('--ppapi_plugin_mimetype', dest='ppapi_plugin_mimetype', 83 action='store', type='string', default='application/x-nacl', 84 help='Associate this mimetype with the browser plugin. ' 85 'Unused if --ppapi_plugin is not specified.') 86 parser.add_option('--sel_ldr', dest='sel_ldr', action='store', 87 type='string', default=None, 88 help='Use the sel_ldr located here.') 89 parser.add_option('--sel_ldr_bootstrap', dest='sel_ldr_bootstrap', 90 action='store', type='string', default=None, 91 help='Use the bootstrap loader located here.') 92 parser.add_option('--irt_library', dest='irt_library', action='store', 93 type='string', default=None, 94 help='Use the integrated runtime (IRT) library ' 95 'located here.') 96 parser.add_option('--interactive', dest='interactive', action='store_true', 97 default=False, help='Do not quit after testing is done. ' 98 'Handy for iterative development. Disables timeout.') 99 parser.add_option('--debug', dest='debug', action='store_true', default=False, 100 help='Request debugging output from browser.') 101 parser.add_option('--timeout', dest='timeout', action='store', type='float', 102 default=5.0, 103 help='The maximum amount of time to wait, in seconds, for ' 104 'the browser to make a request. The timer resets with each ' 105 'request.') 106 parser.add_option('--hard_timeout', dest='hard_timeout', action='store', 107 type='float', default=None, 108 help='The maximum amount of time to wait, in seconds, for ' 109 'the entire test. This will kill runaway tests. ') 110 parser.add_option('--allow_404', dest='allow_404', action='store_true', 111 default=False, 112 help='Allow 404s to occur without failing the test.') 113 parser.add_option('-b', '--bandwidth', dest='bandwidth', action='store', 114 type='float', default='0.0', 115 help='The amount of bandwidth (megabits / second) to ' 116 'simulate between the client and the server. This used for ' 117 'replies with file payloads. All other responses are ' 118 'assumed to be short. Bandwidth values <= 0.0 are assumed ' 119 'to mean infinite bandwidth.') 120 parser.add_option('--extension', dest='browser_extensions', action='append', 121 type='string', default=[], 122 help='Load the browser extensions located at the list of ' 123 'paths. Note: this currently only works with the Chrome ' 124 'browser.') 125 parser.add_option('--tool', dest='tool', action='store', 126 type='string', default=None, 127 help='Run tests under a tool.') 128 parser.add_option('--browser_flag', dest='browser_flags', action='append', 129 type='string', default=[], 130 help='Additional flags for the chrome command.') 131 parser.add_option('--enable_ppapi_dev', dest='enable_ppapi_dev', 132 action='store', type='int', default=1, 133 help='Enable/disable PPAPI Dev interfaces while testing.') 134 parser.add_option('--nacl_exe_stdin', dest='nacl_exe_stdin', 135 type='string', default=None, 136 help='Redirect standard input of NaCl executable.') 137 parser.add_option('--nacl_exe_stdout', dest='nacl_exe_stdout', 138 type='string', default=None, 139 help='Redirect standard output of NaCl executable.') 140 parser.add_option('--nacl_exe_stderr', dest='nacl_exe_stderr', 141 type='string', default=None, 142 help='Redirect standard error of NaCl executable.') 143 parser.add_option('--expect_browser_process_crash', 144 dest='expect_browser_process_crash', 145 action='store_true', 146 help='Do not signal a failure if the browser process ' 147 'crashes') 148 parser.add_option('--enable_crash_reporter', dest='enable_crash_reporter', 149 action='store_true', default=False, 150 help='Force crash reporting on.') 151 parser.add_option('--enable_sockets', dest='enable_sockets', 152 action='store_true', default=False, 153 help='Pass --allow-nacl-socket-api=<host> to Chrome, where ' 154 '<host> is the name of the browser tester\'s web server.') 155 156 return parser 157 158 159def ProcessToolLogs(options, logs_dir): 160 if options.tool == 'memcheck': 161 analyzer = memcheck_analyze.MemcheckAnalyzer('', use_gdb=True) 162 logs_wildcard = 'xml.*' 163 elif options.tool == 'tsan': 164 analyzer = tsan_analyze.TsanAnalyzer('', use_gdb=True) 165 logs_wildcard = 'log.*' 166 files = glob.glob(os.path.join(logs_dir, logs_wildcard)) 167 retcode = analyzer.Report(files, options.url) 168 return retcode 169 170 171# An exception that indicates possible flake. 172class RetryTest(Exception): 173 pass 174 175 176def DumpNetLog(netlog): 177 sys.stdout.write('\n') 178 if not os.path.isfile(netlog): 179 sys.stdout.write('Cannot find netlog, did Chrome actually launch?\n') 180 else: 181 sys.stdout.write('Netlog exists (%d bytes).\n' % os.path.getsize(netlog)) 182 sys.stdout.write('Dumping it to stdout.\n\n\n') 183 sys.stdout.write(open(netlog).read()) 184 sys.stdout.write('\n\n\n') 185 186 187# Try to discover the real IP address of this machine. If we can't figure it 188# out, fall back to localhost. 189# A windows bug makes using the loopback interface flaky in rare cases. 190# http://code.google.com/p/chromium/issues/detail?id=114369 191def GetHostName(): 192 host = 'localhost' 193 try: 194 host = socket.gethostbyname(socket.gethostname()) 195 except Exception: 196 pass 197 if host == '0.0.0.0': 198 host = 'localhost' 199 return host 200 201 202def RunTestsOnce(url, options): 203 # Set the default here so we're assured hard_timeout will be defined. 204 # Tests, such as run_inbrowser_trusted_crash_in_startup_test, may not use the 205 # RunFromCommand line entry point - and otherwise get stuck in an infinite 206 # loop when something goes wrong and the hard timeout is not set. 207 # http://code.google.com/p/chromium/issues/detail?id=105406 208 if options.hard_timeout is None: 209 options.hard_timeout = options.timeout * 4 210 211 options.files.append(os.path.join(script_dir, 'browserdata', 'nacltest.js')) 212 213 # Setup the environment with the setuid sandbox path. 214 test_env.enable_sandbox_if_required(os.environ) 215 216 # Create server 217 host = GetHostName() 218 try: 219 server = browsertester.server.Create(host, options.port) 220 except Exception: 221 sys.stdout.write('Could not bind %r, falling back to localhost.\n' % host) 222 server = browsertester.server.Create('localhost', options.port) 223 224 # If port 0 has been requested, an arbitrary port will be bound so we need to 225 # query it. Older version of Python do not set server_address correctly when 226 # The requested port is 0 so we need to break encapsulation and query the 227 # socket directly. 228 host, port = server.socket.getsockname() 229 230 file_mapping = dict(options.map_files) 231 for filename in options.files: 232 file_mapping[os.path.basename(filename)] = filename 233 for server_path, real_path in file_mapping.iteritems(): 234 if not os.path.exists(real_path): 235 raise AssertionError('\'%s\' does not exist.' % real_path) 236 mime_types = {} 237 for ext, mime_type in options.mime_types: 238 mime_types['.' + ext] = mime_type 239 240 def ShutdownCallback(): 241 server.TestingEnded() 242 close_browser = options.tool is not None and not options.interactive 243 return close_browser 244 245 listener = browsertester.rpclistener.RPCListener(ShutdownCallback) 246 server.Configure(file_mapping, 247 dict(options.map_redirects), 248 mime_types, 249 options.allow_404, 250 options.bandwidth, 251 listener, 252 options.serving_dirs, 253 options.output_dir) 254 255 browser = browsertester.browserlauncher.ChromeLauncher(options) 256 257 full_url = 'http://%s:%d/%s' % (host, port, url) 258 if len(options.test_args) > 0: 259 full_url += '?' + urllib.urlencode(options.test_args) 260 browser.Run(full_url, host, port) 261 server.TestingBegun(0.125) 262 263 # In Python 2.5, server.handle_request may block indefinitely. Serving pages 264 # is done in its own thread so the main thread can time out as needed. 265 def Serve(): 266 while server.test_in_progress or options.interactive: 267 server.handle_request() 268 thread.start_new_thread(Serve, ()) 269 270 tool_failed = False 271 time_started = time.time() 272 273 def HardTimeout(total_time): 274 return total_time >= 0.0 and time.time() - time_started >= total_time 275 276 try: 277 while server.test_in_progress or options.interactive: 278 if not browser.IsRunning(): 279 if options.expect_browser_process_crash: 280 break 281 listener.ServerError('Browser process ended during test ' 282 '(return code %r)' % browser.GetReturnCode()) 283 # If Chrome exits prematurely without making a single request to the 284 # web server, this is probally a Chrome crash-on-launch bug not related 285 # to the test at hand. Retry, unless we're in interactive mode. In 286 # interactive mode the user may manually close the browser, so don't 287 # retry (it would just be annoying.) 288 if not server.received_request and not options.interactive: 289 raise RetryTest('Chrome failed to launch.') 290 else: 291 break 292 elif not options.interactive and server.TimedOut(options.timeout): 293 js_time = server.TimeSinceJSHeartbeat() 294 err = 'Did not hear from the test for %.1f seconds.' % options.timeout 295 err += '\nHeard from Javascript %.1f seconds ago.' % js_time 296 if js_time > 2.0: 297 err += '\nThe renderer probably hung or crashed.' 298 else: 299 err += '\nThe test probably did not get a callback that it expected.' 300 listener.ServerError(err) 301 break 302 elif not options.interactive and HardTimeout(options.hard_timeout): 303 listener.ServerError('The test took over %.1f seconds. This is ' 304 'probably a runaway test.' % options.hard_timeout) 305 break 306 else: 307 # If Python 2.5 support is dropped, stick server.handle_request() here. 308 time.sleep(0.125) 309 310 if options.tool: 311 sys.stdout.write('##################### Waiting for the tool to exit\n') 312 browser.WaitForProcessDeath() 313 sys.stdout.write('##################### Processing tool logs\n') 314 tool_failed = ProcessToolLogs(options, browser.tool_log_dir) 315 316 finally: 317 try: 318 if listener.ever_failed and not options.interactive: 319 if not server.received_request: 320 sys.stdout.write('\nNo URLs were served by the test runner. It is ' 321 'unlikely this test failure has anything to do with ' 322 'this particular test.\n') 323 DumpNetLog(browser.NetLogName()) 324 except Exception: 325 listener.ever_failed = 1 326 # Try to let the browser clean itself up normally before killing it. 327 sys.stdout.write('##################### Terminating the browser\n') 328 browser.WaitForProcessDeath() 329 if browser.IsRunning(): 330 sys.stdout.write('##################### TERM failed, KILLING\n') 331 # Always call Cleanup; it kills the process, but also removes the 332 # user-data-dir. 333 browser.Cleanup() 334 # We avoid calling server.server_close() here because it causes 335 # the HTTP server thread to exit uncleanly with an EBADF error, 336 # which adds noise to the logs (though it does not cause the test 337 # to fail). server_close() does not attempt to tell the server 338 # loop to shut down before closing the socket FD it is 339 # select()ing. Since we are about to exit, we don't really need 340 # to close the socket FD. 341 342 if tool_failed: 343 return 2 344 elif listener.ever_failed: 345 return 1 346 else: 347 return 0 348 349 350# This is an entrypoint for tests that treat the browser tester as a Python 351# library rather than an opaque script. 352# (e.g. run_inbrowser_trusted_crash_in_startup_test) 353def Run(url, options): 354 result = 1 355 attempt = 1 356 while True: 357 try: 358 result = RunTestsOnce(url, options) 359 break 360 except RetryTest: 361 # Only retry once. 362 if attempt < 2: 363 sys.stdout.write('\n@@@STEP_WARNINGS@@@\n') 364 sys.stdout.write('WARNING: suspected flake, retrying test!\n\n') 365 attempt += 1 366 continue 367 else: 368 sys.stdout.write('\nWARNING: failed too many times, not retrying.\n\n') 369 result = 1 370 break 371 return result 372 373 374def RunFromCommandLine(): 375 parser = BuildArgParser() 376 options, args = parser.parse_args() 377 378 if len(args) != 0: 379 print args 380 parser.error('Invalid arguments') 381 382 # Validate the URL 383 url = options.url 384 if url is None: 385 parser.error('Must specify a URL') 386 387 return Run(url, options) 388 389 390if __name__ == '__main__': 391 sys.exit(RunFromCommandLine()) 392