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