Bug 1295072 - Focus urlbar after opening an empty new tab r=kmag
[gecko.git] / build / automation.py.in
blob234bc54fa334ec3cd7808a6f03041d6528f051e1
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 from __future__ import with_statement
7 import logging
8 import os
9 import re
10 import select
11 import signal
12 import subprocess
13 import sys
14 import tempfile
15 from datetime import datetime, timedelta
17 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
18 sys.path.insert(0, SCRIPT_DIR)
20 # --------------------------------------------------------------
21 # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900
22 # These paths refer to relative locations to test.zip, not the OBJDIR or SRCDIR
23 here = os.path.dirname(os.path.realpath(__file__))
24 mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase'))
26 if os.path.isdir(mozbase):
27 for package in os.listdir(mozbase):
28 package_path = os.path.join(mozbase, package)
29 if package_path not in sys.path:
30 sys.path.append(package_path)
32 import mozcrash
33 from mozscreenshot import printstatus, dump_screen
36 # ---------------------------------------------------------------
38 _DEFAULT_PREFERENCE_FILE = os.path.join(SCRIPT_DIR, 'prefs_general.js')
40 _DEFAULT_WEB_SERVER = "127.0.0.1"
41 _DEFAULT_HTTP_PORT = 8888
42 _DEFAULT_SSL_PORT = 4443
43 _DEFAULT_WEBSOCKET_PORT = 9988
45 #expand _DIST_BIN = __XPC_BIN_PATH__
46 #expand _IS_WIN32 = len("__WIN32__") != 0
47 #expand _IS_MAC = __IS_MAC__ != 0
48 #expand _IS_LINUX = __IS_LINUX__ != 0
49 #ifdef IS_CYGWIN
50 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
51 #else
52 _IS_CYGWIN = False
53 #endif
54 #expand _BIN_SUFFIX = __BIN_SUFFIX__
56 #expand _DEFAULT_APP = "./" + __BROWSER_PATH__
57 #expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
58 #expand _IS_TEST_BUILD = __IS_TEST_BUILD__
59 #expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
60 #expand _CRASHREPORTER = __CRASHREPORTER__ == 1
61 #expand _IS_ASAN = __IS_ASAN__ == 1
64 if _IS_WIN32:
65 import ctypes, ctypes.wintypes, time, msvcrt
66 else:
67 import errno
69 def resetGlobalLog(log):
70 while _log.handlers:
71 _log.removeHandler(_log.handlers[0])
72 handler = logging.StreamHandler(log)
73 _log.setLevel(logging.INFO)
74 _log.addHandler(handler)
76 # We use the logging system here primarily because it'll handle multiple
77 # threads, which is needed to process the output of the server and application
78 # processes simultaneously.
79 _log = logging.getLogger()
80 resetGlobalLog(sys.stdout)
83 #################
84 # PROFILE SETUP #
85 #################
87 class Automation(object):
88 """
89 Runs the browser from a script, and provides useful utilities
90 for setting up the browser environment.
91 """
93 DIST_BIN = _DIST_BIN
94 IS_WIN32 = _IS_WIN32
95 IS_MAC = _IS_MAC
96 IS_LINUX = _IS_LINUX
97 IS_CYGWIN = _IS_CYGWIN
98 BIN_SUFFIX = _BIN_SUFFIX
100 UNIXISH = not IS_WIN32 and not IS_MAC
102 DEFAULT_APP = _DEFAULT_APP
103 CERTS_SRC_DIR = _CERTS_SRC_DIR
104 IS_TEST_BUILD = _IS_TEST_BUILD
105 IS_DEBUG_BUILD = _IS_DEBUG_BUILD
106 CRASHREPORTER = _CRASHREPORTER
107 IS_ASAN = _IS_ASAN
109 # timeout, in seconds
110 DEFAULT_TIMEOUT = 60.0
111 DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
112 DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
113 DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
114 DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
116 def __init__(self):
117 self.log = _log
118 self.lastTestSeen = "automation.py"
119 self.haveDumpedScreen = False
121 def setServerInfo(self,
122 webServer = _DEFAULT_WEB_SERVER,
123 httpPort = _DEFAULT_HTTP_PORT,
124 sslPort = _DEFAULT_SSL_PORT,
125 webSocketPort = _DEFAULT_WEBSOCKET_PORT):
126 self.webServer = webServer
127 self.httpPort = httpPort
128 self.sslPort = sslPort
129 self.webSocketPort = webSocketPort
131 @property
132 def __all__(self):
133 return [
134 "UNIXISH",
135 "IS_WIN32",
136 "IS_MAC",
137 "log",
138 "runApp",
139 "Process",
140 "DIST_BIN",
141 "DEFAULT_APP",
142 "CERTS_SRC_DIR",
143 "environment",
144 "IS_TEST_BUILD",
145 "IS_DEBUG_BUILD",
146 "DEFAULT_TIMEOUT",
149 class Process(subprocess.Popen):
151 Represents our view of a subprocess.
152 It adds a kill() method which allows it to be stopped explicitly.
155 def __init__(self,
156 args,
157 bufsize=0,
158 executable=None,
159 stdin=None,
160 stdout=None,
161 stderr=None,
162 preexec_fn=None,
163 close_fds=False,
164 shell=False,
165 cwd=None,
166 env=None,
167 universal_newlines=False,
168 startupinfo=None,
169 creationflags=0):
170 _log.info("INFO | automation.py | Launching: %s", subprocess.list2cmdline(args))
171 subprocess.Popen.__init__(self, args, bufsize, executable,
172 stdin, stdout, stderr,
173 preexec_fn, close_fds,
174 shell, cwd, env,
175 universal_newlines, startupinfo, creationflags)
176 self.log = _log
178 def kill(self):
179 if Automation().IS_WIN32:
180 import platform
181 pid = "%i" % self.pid
182 subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
183 else:
184 os.kill(self.pid, signal.SIGKILL)
186 def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None, lsanPath=None):
187 if xrePath == None:
188 xrePath = self.DIST_BIN
189 if env == None:
190 env = dict(os.environ)
192 ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
193 dmdLibrary = None
194 preloadEnvVar = None
195 if self.UNIXISH or self.IS_MAC:
196 envVar = "LD_LIBRARY_PATH"
197 preloadEnvVar = "LD_PRELOAD"
198 if self.IS_MAC:
199 envVar = "DYLD_LIBRARY_PATH"
200 dmdLibrary = "libdmd.dylib"
201 else: # unixish
202 env['MOZILLA_FIVE_HOME'] = xrePath
203 dmdLibrary = "libdmd.so"
204 if envVar in env:
205 ldLibraryPath = ldLibraryPath + ":" + env[envVar]
206 env[envVar] = ldLibraryPath
207 elif self.IS_WIN32:
208 env["PATH"] = env["PATH"] + ";" + str(ldLibraryPath)
209 dmdLibrary = "dmd.dll"
210 preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB"
212 if dmdPath and dmdLibrary and preloadEnvVar:
213 env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary)
215 if crashreporter and not debugger:
216 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
217 env['MOZ_CRASHREPORTER'] = '1'
218 else:
219 env['MOZ_CRASHREPORTER_DISABLE'] = '1'
221 # Crash on non-local network connections by default.
222 # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
223 # enable non-local connections for the purposes of local testing. Don't
224 # override the user's choice here. See bug 1049688.
225 env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1')
227 env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
228 env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
230 # Set WebRTC logging in case it is not set yet
231 env.setdefault('MOZ_LOG', 'signaling:3,mtransport:4,DataChannel:4,jsep:4,MediaPipelineFactory:4')
232 env.setdefault('R_LOG_LEVEL', '6')
233 env.setdefault('R_LOG_DESTINATION', 'stderr')
234 env.setdefault('R_LOG_VERBOSE', '1')
236 # ASan specific environment stuff
237 if self.IS_ASAN and (self.IS_LINUX or self.IS_MAC):
238 # Symbolizer support
239 llvmsym = os.path.join(xrePath, "llvm-symbolizer")
240 if os.path.isfile(llvmsym):
241 env["ASAN_SYMBOLIZER_PATH"] = llvmsym
242 self.log.info("INFO | automation.py | ASan using symbolizer at %s", llvmsym)
243 else:
244 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Failed to find ASan symbolizer at %s", llvmsym)
246 try:
247 totalMemory = int(os.popen("free").readlines()[1].split()[1])
249 # Only 4 GB RAM or less available? Use custom ASan options to reduce
250 # the amount of resources required to do the tests. Standard options
251 # will otherwise lead to OOM conditions on the current test slaves.
252 if totalMemory <= 1024 * 1024 * 4:
253 self.log.info("INFO | automation.py | ASan running in low-memory configuration")
254 env["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5"
255 else:
256 self.log.info("INFO | automation.py | ASan running in default memory configuration")
257 except OSError,err:
258 self.log.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err.strerror)
259 except:
260 self.log.info("Failed determine available memory, disabling ASan low-memory configuration")
262 return env
264 def killPid(self, pid):
265 try:
266 os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
267 except WindowsError:
268 self.log.info("Failed to kill process %d." % pid)
270 if IS_WIN32:
271 PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
272 GetLastError = ctypes.windll.kernel32.GetLastError
274 def readWithTimeout(self, f, timeout):
276 Try to read a line of output from the file object |f|. |f| must be a
277 pipe, like the |stdout| member of a subprocess.Popen object created
278 with stdout=PIPE. Returns a tuple (line, did_timeout), where |did_timeout|
279 is True if the read timed out, and False otherwise. If no output is
280 received within |timeout| seconds, returns a blank line.
283 if timeout is None:
284 timeout = 0
286 x = msvcrt.get_osfhandle(f.fileno())
287 l = ctypes.c_long()
288 done = time.time() + timeout
290 buffer = ""
291 while timeout == 0 or time.time() < done:
292 if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
293 err = self.GetLastError()
294 if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
295 return ('', False)
296 else:
297 self.log.error("readWithTimeout got error: %d", err)
298 # read a character at a time, checking for eol. Return once we get there.
299 index = 0
300 while index < l.value:
301 char = f.read(1)
302 buffer += char
303 if char == '\n':
304 return (buffer, False)
305 index = index + 1
306 time.sleep(0.01)
307 return (buffer, True)
309 def isPidAlive(self, pid):
310 STILL_ACTIVE = 259
311 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
312 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
313 if not pHandle:
314 return False
315 pExitCode = ctypes.wintypes.DWORD()
316 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
317 ctypes.windll.kernel32.CloseHandle(pHandle)
318 return pExitCode.value == STILL_ACTIVE
320 else:
322 def readWithTimeout(self, f, timeout):
323 """Try to read a line of output from the file object |f|. If no output
324 is received within |timeout| seconds, return a blank line.
325 Returns a tuple (line, did_timeout), where |did_timeout| is True
326 if the read timed out, and False otherwise."""
327 (r, w, e) = select.select([f], [], [], timeout)
328 if len(r) == 0:
329 return ('', True)
330 return (f.readline(), False)
332 def isPidAlive(self, pid):
333 try:
334 # kill(pid, 0) checks for a valid PID without actually sending a signal
335 # The method throws OSError if the PID is invalid, which we catch below.
336 os.kill(pid, 0)
338 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
339 # the process terminates before we get to this point.
340 wpid, wstatus = os.waitpid(pid, os.WNOHANG)
341 return wpid == 0
342 except OSError, err:
343 # Catch the errors we might expect from os.kill/os.waitpid,
344 # and re-raise any others
345 if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
346 return False
347 raise
349 def dumpScreen(self, utilityPath):
350 if self.haveDumpedScreen:
351 self.log.info("Not taking screenshot here: see the one that was previously logged")
352 return
354 self.haveDumpedScreen = True;
355 dump_screen(utilityPath, self.log)
358 def killAndGetStack(self, processPID, utilityPath, debuggerInfo):
359 """Kill the process, preferrably in a way that gets us a stack trace.
360 Also attempts to obtain a screenshot before killing the process."""
361 if not debuggerInfo:
362 self.dumpScreen(utilityPath)
363 self.killAndGetStackNoScreenshot(processPID, utilityPath, debuggerInfo)
365 def killAndGetStackNoScreenshot(self, processPID, utilityPath, debuggerInfo):
366 """Kill the process, preferrably in a way that gets us a stack trace."""
367 if self.CRASHREPORTER and not debuggerInfo:
368 if not self.IS_WIN32:
369 # ABRT will get picked up by Breakpad's signal handler
370 os.kill(processPID, signal.SIGABRT)
371 return
372 else:
373 # We should have a "crashinject" program in our utility path
374 crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
375 if os.path.exists(crashinject):
376 status = subprocess.Popen([crashinject, str(processPID)]).wait()
377 printstatus("crashinject", status)
378 if status == 0:
379 return
380 self.log.info("Can't trigger Breakpad, just killing process")
381 self.killPid(processPID)
383 def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, outputHandler=None):
384 """ Look for timeout or crashes and return the status after the process terminates """
385 stackFixerFunction = None
386 didTimeout = False
387 hitMaxTime = False
388 if proc.stdout is None:
389 self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
390 else:
391 logsource = proc.stdout
393 if self.IS_DEBUG_BUILD and symbolsPath and os.path.exists(symbolsPath):
394 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
395 # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
396 sys.path.insert(0, utilityPath)
397 import fix_stack_using_bpsyms as stackFixerModule
398 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
399 del sys.path[0]
400 elif self.IS_DEBUG_BUILD and self.IS_MAC:
401 # Run each line through a function in fix_macosx_stack.py (uses atos)
402 sys.path.insert(0, utilityPath)
403 import fix_macosx_stack as stackFixerModule
404 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
405 del sys.path[0]
406 elif self.IS_DEBUG_BUILD and self.IS_LINUX:
407 # Run each line through a function in fix_linux_stack.py (uses addr2line)
408 # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
409 sys.path.insert(0, utilityPath)
410 import fix_linux_stack as stackFixerModule
411 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
412 del sys.path[0]
414 # With metro browser runs this script launches the metro test harness which launches the browser.
415 # The metro test harness hands back the real browser process id via log output which we need to
416 # pick up on and parse out. This variable tracks the real browser process id if we find it.
417 browserProcessId = -1
419 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
420 while line != "" and not didTimeout:
421 if stackFixerFunction:
422 line = stackFixerFunction(line)
424 if outputHandler is None:
425 self.log.info(line.rstrip().decode("UTF-8", "ignore"))
426 else:
427 outputHandler(line)
429 if "TEST-START" in line and "|" in line:
430 self.lastTestSeen = line.split("|")[1].strip()
431 if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
432 self.dumpScreen(utilityPath)
434 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
436 if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
437 # Kill the application.
438 hitMaxTime = True
439 self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
440 self.log.error("Force-terminating active process(es).");
441 self.killAndGetStack(proc.pid, utilityPath, debuggerInfo)
442 if didTimeout:
443 if line:
444 self.log.info(line.rstrip().decode("UTF-8", "ignore"))
445 self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
446 self.log.error("Force-terminating active process(es).");
447 if browserProcessId == -1:
448 browserProcessId = proc.pid
449 self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo)
451 status = proc.wait()
452 printstatus("Main app process", status)
453 if status == 0:
454 self.lastTestSeen = "Main app process exited normally"
455 if status != 0 and not didTimeout and not hitMaxTime:
456 self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
457 return status
459 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
460 """ build the application command line """
462 cmd = os.path.abspath(app)
463 if self.IS_MAC and os.path.exists(cmd + "-bin"):
464 # Prefer 'app-bin' in case 'app' is a shell script.
465 # We can remove this hack once bug 673899 etc are fixed.
466 cmd += "-bin"
468 args = []
470 if debuggerInfo:
471 args.extend(debuggerInfo.args)
472 args.append(cmd)
473 cmd = os.path.abspath(debuggerInfo.path)
475 if self.IS_MAC:
476 args.append("-foreground")
478 if self.IS_CYGWIN:
479 profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
480 else:
481 profileDirectory = profileDir + "/"
483 args.extend(("-no-remote", "-profile", profileDirectory))
484 if testURL is not None:
485 args.append((testURL))
486 args.extend(extraArgs)
487 return cmd, args
489 def checkForZombies(self, processLog, utilityPath, debuggerInfo):
490 """ Look for hung processes """
491 if not os.path.exists(processLog):
492 self.log.info('Automation Error: PID log not found: %s', processLog)
493 # Whilst no hung process was found, the run should still display as a failure
494 return True
496 foundZombie = False
497 self.log.info('INFO | zombiecheck | Reading PID log: %s', processLog)
498 processList = []
499 pidRE = re.compile(r'launched child process (\d+)$')
500 processLogFD = open(processLog)
501 for line in processLogFD:
502 self.log.info(line.rstrip())
503 m = pidRE.search(line)
504 if m:
505 processList.append(int(m.group(1)))
506 processLogFD.close()
508 for processPID in processList:
509 self.log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID)
510 if self.isPidAlive(processPID):
511 foundZombie = True
512 self.log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID)
513 self.killAndGetStack(processPID, utilityPath, debuggerInfo)
514 return foundZombie
516 def checkForCrashes(self, minidumpDir, symbolsPath):
517 return mozcrash.check_for_crashes(minidumpDir, symbolsPath, test_name=self.lastTestSeen)
519 def runApp(self, testURL, env, app, profileDir, extraArgs, utilityPath = None,
520 xrePath = None, certPath = None,
521 debuggerInfo = None, symbolsPath = None,
522 timeout = -1, maxTime = None, onLaunch = None,
523 detectShutdownLeaks = False, screenshotOnFail=False, testPath=None, bisectChunk=None,
524 valgrindPath=None, valgrindArgs=None, valgrindSuppFiles=None, outputHandler=None):
526 Run the app, log the duration it took to execute, return the status code.
527 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
530 if utilityPath == None:
531 utilityPath = self.DIST_BIN
532 if xrePath == None:
533 xrePath = self.DIST_BIN
534 if certPath == None:
535 certPath = self.CERTS_SRC_DIR
536 if timeout == -1:
537 timeout = self.DEFAULT_TIMEOUT
539 # copy env so we don't munge the caller's environment
540 env = dict(env);
541 env["NO_EM_RESTART"] = "1"
542 tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
543 os.close(tmpfd)
544 env["MOZ_PROCESS_LOG"] = processLog
547 cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
548 startTime = datetime.now()
550 if debuggerInfo and debuggerInfo.interactive:
551 # If an interactive debugger is attached, don't redirect output,
552 # don't use timeouts, and don't capture ctrl-c.
553 timeout = None
554 maxTime = None
555 outputPipe = None
556 signal.signal(signal.SIGINT, lambda sigid, frame: None)
557 else:
558 outputPipe = subprocess.PIPE
560 self.lastTestSeen = "automation.py"
561 proc = self.Process([cmd] + args,
562 env = self.environment(env, xrePath = xrePath,
563 crashreporter = not debuggerInfo),
564 stdout = outputPipe,
565 stderr = subprocess.STDOUT)
566 self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
568 if onLaunch is not None:
569 # Allow callers to specify an onLaunch callback to be fired after the
570 # app is launched.
571 onLaunch()
573 status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath,
574 outputHandler=outputHandler)
575 self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
577 # Do a final check for zombie child processes.
578 zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo)
580 crashed = self.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath)
582 if crashed or zombieProcesses:
583 status = 1
585 if os.path.exists(processLog):
586 os.unlink(processLog)
588 return status
590 def elf_arm(self, filename):
591 data = open(filename, 'rb').read(20)
592 return data[:4] == "\x7fELF" and ord(data[18]) == 40 # EM_ARM