| #!/usr/bin/python |
| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import glob |
| import optparse |
| import os.path |
| import socket |
| import sys |
| import thread |
| import time |
| import urllib |
| |
| # Allow the import of third party modules |
| script_dir = os.path.dirname(os.path.abspath(__file__)) |
| sys.path.insert(0, os.path.join(script_dir, '../../../../third_party/')) |
| sys.path.insert(0, os.path.join(script_dir, '../../../../tools/valgrind/')) |
| sys.path.insert(0, os.path.join(script_dir, '../../../../testing/')) |
| |
| import browsertester.browserlauncher |
| import browsertester.rpclistener |
| import browsertester.server |
| |
| import memcheck_analyze |
| |
| import test_env |
| |
| def BuildArgParser(): |
| usage = 'usage: %prog [options]' |
| parser = optparse.OptionParser(usage) |
| |
| parser.add_option('-p', '--port', dest='port', action='store', type='int', |
| default='0', help='The TCP port the server will bind to. ' |
| 'The default is to pick an unused port number.') |
| parser.add_option('--browser_path', dest='browser_path', action='store', |
| type='string', default=None, |
| help='Use the browser located here.') |
| parser.add_option('--map_file', dest='map_files', action='append', |
| type='string', nargs=2, default=[], |
| metavar='DEST SRC', |
| help='Add file SRC to be served from the HTTP server, ' |
| 'to be made visible under the path DEST.') |
| parser.add_option('--serving_dir', dest='serving_dirs', action='append', |
| type='string', default=[], |
| metavar='DIRNAME', |
| help='Add directory DIRNAME to be served from the HTTP ' |
| 'server to be made visible under the root.') |
| parser.add_option('--output_dir', dest='output_dir', action='store', |
| type='string', default=None, |
| metavar='DIRNAME', |
| help='Set directory DIRNAME to be the output directory ' |
| 'when POSTing data to the server. NOTE: if this flag is ' |
| 'not set, POSTs will fail.') |
| parser.add_option('--test_arg', dest='test_args', action='append', |
| type='string', nargs=2, default=[], |
| metavar='KEY VALUE', |
| help='Parameterize the test with a key/value pair.') |
| parser.add_option('--redirect_url', dest='map_redirects', action='append', |
| type='string', nargs=2, default=[], |
| metavar='DEST SRC', |
| help='Add a redirect to the HTTP server, ' |
| 'requests for SRC will result in a redirect (302) to DEST.') |
| parser.add_option('-f', '--file', dest='files', action='append', |
| type='string', default=[], |
| metavar='FILENAME', |
| help='Add a file to serve from the HTTP server, to be ' |
| 'made visible in the root directory. ' |
| '"--file path/to/foo.html" is equivalent to ' |
| '"--map_file foo.html path/to/foo.html"') |
| parser.add_option('--mime_type', dest='mime_types', action='append', |
| type='string', nargs=2, default=[], metavar='DEST SRC', |
| help='Map file extension SRC to MIME type DEST when ' |
| 'serving it from the HTTP server.') |
| parser.add_option('-u', '--url', dest='url', action='store', |
| type='string', default=None, |
| help='The webpage to load.') |
| parser.add_option('--ppapi_plugin', dest='ppapi_plugin', action='store', |
| type='string', default=None, |
| help='Use the browser plugin located here.') |
| parser.add_option('--ppapi_plugin_mimetype', dest='ppapi_plugin_mimetype', |
| action='store', type='string', default='application/x-nacl', |
| help='Associate this mimetype with the browser plugin. ' |
| 'Unused if --ppapi_plugin is not specified.') |
| parser.add_option('--sel_ldr', dest='sel_ldr', action='store', |
| type='string', default=None, |
| help='Use the sel_ldr located here.') |
| parser.add_option('--sel_ldr_bootstrap', dest='sel_ldr_bootstrap', |
| action='store', type='string', default=None, |
| help='Use the bootstrap loader located here.') |
| parser.add_option('--irt_library', dest='irt_library', action='store', |
| type='string', default=None, |
| help='Use the integrated runtime (IRT) library ' |
| 'located here.') |
| parser.add_option('--interactive', dest='interactive', action='store_true', |
| default=False, help='Do not quit after testing is done. ' |
| 'Handy for iterative development. Disables timeout.') |
| parser.add_option('--debug', dest='debug', action='store_true', default=False, |
| help='Request debugging output from browser.') |
| parser.add_option('--timeout', dest='timeout', action='store', type='float', |
| default=5.0, |
| help='The maximum amount of time to wait, in seconds, for ' |
| 'the browser to make a request. The timer resets with each ' |
| 'request.') |
| parser.add_option('--hard_timeout', dest='hard_timeout', action='store', |
| type='float', default=None, |
| help='The maximum amount of time to wait, in seconds, for ' |
| 'the entire test. This will kill runaway tests. ') |
| parser.add_option('--allow_404', dest='allow_404', action='store_true', |
| default=False, |
| help='Allow 404s to occur without failing the test.') |
| parser.add_option('-b', '--bandwidth', dest='bandwidth', action='store', |
| type='float', default='0.0', |
| help='The amount of bandwidth (megabits / second) to ' |
| 'simulate between the client and the server. This used for ' |
| 'replies with file payloads. All other responses are ' |
| 'assumed to be short. Bandwidth values <= 0.0 are assumed ' |
| 'to mean infinite bandwidth.') |
| parser.add_option('--extension', dest='browser_extensions', action='append', |
| type='string', default=[], |
| help='Load the browser extensions located at the list of ' |
| 'paths. Note: this currently only works with the Chrome ' |
| 'browser.') |
| parser.add_option('--tool', dest='tool', action='store', |
| type='string', default=None, |
| help='Run tests under a tool.') |
| parser.add_option('--browser_flag', dest='browser_flags', action='append', |
| type='string', default=[], |
| help='Additional flags for the chrome command.') |
| parser.add_option('--enable_ppapi_dev', dest='enable_ppapi_dev', |
| action='store', type='int', default=1, |
| help='Enable/disable PPAPI Dev interfaces while testing.') |
| parser.add_option('--nacl_exe_stdin', dest='nacl_exe_stdin', |
| type='string', default=None, |
| help='Redirect standard input of NaCl executable.') |
| parser.add_option('--nacl_exe_stdout', dest='nacl_exe_stdout', |
| type='string', default=None, |
| help='Redirect standard output of NaCl executable.') |
| parser.add_option('--nacl_exe_stderr', dest='nacl_exe_stderr', |
| type='string', default=None, |
| help='Redirect standard error of NaCl executable.') |
| parser.add_option('--expect_browser_process_crash', |
| dest='expect_browser_process_crash', |
| action='store_true', |
| help='Do not signal a failure if the browser process ' |
| 'crashes') |
| parser.add_option('--enable_crash_reporter', dest='enable_crash_reporter', |
| action='store_true', default=False, |
| help='Force crash reporting on.') |
| parser.add_option('--enable_sockets', dest='enable_sockets', |
| action='store_true', default=False, |
| help='Pass --allow-nacl-socket-api=<host> to Chrome, where ' |
| '<host> is the name of the browser tester\'s web server.') |
| |
| return parser |
| |
| |
| def ProcessToolLogs(options, logs_dir): |
| if options.tool == 'memcheck': |
| analyzer = memcheck_analyze.MemcheckAnalyzer('', use_gdb=True) |
| logs_wildcard = 'xml.*' |
| files = glob.glob(os.path.join(logs_dir, logs_wildcard)) |
| retcode = analyzer.Report(files, options.url) |
| return retcode |
| |
| |
| # An exception that indicates possible flake. |
| class RetryTest(Exception): |
| pass |
| |
| |
| def DumpNetLog(netlog): |
| sys.stdout.write('\n') |
| if not os.path.isfile(netlog): |
| sys.stdout.write('Cannot find netlog, did Chrome actually launch?\n') |
| else: |
| sys.stdout.write('Netlog exists (%d bytes).\n' % os.path.getsize(netlog)) |
| sys.stdout.write('Dumping it to stdout.\n\n\n') |
| sys.stdout.write(open(netlog).read()) |
| sys.stdout.write('\n\n\n') |
| |
| |
| # Try to discover the real IP address of this machine. If we can't figure it |
| # out, fall back to localhost. |
| # A windows bug makes using the loopback interface flaky in rare cases. |
| # http://code.google.com/p/chromium/issues/detail?id=114369 |
| def GetHostName(): |
| host = 'localhost' |
| try: |
| host = socket.gethostbyname(socket.gethostname()) |
| except Exception: |
| pass |
| if host == '0.0.0.0': |
| host = 'localhost' |
| return host |
| |
| |
| def RunTestsOnce(url, options): |
| # Set the default here so we're assured hard_timeout will be defined. |
| # Tests, such as run_inbrowser_trusted_crash_in_startup_test, may not use the |
| # RunFromCommand line entry point - and otherwise get stuck in an infinite |
| # loop when something goes wrong and the hard timeout is not set. |
| # http://code.google.com/p/chromium/issues/detail?id=105406 |
| if options.hard_timeout is None: |
| options.hard_timeout = options.timeout * 4 |
| |
| options.files.append(os.path.join(script_dir, 'browserdata', 'nacltest.js')) |
| |
| # Setup the environment with the setuid sandbox path. |
| os.environ.update(test_env.get_sandbox_env(os.environ)) |
| |
| # Create server |
| host = GetHostName() |
| try: |
| server = browsertester.server.Create(host, options.port) |
| except Exception: |
| sys.stdout.write('Could not bind %r, falling back to localhost.\n' % host) |
| server = browsertester.server.Create('localhost', options.port) |
| |
| # If port 0 has been requested, an arbitrary port will be bound so we need to |
| # query it. Older version of Python do not set server_address correctly when |
| # The requested port is 0 so we need to break encapsulation and query the |
| # socket directly. |
| host, port = server.socket.getsockname() |
| |
| file_mapping = dict(options.map_files) |
| for filename in options.files: |
| file_mapping[os.path.basename(filename)] = filename |
| for server_path, real_path in file_mapping.iteritems(): |
| if not os.path.exists(real_path): |
| raise AssertionError('\'%s\' does not exist.' % real_path) |
| mime_types = {} |
| for ext, mime_type in options.mime_types: |
| mime_types['.' + ext] = mime_type |
| |
| def ShutdownCallback(): |
| server.TestingEnded() |
| close_browser = options.tool is not None and not options.interactive |
| return close_browser |
| |
| listener = browsertester.rpclistener.RPCListener(ShutdownCallback) |
| server.Configure(file_mapping, |
| dict(options.map_redirects), |
| mime_types, |
| options.allow_404, |
| options.bandwidth, |
| listener, |
| options.serving_dirs, |
| options.output_dir) |
| |
| browser = browsertester.browserlauncher.ChromeLauncher(options) |
| |
| full_url = 'http://%s:%d/%s' % (host, port, url) |
| if len(options.test_args) > 0: |
| full_url += '?' + urllib.urlencode(options.test_args) |
| browser.Run(full_url, host, port) |
| server.TestingBegun(0.125) |
| |
| # In Python 2.5, server.handle_request may block indefinitely. Serving pages |
| # is done in its own thread so the main thread can time out as needed. |
| def Serve(): |
| while server.test_in_progress or options.interactive: |
| server.handle_request() |
| thread.start_new_thread(Serve, ()) |
| |
| tool_failed = False |
| time_started = time.time() |
| |
| def HardTimeout(total_time): |
| return total_time >= 0.0 and time.time() - time_started >= total_time |
| |
| try: |
| while server.test_in_progress or options.interactive: |
| if not browser.IsRunning(): |
| if options.expect_browser_process_crash: |
| break |
| listener.ServerError('Browser process ended during test ' |
| '(return code %r)' % browser.GetReturnCode()) |
| # If Chrome exits prematurely without making a single request to the |
| # web server, this is probally a Chrome crash-on-launch bug not related |
| # to the test at hand. Retry, unless we're in interactive mode. In |
| # interactive mode the user may manually close the browser, so don't |
| # retry (it would just be annoying.) |
| if not server.received_request and not options.interactive: |
| raise RetryTest('Chrome failed to launch.') |
| else: |
| break |
| elif not options.interactive and server.TimedOut(options.timeout): |
| js_time = server.TimeSinceJSHeartbeat() |
| err = 'Did not hear from the test for %.1f seconds.' % options.timeout |
| err += '\nHeard from Javascript %.1f seconds ago.' % js_time |
| if js_time > 2.0: |
| err += '\nThe renderer probably hung or crashed.' |
| else: |
| err += '\nThe test probably did not get a callback that it expected.' |
| listener.ServerError(err) |
| if not server.received_request: |
| raise RetryTest('Chrome hung before running the test.') |
| break |
| elif not options.interactive and HardTimeout(options.hard_timeout): |
| listener.ServerError('The test took over %.1f seconds. This is ' |
| 'probably a runaway test.' % options.hard_timeout) |
| break |
| else: |
| # If Python 2.5 support is dropped, stick server.handle_request() here. |
| time.sleep(0.125) |
| |
| if options.tool: |
| sys.stdout.write('##################### Waiting for the tool to exit\n') |
| browser.WaitForProcessDeath() |
| sys.stdout.write('##################### Processing tool logs\n') |
| tool_failed = ProcessToolLogs(options, browser.tool_log_dir) |
| |
| finally: |
| try: |
| if listener.ever_failed and not options.interactive: |
| if not server.received_request: |
| sys.stdout.write('\nNo URLs were served by the test runner. It is ' |
| 'unlikely this test failure has anything to do with ' |
| 'this particular test.\n') |
| DumpNetLog(browser.NetLogName()) |
| except Exception: |
| listener.ever_failed = 1 |
| # Try to let the browser clean itself up normally before killing it. |
| sys.stdout.write('##################### Terminating the browser\n') |
| browser.WaitForProcessDeath() |
| if browser.IsRunning(): |
| sys.stdout.write('##################### TERM failed, KILLING\n') |
| # Always call Cleanup; it kills the process, but also removes the |
| # user-data-dir. |
| browser.Cleanup() |
| # We avoid calling server.server_close() here because it causes |
| # the HTTP server thread to exit uncleanly with an EBADF error, |
| # which adds noise to the logs (though it does not cause the test |
| # to fail). server_close() does not attempt to tell the server |
| # loop to shut down before closing the socket FD it is |
| # select()ing. Since we are about to exit, we don't really need |
| # to close the socket FD. |
| |
| if tool_failed: |
| return 2 |
| elif listener.ever_failed: |
| return 1 |
| else: |
| return 0 |
| |
| |
| # This is an entrypoint for tests that treat the browser tester as a Python |
| # library rather than an opaque script. |
| # (e.g. run_inbrowser_trusted_crash_in_startup_test) |
| def Run(url, options): |
| result = 1 |
| attempt = 1 |
| while True: |
| try: |
| result = RunTestsOnce(url, options) |
| if result: |
| # Currently (2013/11/15) nacl_integration is fairly flaky and there is |
| # not enough time to look into it. Retry if the test fails for any |
| # reason. Note that in general this test runner tries to only retry |
| # when a known flake is encountered. (See the other raise |
| # RetryTest(..)s in this file.) This blanket retry means that those |
| # other cases could be removed without changing the behavior of the test |
| # runner, but it is hoped that this blanket retry will eventually be |
| # unnecessary and subsequently removed. The more precise retries have |
| # been left in place to preserve the knowledge. |
| raise RetryTest('HACK retrying failed test.') |
| break |
| except RetryTest: |
| # Only retry once. |
| if attempt < 2: |
| sys.stdout.write('\n@@@STEP_WARNINGS@@@\n') |
| sys.stdout.write('WARNING: suspected flake, retrying test!\n\n') |
| attempt += 1 |
| continue |
| else: |
| sys.stdout.write('\nWARNING: failed too many times, not retrying.\n\n') |
| result = 1 |
| break |
| return result |
| |
| |
| def RunFromCommandLine(): |
| parser = BuildArgParser() |
| options, args = parser.parse_args() |
| |
| if len(args) != 0: |
| print args |
| parser.error('Invalid arguments') |
| |
| # Validate the URL |
| url = options.url |
| if url is None: |
| parser.error('Must specify a URL') |
| |
| return Run(url, options) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(RunFromCommandLine()) |