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