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
21 from datetime
import datetime
, timedelta
22 from string
import Template
24 SCRIPT_DIR
= os
.path
.abspath(os
.path
.realpath(os
.path
.dirname(sys
.argv
[0])))
25 sys
.path
.insert(0, SCRIPT_DIR
)
26 import automationutils
28 # --------------------------------------------------------------
29 # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900
30 # These paths refer to relative locations to test.zip, not the OBJDIR or SRCDIR
31 here
= os
.path
.dirname(os
.path
.realpath(__file__
))
32 mozbase
= os
.path
.realpath(os
.path
.join(os
.path
.dirname(here
), 'mozbase'))
34 if os
.path
.isdir(mozbase
):
35 for package
in os
.listdir(mozbase
):
36 package_path
= os
.path
.join(mozbase
, package
)
37 if package_path
not in sys
.path
:
38 sys
.path
.append(package_path
)
41 from mozprofile
import Profile
, Preferences
42 from mozprofile
.permissions
import ServerLocations
44 # ---------------------------------------------------------------
46 _DEFAULT_PREFERENCE_FILE
= os
.path
.join(SCRIPT_DIR
, 'prefs_general.js')
47 _DEFAULT_APPS_FILE
= os
.path
.join(SCRIPT_DIR
, 'webapps_mochitest.json')
49 _DEFAULT_WEB_SERVER
= "127.0.0.1"
50 _DEFAULT_HTTP_PORT
= 8888
51 _DEFAULT_SSL_PORT
= 4443
52 _DEFAULT_WEBSOCKET_PORT
= 9988
54 # from nsIPrincipal.idl
55 _APP_STATUS_NOT_INSTALLED
= 0
56 _APP_STATUS_INSTALLED
= 1
57 _APP_STATUS_PRIVILEGED
= 2
58 _APP_STATUS_CERTIFIED
= 3
60 #expand _DIST_BIN = __XPC_BIN_PATH__
61 #expand _IS_WIN32 = len("__WIN32__") != 0
62 #expand _IS_MAC = __IS_MAC__ != 0
63 #expand _IS_LINUX = __IS_LINUX__ != 0
65 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
69 #expand _IS_CAMINO = __IS_CAMINO__ != 0
70 #expand _BIN_SUFFIX = __BIN_SUFFIX__
71 #expand _PERL = __PERL__
73 #expand _DEFAULT_APP = "./" + __BROWSER_PATH__
74 #expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
75 #expand _IS_TEST_BUILD = __IS_TEST_BUILD__
76 #expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
77 #expand _CRASHREPORTER = __CRASHREPORTER__ == 1
78 #expand _IS_ASAN = __IS_ASAN__ == 1
82 import ctypes
, ctypes
.wintypes
, time
, msvcrt
90 def resetGlobalLog(log
):
92 _log
.removeHandler(_log
.handlers
[0])
93 handler
= logging
.StreamHandler(log
)
94 _log
.setLevel(logging
.INFO
)
95 _log
.addHandler(handler
)
97 # We use the logging system here primarily because it'll handle multiple
98 # threads, which is needed to process the output of the server and application
99 # processes simultaneously.
100 _log
= logging
.getLogger()
101 resetGlobalLog(sys
.stdout
)
108 class SyntaxError(Exception):
109 "Signifies a syntax error on a particular line in server-locations.txt."
111 def __init__(self
, lineno
, msg
= None):
116 s
= "Syntax error on line " + str(self
.lineno
)
118 s
+= ": %s." % self
.msg
125 "Represents a location line in server-locations.txt."
127 def __init__(self
, scheme
, host
, port
, options
):
131 self
.options
= options
133 class Automation(object):
135 Runs the browser from a script, and provides useful utilities
136 for setting up the browser environment.
143 IS_CYGWIN
= _IS_CYGWIN
144 IS_CAMINO
= _IS_CAMINO
145 BIN_SUFFIX
= _BIN_SUFFIX
148 UNIXISH
= not IS_WIN32
and not IS_MAC
150 DEFAULT_APP
= _DEFAULT_APP
151 CERTS_SRC_DIR
= _CERTS_SRC_DIR
152 IS_TEST_BUILD
= _IS_TEST_BUILD
153 IS_DEBUG_BUILD
= _IS_DEBUG_BUILD
154 CRASHREPORTER
= _CRASHREPORTER
157 # timeout, in seconds
158 DEFAULT_TIMEOUT
= 60.0
159 DEFAULT_WEB_SERVER
= _DEFAULT_WEB_SERVER
160 DEFAULT_HTTP_PORT
= _DEFAULT_HTTP_PORT
161 DEFAULT_SSL_PORT
= _DEFAULT_SSL_PORT
162 DEFAULT_WEBSOCKET_PORT
= _DEFAULT_WEBSOCKET_PORT
166 self
.lastTestSeen
= "automation.py"
167 self
.haveDumpedScreen
= False
169 def setServerInfo(self
,
170 webServer
= _DEFAULT_WEB_SERVER
,
171 httpPort
= _DEFAULT_HTTP_PORT
,
172 sslPort
= _DEFAULT_SSL_PORT
,
173 webSocketPort
= _DEFAULT_WEBSOCKET_PORT
):
174 self
.webServer
= webServer
175 self
.httpPort
= httpPort
176 self
.sslPort
= sslPort
177 self
.webSocketPort
= webSocketPort
199 class Process(subprocess
.Popen
):
201 Represents our view of a subprocess.
202 It adds a kill() method which allows it to be stopped explicitly.
217 universal_newlines
=False,
220 _log
.info("INFO | automation.py | Launching: %s", subprocess
.list2cmdline(args
))
221 subprocess
.Popen
.__init
__(self
, args
, bufsize
, executable
,
222 stdin
, stdout
, stderr
,
223 preexec_fn
, close_fds
,
225 universal_newlines
, startupinfo
, creationflags
)
229 if Automation().IS_WIN32
:
231 pid
= "%i" % self
.pid
232 if platform
.release() == "2000":
233 # Windows 2000 needs 'kill.exe' from the
234 #'Windows 2000 Resource Kit tools'. (See bug 475455.)
236 subprocess
.Popen(["kill", "-f", pid
]).wait()
238 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid
)
240 # Windows XP and later.
241 subprocess
.Popen(["taskkill", "/F", "/PID", pid
]).wait()
243 os
.kill(self
.pid
, signal
.SIGKILL
)
245 def readLocations(self
, locationsPath
= "server-locations.txt"):
247 Reads the locations at which the Mochitest HTTP server is available from
248 server-locations.txt.
251 locationFile
= codecs
.open(locationsPath
, "r", "UTF-8")
253 # Perhaps more detail than necessary, but it's the easiest way to make sure
254 # we get exactly the format we want. See server-locations.txt for the exact
255 # format guaranteed here.
256 lineRe
= re
.compile(r
"^(?P<scheme>[a-z][-a-z0-9+.]*)"
259 r
"\d+\.\d+\.\d+\.\d+"
261 r
"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
262 r
"[a-z](?:[-a-z0-9]*[a-z0-9])?"
268 r
"(?P<options>\S+(?:,\S+)*)"
273 for line
in locationFile
:
275 if line
.startswith("#") or line
== "\n":
278 match
= lineRe
.match(line
)
280 raise SyntaxError(lineno
)
282 options
= match
.group("options")
284 options
= options
.split(",")
285 if "primary" in options
:
287 raise SyntaxError(lineno
, "multiple primary locations")
292 locations
.append(Location(match
.group("scheme"), match
.group("host"),
293 match
.group("port"), options
))
296 raise SyntaxError(lineno
+ 1, "missing primary location")
300 def setupPermissionsDatabase(self
, profileDir
, permissions
):
301 # Included for reftest compatibility;
302 # see https://bugzilla.mozilla.org/show_bug.cgi?id=688667
304 # Open database and create table
305 permDB
= sqlite3
.connect(os
.path
.join(profileDir
, "permissions.sqlite"))
306 cursor
= permDB
.cursor();
308 cursor
.execute("PRAGMA user_version=3");
310 # SQL copied from nsPermissionManager.cpp
311 cursor
.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
312 id INTEGER PRIMARY KEY,
319 isInBrowserElement INTEGER)""")
321 # Insert desired permissions
322 for perm
in permissions
.keys():
323 for host
,allow
in permissions
[perm
]:
324 cursor
.execute("INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)",
325 (host
, perm
, 1 if allow
else 2))
331 def initializeProfile(self
, profileDir
,
333 useServerLocations
=False,
334 prefsPath
=_DEFAULT_PREFERENCE_FILE
,
335 appsPath
=_DEFAULT_APPS_FILE
,
337 " Sets up the standard testing profile."
339 extraPrefs
= extraPrefs
or []
344 if useServerLocations
:
345 locations
= ServerLocations()
346 locations
.read(os
.path
.abspath('server-locations.txt'), True)
348 prefs
['network.proxy.type'] = 0
350 prefs
.update(Preferences
.read_prefs(prefsPath
))
353 thispref
= v
.split("=", 1)
354 if len(thispref
) < 2:
355 print "Error: syntax error in --setpref=" + v
357 prefs
[thispref
[0]] = thispref
[1]
360 interpolation
= {"server": "%s:%s" % (self
.webServer
, self
.httpPort
)}
361 prefs
= json
.loads(json
.dumps(prefs
) % interpolation
)
363 prefs
[pref
] = Preferences
.cast(prefs
[pref
])
367 if appsPath
and os
.path
.exists(appsPath
):
368 with
open(appsPath
, 'r') as apps_file
:
369 apps
= json
.load(apps_file
)
371 proxy
= {'remote': str(self
.webServer
),
372 'http': str(self
.httpPort
),
373 'https': str(self
.sslPort
),
374 # use SSL port for legacy compatibility; see
375 # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
376 # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
377 # 'ws': str(self.webSocketPort)
378 'ws': str(self
.sslPort
)
381 # return profile object
382 profile
= Profile(profile
=profileDir
,
391 def addCommonOptions(self
, parser
):
392 "Adds command-line options which are common to mochitest and reftest."
394 parser
.add_option("--setpref",
395 action
= "append", type = "string",
397 dest
= "extraPrefs", metavar
= "PREF=VALUE",
398 help = "defines an extra user preference")
400 def fillCertificateDB(self
, profileDir
, certPath
, utilityPath
, xrePath
):
401 pwfilePath
= os
.path
.join(profileDir
, ".crtdbpw")
402 pwfile
= open(pwfilePath
, "w")
406 # Create head of the ssltunnel configuration file
407 sslTunnelConfigPath
= os
.path
.join(profileDir
, "ssltunnel.cfg")
408 sslTunnelConfig
= open(sslTunnelConfigPath
, "w")
410 sslTunnelConfig
.write("httpproxy:1\n")
411 sslTunnelConfig
.write("certdbdir:%s\n" % certPath
)
412 sslTunnelConfig
.write("forward:127.0.0.1:%s\n" % self
.httpPort
)
413 sslTunnelConfig
.write("websocketserver:%s:%s\n" % (self
.webServer
, self
.webSocketPort
))
414 sslTunnelConfig
.write("listen:*:%s:pgo server certificate\n" % self
.sslPort
)
416 # Configure automatic certificate and bind custom certificates, client authentication
417 locations
= self
.readLocations()
419 for loc
in locations
:
420 if loc
.scheme
== "https" and "nocert" not in loc
.options
:
421 customCertRE
= re
.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
422 clientAuthRE
= re
.compile("^clientauth=(?P<clientauth>[a-z]+)")
423 redirRE
= re
.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
424 for option
in loc
.options
:
425 match
= customCertRE
.match(option
)
427 customcert
= match
.group("nickname");
428 sslTunnelConfig
.write("listen:%s:%s:%s:%s\n" %
429 (loc
.host
, loc
.port
, self
.sslPort
, customcert
))
431 match
= clientAuthRE
.match(option
)
433 clientauth
= match
.group("clientauth");
434 sslTunnelConfig
.write("clientauth:%s:%s:%s:%s\n" %
435 (loc
.host
, loc
.port
, self
.sslPort
, clientauth
))
437 match
= redirRE
.match(option
)
439 redirhost
= match
.group("redirhost")
440 sslTunnelConfig
.write("redirhost:%s:%s:%s:%s\n" %
441 (loc
.host
, loc
.port
, self
.sslPort
, redirhost
))
443 sslTunnelConfig
.close()
445 # Pre-create the certification database for the profile
446 env
= self
.environment(xrePath
= xrePath
)
447 certutil
= os
.path
.join(utilityPath
, "certutil" + self
.BIN_SUFFIX
)
448 pk12util
= os
.path
.join(utilityPath
, "pk12util" + self
.BIN_SUFFIX
)
450 status
= self
.Process([certutil
, "-N", "-d", profileDir
, "-f", pwfilePath
], env
= env
).wait()
451 automationutils
.printstatus(status
, "certutil")
455 # Walk the cert directory and add custom CAs and client certs
456 files
= os
.listdir(certPath
)
458 root
, ext
= os
.path
.splitext(item
)
461 if root
.endswith("-object"):
463 status
= self
.Process([certutil
, "-A", "-i", os
.path
.join(certPath
, item
),
464 "-d", profileDir
, "-f", pwfilePath
, "-n", root
, "-t", trustBits
],
466 automationutils
.printstatus(status
, "certutil")
468 status
= self
.Process([pk12util
, "-i", os
.path
.join(certPath
, item
), "-w",
469 pwfilePath
, "-d", profileDir
],
471 automationutils
.printstatus(status
, "pk12util")
473 os
.unlink(pwfilePath
)
476 def environment(self
, env
=None, xrePath
=None, crashreporter
=True, debugger
=False, dmdPath
=None, lsanPath
=None):
478 xrePath
= self
.DIST_BIN
480 env
= dict(os
.environ
)
482 ldLibraryPath
= os
.path
.abspath(os
.path
.join(SCRIPT_DIR
, xrePath
))
485 if self
.UNIXISH
or self
.IS_MAC
:
486 envVar
= "LD_LIBRARY_PATH"
487 preloadEnvVar
= "LD_PRELOAD"
489 envVar
= "DYLD_LIBRARY_PATH"
490 dmdLibrary
= "libdmd.dylib"
492 env
['MOZILLA_FIVE_HOME'] = xrePath
493 dmdLibrary
= "libdmd.so"
495 ldLibraryPath
= ldLibraryPath
+ ":" + env
[envVar
]
496 env
[envVar
] = ldLibraryPath
498 env
["PATH"] = env
["PATH"] + ";" + str(ldLibraryPath
)
499 dmdLibrary
= "dmd.dll"
500 preloadEnvVar
= "MOZ_REPLACE_MALLOC_LIB"
502 if dmdPath
and dmdLibrary
and preloadEnvVar
:
504 env
[preloadEnvVar
] = os
.path
.join(dmdPath
, dmdLibrary
)
506 if crashreporter
and not debugger
:
507 env
['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
508 env
['MOZ_CRASHREPORTER'] = '1'
510 env
['MOZ_CRASHREPORTER_DISABLE'] = '1'
512 # Crash on non-local network connections.
513 env
['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
515 env
['GNOME_DISABLE_CRASH_DIALOG'] = '1'
516 env
['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
517 env
['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
519 # Set WebRTC logging in case it is not set yet
520 env
.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:5,datachannel:5')
521 env
.setdefault('R_LOG_LEVEL', '6')
522 env
.setdefault('R_LOG_DESTINATION', 'stderr')
523 env
.setdefault('R_LOG_VERBOSE', '1')
525 # ASan specific environment stuff
526 if self
.IS_ASAN
and (self
.IS_LINUX
or self
.IS_MAC
):
528 llvmsym
= os
.path
.join(xrePath
, "llvm-symbolizer")
529 if os
.path
.isfile(llvmsym
):
530 env
["ASAN_SYMBOLIZER_PATH"] = llvmsym
531 self
.log
.info("INFO | automation.py | ASan using symbolizer at %s", llvmsym
)
533 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | Failed to find ASan symbolizer at %s", llvmsym
)
536 totalMemory
= int(os
.popen("free").readlines()[1].split()[1])
538 # Only 4 GB RAM or less available? Use custom ASan options to reduce
539 # the amount of resources required to do the tests. Standard options
540 # will otherwise lead to OOM conditions on the current test slaves.
541 if totalMemory
<= 1024 * 1024 * 4:
542 self
.log
.info("INFO | automation.py | ASan running in low-memory configuration")
543 env
["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5"
545 self
.log
.info("INFO | automation.py | ASan running in default memory configuration")
547 self
.log
.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err
.strerror
)
549 self
.log
.info("Failed determine available memory, disabling ASan low-memory configuration")
553 def killPid(self
, pid
):
555 os
.kill(pid
, getattr(signal
, "SIGKILL", signal
.SIGTERM
))
557 self
.log
.info("Failed to kill process %d." % pid
)
560 PeekNamedPipe
= ctypes
.windll
.kernel32
.PeekNamedPipe
561 GetLastError
= ctypes
.windll
.kernel32
.GetLastError
563 def readWithTimeout(self
, f
, timeout
):
565 Try to read a line of output from the file object |f|. |f| must be a
566 pipe, like the |stdout| member of a subprocess.Popen object created
567 with stdout=PIPE. Returns a tuple (line, did_timeout), where |did_timeout|
568 is True if the read timed out, and False otherwise. If no output is
569 received within |timeout| seconds, returns a blank line.
575 x
= msvcrt
.get_osfhandle(f
.fileno())
577 done
= time
.time() + timeout
580 while timeout
== 0 or time
.time() < done
:
581 if self
.PeekNamedPipe(x
, None, 0, None, ctypes
.byref(l
), None) == 0:
582 err
= self
.GetLastError()
583 if err
== 38 or err
== 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
586 self
.log
.error("readWithTimeout got error: %d", err
)
587 # read a character at a time, checking for eol. Return once we get there.
589 while index
< l
.value
:
593 return (buffer, False)
596 return (buffer, True)
598 def isPidAlive(self
, pid
):
600 PROCESS_QUERY_LIMITED_INFORMATION
= 0x1000
601 pHandle
= ctypes
.windll
.kernel32
.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION
, 0, pid
)
604 pExitCode
= ctypes
.wintypes
.DWORD()
605 ctypes
.windll
.kernel32
.GetExitCodeProcess(pHandle
, ctypes
.byref(pExitCode
))
606 ctypes
.windll
.kernel32
.CloseHandle(pHandle
)
607 return pExitCode
.value
== STILL_ACTIVE
611 def readWithTimeout(self
, f
, timeout
):
612 """Try to read a line of output from the file object |f|. If no output
613 is received within |timeout| seconds, return a blank line.
614 Returns a tuple (line, did_timeout), where |did_timeout| is True
615 if the read timed out, and False otherwise."""
616 (r
, w
, e
) = select
.select([f
], [], [], timeout
)
619 return (f
.readline(), False)
621 def isPidAlive(self
, pid
):
623 # kill(pid, 0) checks for a valid PID without actually sending a signal
624 # The method throws OSError if the PID is invalid, which we catch below.
627 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
628 # the process terminates before we get to this point.
629 wpid
, wstatus
= os
.waitpid(pid
, os
.WNOHANG
)
632 # Catch the errors we might expect from os.kill/os.waitpid,
633 # and re-raise any others
634 if err
.errno
== errno
.ESRCH
or err
.errno
== errno
.ECHILD
:
638 def dumpScreen(self
, utilityPath
):
639 if self
.haveDumpedScreen
:
640 self
.log
.info("Not taking screenshot here: see the one that was previously logged")
643 self
.haveDumpedScreen
= True;
644 automationutils
.dumpScreen(utilityPath
)
647 def killAndGetStack(self
, processPID
, utilityPath
, debuggerInfo
):
648 """Kill the process, preferrably in a way that gets us a stack trace.
649 Also attempts to obtain a screenshot before killing the process."""
651 self
.dumpScreen(utilityPath
)
652 self
.killAndGetStackNoScreenshot(processPID
, utilityPath
, debuggerInfo
)
654 def killAndGetStackNoScreenshot(self
, processPID
, utilityPath
, debuggerInfo
):
655 """Kill the process, preferrably in a way that gets us a stack trace."""
656 if self
.CRASHREPORTER
and not debuggerInfo
:
657 if not self
.IS_WIN32
:
658 # ABRT will get picked up by Breakpad's signal handler
659 os
.kill(processPID
, signal
.SIGABRT
)
662 # We should have a "crashinject" program in our utility path
663 crashinject
= os
.path
.normpath(os
.path
.join(utilityPath
, "crashinject.exe"))
664 if os
.path
.exists(crashinject
):
665 status
= subprocess
.Popen([crashinject
, str(processPID
)]).wait()
666 automationutils
.printstatus(status
, "crashinject")
669 self
.log
.info("Can't trigger Breakpad, just killing process")
670 self
.killPid(processPID
)
672 def waitForFinish(self
, proc
, utilityPath
, timeout
, maxTime
, startTime
, debuggerInfo
, symbolsPath
):
673 """ Look for timeout or crashes and return the status after the process terminates """
674 stackFixerProcess
= None
675 stackFixerFunction
= None
678 if proc
.stdout
is None:
679 self
.log
.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
681 logsource
= proc
.stdout
683 if self
.IS_DEBUG_BUILD
and symbolsPath
and os
.path
.exists(symbolsPath
):
684 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
685 # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
686 sys
.path
.insert(0, utilityPath
)
687 import fix_stack_using_bpsyms
as stackFixerModule
688 stackFixerFunction
= lambda line
: stackFixerModule
.fixSymbols(line
, symbolsPath
)
690 elif self
.IS_DEBUG_BUILD
and self
.IS_MAC
and False:
691 # Run each line through a function in fix_macosx_stack.py (uses atos)
692 sys
.path
.insert(0, utilityPath
)
693 import fix_macosx_stack
as stackFixerModule
694 stackFixerFunction
= lambda line
: stackFixerModule
.fixSymbols(line
)
696 elif self
.IS_DEBUG_BUILD
and self
.IS_LINUX
:
697 # Run logsource through fix-linux-stack.pl (uses addr2line)
698 # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
699 stackFixerProcess
= self
.Process([self
.PERL
, os
.path
.join(utilityPath
, "fix-linux-stack.pl")],
701 stdout
=subprocess
.PIPE
)
702 logsource
= stackFixerProcess
.stdout
704 # With metro browser runs this script launches the metro test harness which launches the browser.
705 # The metro test harness hands back the real browser process id via log output which we need to
706 # pick up on and parse out. This variable tracks the real browser process id if we find it.
707 browserProcessId
= -1
709 (line
, didTimeout
) = self
.readWithTimeout(logsource
, timeout
)
710 while line
!= "" and not didTimeout
:
711 if stackFixerFunction
:
712 line
= stackFixerFunction(line
)
713 self
.log
.info(line
.rstrip().decode("UTF-8", "ignore"))
714 if "TEST-START" in line
and "|" in line
:
715 self
.lastTestSeen
= line
.split("|")[1].strip()
716 if not debuggerInfo
and "TEST-UNEXPECTED-FAIL" in line
and "Test timed out" in line
:
717 self
.dumpScreen(utilityPath
)
719 (line
, didTimeout
) = self
.readWithTimeout(logsource
, timeout
)
721 if "METRO_BROWSER_PROCESS" in line
:
722 index
= line
.find("=")
724 browserProcessId
= line
[index
+1:].rstrip()
725 self
.log
.info("INFO | automation.py | metro browser sub process id detected: %s", browserProcessId
)
727 if not hitMaxTime
and maxTime
and datetime
.now() - startTime
> timedelta(seconds
= maxTime
):
728 # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
730 self
.log
.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self
.lastTestSeen
, int(maxTime
))
731 self
.killAndGetStack(proc
.pid
, utilityPath
, debuggerInfo
)
734 self
.log
.info(line
.rstrip().decode("UTF-8", "ignore"))
735 self
.log
.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self
.lastTestSeen
, int(timeout
))
736 if browserProcessId
== -1:
737 browserProcessId
= proc
.pid
738 self
.killAndGetStack(browserProcessId
, utilityPath
, debuggerInfo
)
741 automationutils
.printstatus(status
, "Main app process")
743 self
.lastTestSeen
= "Main app process exited normally"
744 if status
!= 0 and not didTimeout
and not hitMaxTime
:
745 self
.log
.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self
.lastTestSeen
, status
)
746 if stackFixerProcess
is not None:
747 fixerStatus
= stackFixerProcess
.wait()
748 automationutils
.printstatus(status
, "stackFixerProcess")
749 if fixerStatus
!= 0 and not didTimeout
and not hitMaxTime
:
750 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus
)
753 def buildCommandLine(self
, app
, debuggerInfo
, profileDir
, testURL
, extraArgs
):
754 """ build the application command line """
756 cmd
= os
.path
.abspath(app
)
757 if self
.IS_MAC
and not self
.IS_CAMINO
and os
.path
.exists(cmd
+ "-bin"):
758 # Prefer 'app-bin' in case 'app' is a shell script.
759 # We can remove this hack once bug 673899 etc are fixed.
765 args
.extend(debuggerInfo
["args"])
767 cmd
= os
.path
.abspath(debuggerInfo
["path"])
770 args
.append("-foreground")
773 profileDirectory
= commands
.getoutput("cygpath -w \"" + profileDir
+ "/\"")
775 profileDirectory
= profileDir
+ "/"
777 args
.extend(("-no-remote", "-profile", profileDirectory
))
778 if testURL
is not None:
780 args
.extend(("-url", testURL
))
782 args
.append((testURL
))
783 args
.extend(extraArgs
)
786 def checkForZombies(self
, processLog
, utilityPath
, debuggerInfo
):
787 """ Look for hung processes """
788 if not os
.path
.exists(processLog
):
789 self
.log
.info('Automation Error: PID log not found: %s', processLog
)
790 # Whilst no hung process was found, the run should still display as a failure
794 self
.log
.info('INFO | zombiecheck | Reading PID log: %s', processLog
)
796 pidRE
= re
.compile(r
'launched child process (\d+)$')
797 processLogFD
= open(processLog
)
798 for line
in processLogFD
:
799 self
.log
.info(line
.rstrip())
800 m
= pidRE
.search(line
)
802 processList
.append(int(m
.group(1)))
805 for processPID
in processList
:
806 self
.log
.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID
)
807 if self
.isPidAlive(processPID
):
809 self
.log
.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID
)
810 self
.killAndGetStack(processPID
, utilityPath
, debuggerInfo
)
813 def checkForCrashes(self
, minidumpDir
, symbolsPath
):
814 return mozcrash
.check_for_crashes(minidumpDir
, symbolsPath
, test_name
=self
.lastTestSeen
)
816 def runApp(self
, testURL
, env
, app
, profileDir
, extraArgs
,
817 runSSLTunnel
= False, utilityPath
= None,
818 xrePath
= None, certPath
= None,
819 debuggerInfo
= None, symbolsPath
= None,
820 timeout
= -1, maxTime
= None, onLaunch
= None,
821 detectShutdownLeaks
= False, screenshotOnFail
=False, testPath
=None, bisectChunk
=None):
823 Run the app, log the duration it took to execute, return the status code.
824 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
827 if utilityPath
== None:
828 utilityPath
= self
.DIST_BIN
830 xrePath
= self
.DIST_BIN
832 certPath
= self
.CERTS_SRC_DIR
834 timeout
= self
.DEFAULT_TIMEOUT
836 # copy env so we don't munge the caller's environment
838 env
["NO_EM_RESTART"] = "1"
839 tmpfd
, processLog
= tempfile
.mkstemp(suffix
='pidlog')
841 env
["MOZ_PROCESS_LOG"] = processLog
843 if self
.IS_TEST_BUILD
and runSSLTunnel
:
844 # create certificate database for the profile
845 certificateStatus
= self
.fillCertificateDB(profileDir
, certPath
, utilityPath
, xrePath
)
846 if certificateStatus
!= 0:
847 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | Certificate integration failed")
848 return certificateStatus
850 # start ssltunnel to provide https:// URLs capability
851 ssltunnel
= os
.path
.join(utilityPath
, "ssltunnel" + self
.BIN_SUFFIX
)
852 ssltunnelProcess
= self
.Process([ssltunnel
,
853 os
.path
.join(profileDir
, "ssltunnel.cfg")],
854 env
= self
.environment(xrePath
= xrePath
))
855 self
.log
.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess
.pid
)
857 cmd
, args
= self
.buildCommandLine(app
, debuggerInfo
, profileDir
, testURL
, extraArgs
)
858 startTime
= datetime
.now()
860 if debuggerInfo
and debuggerInfo
["interactive"]:
861 # If an interactive debugger is attached, don't redirect output,
862 # don't use timeouts, and don't capture ctrl-c.
866 signal
.signal(signal
.SIGINT
, lambda sigid
, frame
: None)
868 outputPipe
= subprocess
.PIPE
870 self
.lastTestSeen
= "automation.py"
871 proc
= self
.Process([cmd
] + args
,
872 env
= self
.environment(env
, xrePath
= xrePath
,
873 crashreporter
= not debuggerInfo
),
875 stderr
= subprocess
.STDOUT
)
876 self
.log
.info("INFO | automation.py | Application pid: %d", proc
.pid
)
878 if onLaunch
is not None:
879 # Allow callers to specify an onLaunch callback to be fired after the
883 status
= self
.waitForFinish(proc
, utilityPath
, timeout
, maxTime
, startTime
, debuggerInfo
, symbolsPath
)
884 self
.log
.info("INFO | automation.py | Application ran for: %s", str(datetime
.now() - startTime
))
886 # Do a final check for zombie child processes.
887 zombieProcesses
= self
.checkForZombies(processLog
, utilityPath
, debuggerInfo
)
889 crashed
= self
.checkForCrashes(os
.path
.join(profileDir
, "minidumps"), symbolsPath
)
891 if crashed
or zombieProcesses
:
894 if os
.path
.exists(processLog
):
895 os
.unlink(processLog
)
897 if self
.IS_TEST_BUILD
and runSSLTunnel
:
898 ssltunnelProcess
.kill()
902 def getExtensionIDFromRDF(self
, rdfSource
):
904 Retrieves the extension id from an install.rdf file (or string).
906 from xml
.dom
.minidom
import parse
, parseString
, Node
908 if isinstance(rdfSource
, file):
909 document
= parse(rdfSource
)
911 document
= parseString(rdfSource
)
913 # Find the <em:id> element. There can be multiple <em:id> tags
914 # within <em:targetApplication> tags, so we have to check this way.
915 for rdfChild
in document
.documentElement
.childNodes
:
916 if rdfChild
.nodeType
== Node
.ELEMENT_NODE
and rdfChild
.tagName
== "Description":
917 for descChild
in rdfChild
.childNodes
:
918 if descChild
.nodeType
== Node
.ELEMENT_NODE
and descChild
.tagName
== "em:id":
919 return descChild
.childNodes
[0].data
923 def installExtension(self
, extensionSource
, profileDir
, extensionID
= None):
925 Copies an extension into the extensions directory of the given profile.
926 extensionSource - the source location of the extension files. This can be either
927 a directory or a path to an xpi file.
928 profileDir - the profile directory we are copying into. We will create the
929 "extensions" directory there if it doesn't exist.
930 extensionID - the id of the extension to be used as the containing directory for the
931 extension, if extensionSource is a directory, i.e.
932 this is the name of the folder in the <profileDir>/extensions/<extensionID>
934 if not os
.path
.isdir(profileDir
):
935 self
.log
.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir
)
938 installRDFFilename
= "install.rdf"
940 extensionsRootDir
= os
.path
.join(profileDir
, "extensions", "staged")
941 if not os
.path
.isdir(extensionsRootDir
):
942 os
.makedirs(extensionsRootDir
)
944 if os
.path
.isfile(extensionSource
):
945 reader
= automationutils
.ZipFileReader(extensionSource
)
947 for filename
in reader
.namelist():
948 # Sanity check the zip file.
949 if os
.path
.isabs(filename
):
950 self
.log
.info("INFO | automation.py | Cannot install extension, bad files in xpi")
953 # We may need to dig the extensionID out of the zip file...
954 if extensionID
is None and filename
== installRDFFilename
:
955 extensionID
= self
.getExtensionIDFromRDF(reader
.read(filename
))
957 # We must know the extensionID now.
958 if extensionID
is None:
959 self
.log
.info("INFO | automation.py | Cannot install extension, missing extensionID")
962 # Make the extension directory.
963 extensionDir
= os
.path
.join(extensionsRootDir
, extensionID
)
964 os
.mkdir(extensionDir
)
967 reader
.extractall(extensionDir
)
969 elif os
.path
.isdir(extensionSource
):
970 if extensionID
is None:
971 filename
= os
.path
.join(extensionSource
, installRDFFilename
)
972 if os
.path
.isfile(filename
):
973 with
open(filename
, "r") as installRDF
:
974 extensionID
= self
.getExtensionIDFromRDF(installRDF
)
976 if extensionID
is None:
977 self
.log
.info("INFO | automation.py | Cannot install extension, missing extensionID")
980 # Copy extension tree into its own directory.
981 # "destination directory must not already exist".
982 shutil
.copytree(extensionSource
, os
.path
.join(extensionsRootDir
, extensionID
))
985 self
.log
.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource
)
987 def elf_arm(self
, filename
):
988 data
= open(filename
, 'rb').read(20)
989 return data
[:4] == "\x7fELF" and ord(data
[18]) == 40 # EM_ARM