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
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
)
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
50 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
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
65 import ctypes
, ctypes
.wintypes
, time
, msvcrt
69 def resetGlobalLog(log
):
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
)
87 class Automation(object):
89 Runs the browser from a script, and provides useful utilities
90 for setting up the browser environment.
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
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
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
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.
167 universal_newlines
=False,
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
,
175 universal_newlines
, startupinfo
, creationflags
)
179 if Automation().IS_WIN32
:
181 pid
= "%i" % self
.pid
182 subprocess
.Popen(["taskkill", "/F", "/PID", pid
]).wait()
184 os
.kill(self
.pid
, signal
.SIGKILL
)
186 def environment(self
, env
=None, xrePath
=None, crashreporter
=True, debugger
=False, dmdPath
=None, lsanPath
=None):
188 xrePath
= self
.DIST_BIN
190 env
= dict(os
.environ
)
192 ldLibraryPath
= os
.path
.abspath(os
.path
.join(SCRIPT_DIR
, xrePath
))
195 if self
.UNIXISH
or self
.IS_MAC
:
196 envVar
= "LD_LIBRARY_PATH"
197 preloadEnvVar
= "LD_PRELOAD"
199 envVar
= "DYLD_LIBRARY_PATH"
200 dmdLibrary
= "libdmd.dylib"
202 env
['MOZILLA_FIVE_HOME'] = xrePath
203 dmdLibrary
= "libdmd.so"
205 ldLibraryPath
= ldLibraryPath
+ ":" + env
[envVar
]
206 env
[envVar
] = ldLibraryPath
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'
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
):
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
)
244 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | Failed to find ASan symbolizer at %s", llvmsym
)
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"
256 self
.log
.info("INFO | automation.py | ASan running in default memory configuration")
258 self
.log
.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err
.strerror
)
260 self
.log
.info("Failed determine available memory, disabling ASan low-memory configuration")
264 def killPid(self
, pid
):
266 os
.kill(pid
, getattr(signal
, "SIGKILL", signal
.SIGTERM
))
268 self
.log
.info("Failed to kill process %d." % pid
)
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.
286 x
= msvcrt
.get_osfhandle(f
.fileno())
288 done
= time
.time() + timeout
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
297 self
.log
.error("readWithTimeout got error: %d", err
)
298 # read a character at a time, checking for eol. Return once we get there.
300 while index
< l
.value
:
304 return (buffer, False)
307 return (buffer, True)
309 def isPidAlive(self
, pid
):
311 PROCESS_QUERY_LIMITED_INFORMATION
= 0x1000
312 pHandle
= ctypes
.windll
.kernel32
.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION
, 0, pid
)
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
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
)
330 return (f
.readline(), False)
332 def isPidAlive(self
, pid
):
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.
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
)
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
:
349 def dumpScreen(self
, utilityPath
):
350 if self
.haveDumpedScreen
:
351 self
.log
.info("Not taking screenshot here: see the one that was previously logged")
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."""
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
)
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
)
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
388 if proc
.stdout
is None:
389 self
.log
.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
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
)
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
)
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
)
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"))
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.
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
)
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
)
452 printstatus("Main app process", status
)
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
)
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.
471 args
.extend(debuggerInfo
.args
)
473 cmd
= os
.path
.abspath(debuggerInfo
.path
)
476 args
.append("-foreground")
479 profileDirectory
= commands
.getoutput("cygpath -w \"" + profileDir
+ "/\"")
481 profileDirectory
= profileDir
+ "/"
483 args
.extend(("-no-remote", "-profile", profileDirectory
))
484 if testURL
is not None:
485 args
.append((testURL
))
486 args
.extend(extraArgs
)
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
497 self
.log
.info('INFO | zombiecheck | Reading PID log: %s', processLog
)
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
)
505 processList
.append(int(m
.group(1)))
508 for processPID
in processList
:
509 self
.log
.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID
)
510 if self
.isPidAlive(processPID
):
512 self
.log
.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID
)
513 self
.killAndGetStack(processPID
, utilityPath
, debuggerInfo
)
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
533 xrePath
= self
.DIST_BIN
535 certPath
= self
.CERTS_SRC_DIR
537 timeout
= self
.DEFAULT_TIMEOUT
539 # copy env so we don't munge the caller's environment
541 env
["NO_EM_RESTART"] = "1"
542 tmpfd
, processLog
= tempfile
.mkstemp(suffix
='pidlog')
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.
556 signal
.signal(signal
.SIGINT
, lambda sigid
, frame
: None)
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
),
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
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
:
585 if os
.path
.exists(processLog
):
586 os
.unlink(processLog
)
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