Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ppapi / native_client / tools / browser_tester / browsertester / browserlauncher.py
blob1abbb4479d891c014823f896fbd45518f98548a8
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.
6 import os.path
7 import re
8 import shutil
9 import sys
10 import tempfile
11 import time
12 import urlparse
14 import browserprocess
16 class LaunchFailure(Exception):
17 pass
20 def GetPlatform():
21 if sys.platform == 'darwin':
22 platform = 'mac'
23 elif sys.platform.startswith('linux'):
24 platform = 'linux'
25 elif sys.platform in ('cygwin', 'win32'):
26 platform = 'windows'
27 else:
28 raise LaunchFailure('Unknown platform: %s' % sys.platform)
29 return platform
32 PLATFORM = GetPlatform()
35 def SelectRunCommand():
36 # The subprocess module added support for .kill in Python 2.6
37 assert (sys.version_info[0] >= 3 or (sys.version_info[0] == 2 and
38 sys.version_info[1] >= 6))
39 if PLATFORM == 'linux':
40 return browserprocess.RunCommandInProcessGroup
41 else:
42 return browserprocess.RunCommandWithSubprocess
45 RunCommand = SelectRunCommand()
47 def RemoveDirectory(path):
48 retry = 5
49 sleep_time = 0.25
50 while True:
51 try:
52 shutil.rmtree(path)
53 except Exception:
54 # Windows processes sometime hang onto files too long
55 if retry > 0:
56 retry -= 1
57 time.sleep(sleep_time)
58 sleep_time *= 2
59 else:
60 # No luck - don't mask the error
61 raise
62 else:
63 # succeeded
64 break
68 # In Windows, subprocess seems to have an issue with file names that
69 # contain spaces.
70 def EscapeSpaces(path):
71 if PLATFORM == 'windows' and ' ' in path:
72 return '"%s"' % path
73 return path
76 def MakeEnv(options):
77 env = dict(os.environ)
78 # Enable PPAPI Dev interfaces for testing.
79 env['NACL_ENABLE_PPAPI_DEV'] = str(options.enable_ppapi_dev)
80 if options.debug:
81 env['NACL_PLUGIN_DEBUG'] = '1'
82 # env['NACL_SRPC_DEBUG'] = '1'
83 return env
86 class BrowserLauncher(object):
88 WAIT_TIME = 20
89 WAIT_STEPS = 80
90 SLEEP_TIME = float(WAIT_TIME) / WAIT_STEPS
92 def __init__(self, options):
93 self.options = options
94 self.profile = None
95 self.binary = None
96 self.tool_log_dir = None
98 def KnownPath(self):
99 raise NotImplementedError
101 def BinaryName(self):
102 raise NotImplementedError
104 def CreateProfile(self):
105 raise NotImplementedError
107 def MakeCmd(self, url, host, port):
108 raise NotImplementedError
110 def CreateToolLogDir(self):
111 self.tool_log_dir = tempfile.mkdtemp(prefix='vglogs_')
112 return self.tool_log_dir
114 def FindBinary(self):
115 if self.options.browser_path:
116 return self.options.browser_path
117 else:
118 path = self.KnownPath()
119 if path is None or not os.path.exists(path):
120 raise LaunchFailure('Cannot find the browser directory')
121 binary = os.path.join(path, self.BinaryName())
122 if not os.path.exists(binary):
123 raise LaunchFailure('Cannot find the browser binary')
124 return binary
126 def WaitForProcessDeath(self):
127 self.browser_process.Wait(self.WAIT_STEPS, self.SLEEP_TIME)
129 def Cleanup(self):
130 self.browser_process.Kill()
132 RemoveDirectory(self.profile)
133 if self.tool_log_dir is not None:
134 RemoveDirectory(self.tool_log_dir)
136 def MakeProfileDirectory(self):
137 self.profile = tempfile.mkdtemp(prefix='browserprofile_')
138 return self.profile
140 def SetStandardStream(self, env, var_name, redirect_file, is_output):
141 if redirect_file is None:
142 return
143 file_prefix = 'file:'
144 dev_prefix = 'dev:'
145 debug_warning = 'DEBUG_ONLY:'
146 # logic must match src/trusted/service_runtime/nacl_resource.*
147 # resource specification notation. file: is the default
148 # interpretation, so we must have an exhaustive list of
149 # alternative schemes accepted. if we remove the file-is-default
150 # interpretation, replace with
151 # is_file = redirect_file.startswith(file_prefix)
152 # and remove the list of non-file schemes.
153 is_file = (not (redirect_file.startswith(dev_prefix) or
154 redirect_file.startswith(debug_warning + dev_prefix)))
155 if is_file:
156 if redirect_file.startswith(file_prefix):
157 bare_file = redirect_file[len(file_prefix)]
158 else:
159 bare_file = redirect_file
160 # why always abspath? does chrome chdir or might it in the
161 # future? this means we do not test/use the relative path case.
162 redirect_file = file_prefix + os.path.abspath(bare_file)
163 else:
164 bare_file = None # ensure error if used without checking is_file
165 env[var_name] = redirect_file
166 if is_output:
167 # sel_ldr appends program output to the file so we need to clear it
168 # in order to get the stable result.
169 if is_file:
170 if os.path.exists(bare_file):
171 os.remove(bare_file)
172 parent_dir = os.path.dirname(bare_file)
173 # parent directory may not exist.
174 if not os.path.exists(parent_dir):
175 os.makedirs(parent_dir)
177 def Launch(self, cmd, env):
178 browser_path = cmd[0]
179 if not os.path.exists(browser_path):
180 raise LaunchFailure('Browser does not exist %r'% browser_path)
181 if not os.access(browser_path, os.X_OK):
182 raise LaunchFailure('Browser cannot be executed %r (Is this binary on an '
183 'NFS volume?)' % browser_path)
184 if self.options.sel_ldr:
185 env['NACL_SEL_LDR'] = self.options.sel_ldr
186 if self.options.sel_ldr_bootstrap:
187 env['NACL_SEL_LDR_BOOTSTRAP'] = self.options.sel_ldr_bootstrap
188 if self.options.irt_library:
189 env['NACL_IRT_LIBRARY'] = self.options.irt_library
190 self.SetStandardStream(env, 'NACL_EXE_STDIN',
191 self.options.nacl_exe_stdin, False)
192 self.SetStandardStream(env, 'NACL_EXE_STDOUT',
193 self.options.nacl_exe_stdout, True)
194 self.SetStandardStream(env, 'NACL_EXE_STDERR',
195 self.options.nacl_exe_stderr, True)
196 print 'ENV:', ' '.join(['='.join(pair) for pair in env.iteritems()])
197 print 'LAUNCHING: %s' % ' '.join(cmd)
198 sys.stdout.flush()
199 self.browser_process = RunCommand(cmd, env=env)
201 def IsRunning(self):
202 return self.browser_process.IsRunning()
204 def GetReturnCode(self):
205 return self.browser_process.GetReturnCode()
207 def Run(self, url, host, port):
208 self.binary = EscapeSpaces(self.FindBinary())
209 self.profile = self.CreateProfile()
210 if self.options.tool is not None:
211 self.tool_log_dir = self.CreateToolLogDir()
212 cmd = self.MakeCmd(url, host, port)
213 self.Launch(cmd, MakeEnv(self.options))
216 def EnsureDirectory(path):
217 if not os.path.exists(path):
218 os.makedirs(path)
221 def EnsureDirectoryForFile(path):
222 EnsureDirectory(os.path.dirname(path))
225 class ChromeLauncher(BrowserLauncher):
227 def KnownPath(self):
228 if PLATFORM == 'linux':
229 # TODO(ncbray): look in path?
230 return '/opt/google/chrome'
231 elif PLATFORM == 'mac':
232 return '/Applications/Google Chrome.app/Contents/MacOS'
233 else:
234 homedir = os.path.expanduser('~')
235 path = os.path.join(homedir, r'AppData\Local\Google\Chrome\Application')
236 return path
238 def BinaryName(self):
239 if PLATFORM == 'mac':
240 return 'Google Chrome'
241 elif PLATFORM == 'windows':
242 return 'chrome.exe'
243 else:
244 return 'chrome'
246 def MakeEmptyJSONFile(self, path):
247 EnsureDirectoryForFile(path)
248 f = open(path, 'w')
249 f.write('{}')
250 f.close()
252 def CreateProfile(self):
253 profile = self.MakeProfileDirectory()
255 # Squelch warnings by creating bogus files.
256 self.MakeEmptyJSONFile(os.path.join(profile, 'Default', 'Preferences'))
257 self.MakeEmptyJSONFile(os.path.join(profile, 'Local State'))
259 return profile
261 def NetLogName(self):
262 return os.path.join(self.profile, 'netlog.json')
264 def MakeCmd(self, url, host, port):
265 cmd = [self.binary,
266 # --enable-logging enables stderr output from Chromium subprocesses
267 # on Windows (see
268 # https://code.google.com/p/chromium/issues/detail?id=171836)
269 '--enable-logging',
270 '--disable-web-resources',
271 '--disable-preconnect',
272 # This is speculative, sync should not occur with a clean profile.
273 '--disable-sync',
274 # This prevents Chrome from making "hidden" network requests at
275 # startup. These requests could be a source of non-determinism,
276 # and they also add noise to the netlogs.
277 '--dns-prefetch-disable',
278 '--no-first-run',
279 '--no-default-browser-check',
280 '--log-level=1',
281 '--safebrowsing-disable-auto-update',
282 '--disable-default-apps',
283 # Suppress metrics reporting. This prevents misconfigured bots,
284 # people testing at their desktop, etc from poisoning the UMA data.
285 '--metrics-recording-only',
286 # Chrome explicitly blacklists some ports as "unsafe" because
287 # certain protocols use them. Chrome gives an error like this:
288 # Error 312 (net::ERR_UNSAFE_PORT): Unknown error
289 # Unfortunately, the browser tester can randomly choose a
290 # blacklisted port. To work around this, the tester whitelists
291 # whatever port it is using.
292 '--explicitly-allowed-ports=%d' % port,
293 '--user-data-dir=%s' % self.profile]
294 # Log network requests to assist debugging.
295 cmd.append('--log-net-log=%s' % self.NetLogName())
296 if PLATFORM == 'linux':
297 # Explicitly run with mesa on linux. The test infrastructure doesn't have
298 # sufficient native GL contextes to run these tests.
299 cmd.append('--use-gl=osmesa')
300 if self.options.ppapi_plugin is None:
301 cmd.append('--enable-nacl')
302 disable_sandbox = False
303 # Chrome process can't access file within sandbox
304 disable_sandbox |= self.options.nacl_exe_stdin is not None
305 disable_sandbox |= self.options.nacl_exe_stdout is not None
306 disable_sandbox |= self.options.nacl_exe_stderr is not None
307 if disable_sandbox:
308 cmd.append('--no-sandbox')
309 else:
310 cmd.append('--register-pepper-plugins=%s;%s'
311 % (self.options.ppapi_plugin,
312 self.options.ppapi_plugin_mimetype))
313 cmd.append('--no-sandbox')
314 if self.options.browser_extensions:
315 cmd.append('--load-extension=%s' %
316 ','.join(self.options.browser_extensions))
317 cmd.append('--enable-experimental-extension-apis')
318 if self.options.enable_crash_reporter:
319 cmd.append('--enable-crash-reporter-for-testing')
320 if self.options.tool == 'memcheck':
321 cmd = ['src/third_party/valgrind/memcheck.sh',
322 '-v',
323 '--xml=yes',
324 '--leak-check=no',
325 '--gen-suppressions=all',
326 '--num-callers=30',
327 '--trace-children=yes',
328 '--nacl-file=%s' % (self.options.files[0],),
329 '--suppressions=' +
330 '../tools/valgrind/memcheck/suppressions.txt',
331 '--xml-file=%s/xml.%%p' % (self.tool_log_dir,),
332 '--log-file=%s/log.%%p' % (self.tool_log_dir,)] + cmd
333 elif self.options.tool == 'tsan':
334 cmd = ['src/third_party/valgrind/tsan.sh',
335 '-v',
336 '--num-callers=30',
337 '--trace-children=yes',
338 '--nacl-file=%s' % (self.options.files[0],),
339 '--ignore=../tools/valgrind/tsan/ignores.txt',
340 '--suppressions=../tools/valgrind/tsan/suppressions.txt',
341 '--log-file=%s/log.%%p' % (self.tool_log_dir,)] + cmd
342 elif self.options.tool != None:
343 raise LaunchFailure('Invalid tool name "%s"' % (self.options.tool,))
344 if self.options.enable_sockets:
345 cmd.append('--allow-nacl-socket-api=%s' % host)
346 cmd.extend(self.options.browser_flags)
347 cmd.append(url)
348 return cmd