2 # ***** BEGIN LICENSE BLOCK *****
3 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 # The contents of this file are subject to the Mozilla Public License Version
6 # 1.1 (the "License"); you may not use this file except in compliance with
7 # the License. You may obtain a copy of the License at
8 # http://www.mozilla.org/MPL/
10 # Software distributed under the License is distributed on an "AS IS" basis,
11 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 # for the specific language governing rights and limitations under the
15 # The Original Code is mozilla.org code.
17 # The Initial Developer of the Original Code is
19 # Portions created by the Initial Developer are Copyright (C) 2008
20 # the Initial Developer. All Rights Reserved.
23 # Robert Sayre <sayrer@gmail.com>
24 # Jeff Walden <jwalden+bmo@mit.edu>
26 # Alternatively, the contents of this file may be used under the terms of
27 # either the GNU General Public License Version 2 or later (the "GPL"), or
28 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29 # in which case the provisions of the GPL or the LGPL are applicable instead
30 # of those above. If you wish to allow use of your version of this file only
31 # under the terms of either the GPL or the LGPL, and not to allow others to
32 # use your version of this file under the terms of the MPL, indicate your
33 # decision by deleting the provisions above and replace them with the notice
34 # and other provisions required by the GPL or the LGPL. If you do not delete
35 # the provisions above, a recipient may use your version of this file under
36 # the terms of any one of the MPL, the GPL or the LGPL.
38 # ***** END LICENSE BLOCK *****
41 from datetime
import datetime
, timedelta
56 SCRIPT_DIR
= os
.path
.abspath(os
.path
.realpath(os
.path
.dirname(sys
.argv
[0])))
57 sys
.path
.insert(0, SCRIPT_DIR
)
58 import automationutils
60 _DEFAULT_WEB_SERVER
= "127.0.0.1"
61 _DEFAULT_HTTP_PORT
= 8888
62 _DEFAULT_SSL_PORT
= 4443
63 _DEFAULT_WEBSOCKET_PORT
= 9988
65 #expand _DIST_BIN = __XPC_BIN_PATH__
66 #expand _IS_WIN32 = len("__WIN32__") != 0
67 #expand _IS_MAC = __IS_MAC__ != 0
68 #expand _IS_LINUX = __IS_LINUX__ != 0
70 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
74 #expand _IS_CAMINO = __IS_CAMINO__ != 0
75 #expand _BIN_SUFFIX = __BIN_SUFFIX__
76 #expand _PERL = __PERL__
78 #expand _DEFAULT_APP = "./" + __BROWSER_PATH__
79 #expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
80 #expand _IS_TEST_BUILD = __IS_TEST_BUILD__
81 #expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
82 #expand _CRASHREPORTER = __CRASHREPORTER__ == 1
86 import ctypes
, ctypes
.wintypes
, time
, msvcrt
91 # We use the logging system here primarily because it'll handle multiple
92 # threads, which is needed to process the output of the server and application
93 # processes simultaneously.
94 _log
= logging
.getLogger()
95 handler
= logging
.StreamHandler(sys
.stdout
)
96 _log
.setLevel(logging
.INFO
)
97 _log
.addHandler(handler
)
104 class SyntaxError(Exception):
105 "Signifies a syntax error on a particular line in server-locations.txt."
107 def __init__(self
, lineno
, msg
= None):
112 s
= "Syntax error on line " + str(self
.lineno
)
114 s
+= ": %s." % self
.msg
121 "Represents a location line in server-locations.txt."
123 def __init__(self
, scheme
, host
, port
, options
):
127 self
.options
= options
129 class Automation(object):
131 Runs the browser from a script, and provides useful utilities
132 for setting up the browser environment.
139 IS_CYGWIN
= _IS_CYGWIN
140 IS_CAMINO
= _IS_CAMINO
141 BIN_SUFFIX
= _BIN_SUFFIX
144 UNIXISH
= not IS_WIN32
and not IS_MAC
146 DEFAULT_APP
= _DEFAULT_APP
147 CERTS_SRC_DIR
= _CERTS_SRC_DIR
148 IS_TEST_BUILD
= _IS_TEST_BUILD
149 IS_DEBUG_BUILD
= _IS_DEBUG_BUILD
150 CRASHREPORTER
= _CRASHREPORTER
152 # timeout, in seconds
153 DEFAULT_TIMEOUT
= 60.0
154 DEFAULT_WEB_SERVER
= _DEFAULT_WEB_SERVER
155 DEFAULT_HTTP_PORT
= _DEFAULT_HTTP_PORT
156 DEFAULT_SSL_PORT
= _DEFAULT_SSL_PORT
157 DEFAULT_WEBSOCKET_PORT
= _DEFAULT_WEBSOCKET_PORT
161 self
.lastTestSeen
= "automation.py"
162 self
.haveDumpedScreen
= False
164 def setServerInfo(self
,
165 webServer
= _DEFAULT_WEB_SERVER
,
166 httpPort
= _DEFAULT_HTTP_PORT
,
167 sslPort
= _DEFAULT_SSL_PORT
,
168 webSocketPort
= _DEFAULT_WEBSOCKET_PORT
):
169 self
.webServer
= webServer
170 self
.httpPort
= httpPort
171 self
.sslPort
= sslPort
172 self
.webSocketPort
= webSocketPort
194 class Process(subprocess
.Popen
):
196 Represents our view of a subprocess.
197 It adds a kill() method which allows it to be stopped explicitly.
212 universal_newlines
=False,
215 args
= automationutils
.wrapCommand(args
)
216 print "args: %s" % args
217 subprocess
.Popen
.__init
__(self
, args
, bufsize
, executable
,
218 stdin
, stdout
, stderr
,
219 preexec_fn
, close_fds
,
221 universal_newlines
, startupinfo
, creationflags
)
225 if Automation().IS_WIN32
:
227 pid
= "%i" % self
.pid
228 if platform
.release() == "2000":
229 # Windows 2000 needs 'kill.exe' from the
230 #'Windows 2000 Resource Kit tools'. (See bug 475455.)
232 subprocess
.Popen(["kill", "-f", pid
]).wait()
234 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid
)
236 # Windows XP and later.
237 subprocess
.Popen(["taskkill", "/F", "/PID", pid
]).wait()
239 os
.kill(self
.pid
, signal
.SIGKILL
)
241 def readLocations(self
, locationsPath
= "server-locations.txt"):
243 Reads the locations at which the Mochitest HTTP server is available from
244 server-locations.txt.
247 locationFile
= codecs
.open(locationsPath
, "r", "UTF-8")
249 # Perhaps more detail than necessary, but it's the easiest way to make sure
250 # we get exactly the format we want. See server-locations.txt for the exact
251 # format guaranteed here.
252 lineRe
= re
.compile(r
"^(?P<scheme>[a-z][-a-z0-9+.]*)"
255 r
"\d+\.\d+\.\d+\.\d+"
257 r
"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
258 r
"[a-z](?:[-a-z0-9]*[a-z0-9])?"
264 r
"(?P<options>\S+(?:,\S+)*)"
269 for line
in locationFile
:
271 if line
.startswith("#") or line
== "\n":
274 match
= lineRe
.match(line
)
276 raise SyntaxError(lineno
)
278 options
= match
.group("options")
280 options
= options
.split(",")
281 if "primary" in options
:
283 raise SyntaxError(lineno
, "multiple primary locations")
288 locations
.append(Location(match
.group("scheme"), match
.group("host"),
289 match
.group("port"), options
))
292 raise SyntaxError(lineno
+ 1, "missing primary location")
296 def setupPermissionsDatabase(self
, profileDir
, permissions
):
297 # Open database and create table
298 permDB
= sqlite3
.connect(os
.path
.join(profileDir
, "permissions.sqlite"))
299 cursor
= permDB
.cursor();
300 # SQL copied from nsPermissionManager.cpp
301 cursor
.execute("""CREATE TABLE moz_hosts (
302 id INTEGER PRIMARY KEY,
307 expireTime INTEGER)""")
309 # Insert desired permissions
311 for perm
in permissions
.keys():
312 for host
,allow
in permissions
[perm
]:
314 cursor
.execute("INSERT INTO moz_hosts values(?, ?, ?, ?, 0, 0)",
315 (c
, host
, perm
, 1 if allow
else 2))
321 def initializeProfile(self
, profileDir
, extraPrefs
= [], useServerLocations
= False):
322 " Sets up the standard testing profile."
325 # Start with a clean slate.
326 shutil
.rmtree(profileDir
, True)
329 # Set up permissions database
330 locations
= self
.readLocations()
331 self
.setupPermissionsDatabase(profileDir
,
332 {'allowXULXBL':[(l
.host
, 'noxul' not in l
.options
) for l
in locations
]});
335 user_pref("browser.console.showInPanel", true);
336 user_pref("browser.dom.window.dump.enabled", true);
337 user_pref("dom.allow_scripts_to_close_windows", true);
338 user_pref("dom.disable_open_during_load", false);
339 user_pref("dom.max_script_run_time", 0); // no slow script dialogs
340 user_pref("dom.max_chrome_script_run_time", 0);
341 user_pref("dom.popup_maximum", -1);
342 user_pref("dom.successive_dialog_time_limit", 0);
343 user_pref("signed.applets.codebase_principal_support", true);
344 user_pref("security.warn_submit_insecure", false);
345 user_pref("browser.shell.checkDefaultBrowser", false);
346 user_pref("shell.checkDefaultClient", false);
347 user_pref("browser.warnOnQuit", false);
348 user_pref("accessibility.typeaheadfind.autostart", false);
349 user_pref("javascript.options.showInConsole", true);
350 user_pref("devtools.errorconsole.enabled", true);
351 user_pref("layout.debug.enable_data_xbl", true);
352 user_pref("browser.EULA.override", true);
353 user_pref("javascript.options.tracejit.content", true);
354 user_pref("javascript.options.methodjit.content", true);
355 user_pref("javascript.options.jitprofiling.content", true);
356 user_pref("gfx.color_management.force_srgb", true);
357 user_pref("network.manage-offline-status", false);
358 user_pref("test.mousescroll", true);
359 user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs
360 user_pref("network.http.prompt-temp-redirect", false);
361 user_pref("media.cache_size", 100);
362 user_pref("security.warn_viewing_mixed", false);
363 user_pref("app.update.enabled", false);
364 user_pref("browser.panorama.experienced_first_run", true); // Assume experienced
366 // Only load extensions from the application and user profile
367 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
368 user_pref("extensions.enabledScopes", 5);
369 // Disable metadata caching for installed add-ons by default
370 user_pref("extensions.getAddons.cache.enabled", false);
372 user_pref("extensions.testpilot.runStudies", false);
374 user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
375 user_pref("geo.wifi.testing", true);
376 user_pref("geo.ignore.location_filter", true);
378 user_pref("camino.warn_when_closing", false); // Camino-only, harmless to others
380 // Make url-classifier updates so rare that they won't affect tests
381 user_pref("urlclassifier.updateinterval", 172800);
382 // Point the url-classifier to the local testing server for fast failures
383 user_pref("browser.safebrowsing.provider.0.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
384 user_pref("browser.safebrowsing.provider.0.keyURL", "http://%(server)s/safebrowsing-dummy/newkey");
385 user_pref("browser.safebrowsing.provider.0.updateURL", "http://%(server)s/safebrowsing-dummy/update");
386 // Point update checks to the local testing server for fast failures
387 user_pref("extensions.update.url", "http://%(server)s/extensions-dummy/updateURL");
388 user_pref("extensions.blocklist.url", "http://%(server)s/extensions-dummy/blocklistURL");
389 // Make sure opening about:addons won't hit the network
390 user_pref("extensions.webservice.discoverURL", "http://%(server)s/extensions-dummy/discoveryURL");
391 """ % { "server" : self
.webServer
+ ":" + str(self
.httpPort
) }
394 if useServerLocations
== False:
396 user_pref("capability.principal.codebase.p1.granted",
397 "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
398 UniversalPreferencesRead UniversalPreferencesWrite \
400 user_pref("capability.principal.codebase.p1.id", "%(origin)s");
401 user_pref("capability.principal.codebase.p1.subjectName", "");
402 """ % { "origin": "http://" + self
.webServer
+ ":" + str(self
.httpPort
) }
405 # Grant God-power to all the privileged servers on which tests run.
406 privileged
= filter(lambda loc
: "privileged" in loc
.options
, locations
)
407 for (i
, l
) in itertools
.izip(itertools
.count(1), privileged
):
409 user_pref("capability.principal.codebase.p%(i)d.granted",
410 "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
411 UniversalPreferencesRead UniversalPreferencesWrite \
413 user_pref("capability.principal.codebase.p%(i)d.id", "%(origin)s");
414 user_pref("capability.principal.codebase.p%(i)d.subjectName", "");
416 "origin": (l
.scheme
+ "://" + l
.host
+ ":" + str(l
.port
)) }
419 # We need to proxy every server but the primary one.
420 origins
= ["'%s://%s:%s'" % (l
.scheme
, l
.host
, l
.port
)
421 for l
in filter(lambda l
: "primary" not in l
.options
, locations
)]
422 origins
= ", ".join(origins
)
424 pacURL
= """data:text/plain,
425 function FindProxyForURL(url, host)
427 var origins = [%(origins)s];
428 var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
432 '(?::(\\\\\\\\d+))?/');
433 var matches = regex.exec(url);
436 var isHttp = matches[1] == 'http';
437 var isHttps = matches[1] == 'https';
438 var isWebSocket = matches[1] == 'ws';
441 if (isHttp | isWebSocket) matches[3] = '80';
442 if (isHttps) matches[3] = '443';
447 var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
448 if (origins.indexOf(origin) < 0)
451 return 'PROXY %(remote)s:%(httpport)s';
452 if (isHttps || isWebSocket)
453 return 'PROXY %(remote)s:%(sslport)s';
455 }""" % { "origins": origins
,
456 "remote": self
.webServer
,
457 "httpport":self
.httpPort
,
458 "sslport": self
.sslPort
}
459 pacURL
= "".join(pacURL
.splitlines())
462 user_pref("network.proxy.type", 2);
463 user_pref("network.proxy.autoconfig_url", "%(pacURL)s");
465 user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others
466 """ % {"pacURL": pacURL
}
470 thispref
= v
.split("=")
471 if len(thispref
) < 2:
472 print "Error: syntax error in --setpref=" + v
474 part
= 'user_pref("%s", %s);\n' % (thispref
[0], thispref
[1])
477 # write the preferences
478 prefsFile
= open(profileDir
+ "/" + "user.js", "a")
479 prefsFile
.write("".join(prefs
))
482 def addCommonOptions(self
, parser
):
483 "Adds command-line options which are common to mochitest and reftest."
485 parser
.add_option("--setpref",
486 action
= "append", type = "string",
488 dest
= "extraPrefs", metavar
= "PREF=VALUE",
489 help = "defines an extra user preference")
491 def fillCertificateDB(self
, profileDir
, certPath
, utilityPath
, xrePath
):
492 pwfilePath
= os
.path
.join(profileDir
, ".crtdbpw")
494 pwfile
= open(pwfilePath
, "w")
498 # Create head of the ssltunnel configuration file
499 sslTunnelConfigPath
= os
.path
.join(profileDir
, "ssltunnel.cfg")
500 sslTunnelConfig
= open(sslTunnelConfigPath
, "w")
502 sslTunnelConfig
.write("httpproxy:1\n")
503 sslTunnelConfig
.write("certdbdir:%s\n" % certPath
)
504 sslTunnelConfig
.write("forward:127.0.0.1:%s\n" % self
.httpPort
)
505 sslTunnelConfig
.write("websocketserver:%s:%s\n" % (self
.webServer
, self
.webSocketPort
))
506 sslTunnelConfig
.write("listen:*:%s:pgo server certificate\n" % self
.sslPort
)
508 # Configure automatic certificate and bind custom certificates, client authentication
509 locations
= self
.readLocations()
511 for loc
in locations
:
512 if loc
.scheme
== "https" and "nocert" not in loc
.options
:
513 customCertRE
= re
.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
514 clientAuthRE
= re
.compile("^clientauth=(?P<clientauth>[a-z]+)")
515 for option
in loc
.options
:
516 match
= customCertRE
.match(option
)
518 customcert
= match
.group("nickname");
519 sslTunnelConfig
.write("listen:%s:%s:%s:%s\n" %
520 (loc
.host
, loc
.port
, self
.sslPort
, customcert
))
522 match
= clientAuthRE
.match(option
)
524 clientauth
= match
.group("clientauth");
525 sslTunnelConfig
.write("clientauth:%s:%s:%s:%s\n" %
526 (loc
.host
, loc
.port
, self
.sslPort
, clientauth
))
528 sslTunnelConfig
.close()
530 # Pre-create the certification database for the profile
531 env
= self
.environment(xrePath
= xrePath
)
532 certutil
= os
.path
.join(utilityPath
, "certutil" + self
.BIN_SUFFIX
)
533 pk12util
= os
.path
.join(utilityPath
, "pk12util" + self
.BIN_SUFFIX
)
535 status
= self
.Process([certutil
, "-N", "-d", profileDir
, "-f", pwfilePath
], env
= env
).wait()
539 # Walk the cert directory and add custom CAs and client certs
540 files
= os
.listdir(certPath
)
542 root
, ext
= os
.path
.splitext(item
)
545 if root
.endswith("-object"):
547 self
.Process([certutil
, "-A", "-i", os
.path
.join(certPath
, item
),
548 "-d", profileDir
, "-f", pwfilePath
, "-n", root
, "-t", trustBits
],
551 self
.Process([pk12util
, "-i", os
.path
.join(certPath
, item
), "-w",
552 pwfilePath
, "-d", profileDir
],
555 os
.unlink(pwfilePath
)
558 def environment(self
, env
= None, xrePath
= None, crashreporter
= True):
560 xrePath
= self
.DIST_BIN
562 env
= dict(os
.environ
)
564 ldLibraryPath
= os
.path
.abspath(os
.path
.join(SCRIPT_DIR
, xrePath
))
565 if self
.UNIXISH
or self
.IS_MAC
:
566 envVar
= "LD_LIBRARY_PATH"
568 envVar
= "DYLD_LIBRARY_PATH"
570 env
['MOZILLA_FIVE_HOME'] = xrePath
572 ldLibraryPath
= ldLibraryPath
+ ":" + env
[envVar
]
573 env
[envVar
] = ldLibraryPath
575 env
["PATH"] = env
["PATH"] + ";" + ldLibraryPath
578 env
['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
579 env
['MOZ_CRASHREPORTER'] = '1'
581 env
['MOZ_CRASHREPORTER_DISABLE'] = '1'
583 env
['GNOME_DISABLE_CRASH_DIALOG'] = '1'
584 env
['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
586 # Don't do this for Mac since it makes the Mac OS X 10.5 (32-bit)
587 # trace-malloc leak test hang. (It doesn't make the 10.6 (64-bit)
588 # leak test hang, though.)
590 env
['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
594 PeekNamedPipe
= ctypes
.windll
.kernel32
.PeekNamedPipe
595 GetLastError
= ctypes
.windll
.kernel32
.GetLastError
597 def readWithTimeout(self
, f
, timeout
):
598 """Try to read a line of output from the file object |f|.
599 |f| must be a pipe, like the |stdout| member of a subprocess.Popen
600 object created with stdout=PIPE. If no output
601 is received within |timeout| seconds, return a blank line.
602 Returns a tuple (line, did_timeout), where |did_timeout| is True
603 if the read timed out, and False otherwise."""
605 # shortcut to allow callers to pass in "None" for no timeout.
606 return (f
.readline(), False)
607 x
= msvcrt
.get_osfhandle(f
.fileno())
609 done
= time
.time() + timeout
610 while time
.time() < done
:
611 if self
.PeekNamedPipe(x
, None, 0, None, ctypes
.byref(l
), None) == 0:
612 err
= self
.GetLastError()
613 if err
== 38 or err
== 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
616 log
.error("readWithTimeout got error: %d", err
)
618 # we're assuming that the output is line-buffered,
619 # which is not unreasonable
620 return (f
.readline(), False)
624 def isPidAlive(self
, pid
):
626 PROCESS_QUERY_LIMITED_INFORMATION
= 0x1000
627 pHandle
= ctypes
.windll
.kernel32
.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION
, 0, pid
)
630 pExitCode
= ctypes
.wintypes
.DWORD()
631 ctypes
.windll
.kernel32
.GetExitCodeProcess(pHandle
, ctypes
.byref(pExitCode
))
632 ctypes
.windll
.kernel32
.CloseHandle(pHandle
)
633 if (pExitCode
.value
== STILL_ACTIVE
):
638 def killPid(self
, pid
):
639 PROCESS_TERMINATE
= 0x0001
640 pHandle
= ctypes
.windll
.kernel32
.OpenProcess(PROCESS_TERMINATE
, 0, pid
)
643 success
= ctypes
.windll
.kernel32
.TerminateProcess(pHandle
, 1)
644 ctypes
.windll
.kernel32
.CloseHandle(pHandle
)
648 def readWithTimeout(self
, f
, timeout
):
649 """Try to read a line of output from the file object |f|. If no output
650 is received within |timeout| seconds, return a blank line.
651 Returns a tuple (line, did_timeout), where |did_timeout| is True
652 if the read timed out, and False otherwise."""
653 (r
, w
, e
) = select
.select([f
], [], [], timeout
)
656 return (f
.readline(), False)
658 def isPidAlive(self
, pid
):
660 # kill(pid, 0) checks for a valid PID without actually sending a signal
661 # The method throws OSError if the PID is invalid, which we catch below.
664 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
665 # the process terminates before we get to this point.
666 wpid
, wstatus
= os
.waitpid(pid
, os
.WNOHANG
)
672 # Catch the errors we might expect from os.kill/os.waitpid,
673 # and re-raise any others
674 if err
.errno
== errno
.ESRCH
or err
.errno
== errno
.ECHILD
:
678 def killPid(self
, pid
):
679 os
.kill(pid
, signal
.SIGKILL
)
682 def dumpScreen(self
, utilityPath
):
683 self
.haveDumpedScreen
= True;
685 screentopng
= os
.path
.join(utilityPath
, "screentopng")
687 dumper
= self
.Process([screentopng
], bufsize
=-1,
688 stdout
=subprocess
.PIPE
, close_fds
=True)
690 self
.log
.info("Failed to start %s for screenshot: %s",
691 screentopng
, err
.strerror
)
694 image
= dumper
.stdout
.read()
695 status
= dumper
.wait()
697 self
.log
.info("screentopng exited with code %d", status
)
701 encoded
= base64
.b64encode(image
)
702 self
.log
.info("SCREENSHOT: data:image/png;base64,%s", encoded
)
704 def killAndGetStack(self
, proc
, utilityPath
, debuggerInfo
):
705 """Kill the process, preferrably in a way that gets us a stack trace."""
706 if self
.UNIXISH
and not debuggerInfo
and not self
.haveDumpedScreen
:
707 self
.dumpScreen(utilityPath
)
709 if self
.CRASHREPORTER
and not debuggerInfo
:
711 # ABRT will get picked up by Breakpad's signal handler
712 os
.kill(proc
.pid
, signal
.SIGABRT
)
715 # We should have a "crashinject" program in our utility path
716 crashinject
= os
.path
.normpath(os
.path
.join(utilityPath
, "crashinject.exe"))
717 if os
.path
.exists(crashinject
) and subprocess
.Popen([crashinject
, str(proc
.pid
)]).wait() == 0:
719 #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
720 self
.log
.info("Can't trigger Breakpad, just killing process")
723 def waitForFinish(self
, proc
, utilityPath
, timeout
, maxTime
, startTime
, debuggerInfo
, symbolsPath
):
724 """ Look for timeout or crashes and return the status after the process terminates """
725 stackFixerProcess
= None
726 stackFixerFunction
= None
728 if proc
.stdout
is None:
729 self
.log
.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
731 logsource
= proc
.stdout
733 if self
.IS_DEBUG_BUILD
and (self
.IS_MAC
or self
.IS_LINUX
) and symbolsPath
and os
.path
.exists(symbolsPath
):
734 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
735 # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
736 sys
.path
.insert(0, utilityPath
)
737 import fix_stack_using_bpsyms
as stackFixerModule
738 stackFixerFunction
= lambda line
: stackFixerModule
.fixSymbols(line
, symbolsPath
)
740 elif self
.IS_DEBUG_BUILD
and self
.IS_MAC
and False:
741 # Run each line through a function in fix_macosx_stack.py (uses atos)
742 sys
.path
.insert(0, utilityPath
)
743 import fix_macosx_stack
as stackFixerModule
744 stackFixerFunction
= lambda line
: stackFixerModule
.fixSymbols(line
)
746 elif self
.IS_DEBUG_BUILD
and self
.IS_LINUX
:
747 # Run logsource through fix-linux-stack.pl (uses addr2line)
748 # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
749 stackFixerProcess
= self
.Process([self
.PERL
, os
.path
.join(utilityPath
, "fix-linux-stack.pl")],
751 stdout
=subprocess
.PIPE
)
752 logsource
= stackFixerProcess
.stdout
754 (line
, didTimeout
) = self
.readWithTimeout(logsource
, timeout
)
756 while line
!= "" and not didTimeout
:
757 if "TEST-START" in line
and "|" in line
:
758 self
.lastTestSeen
= line
.split("|")[1].strip()
759 if stackFixerFunction
:
760 line
= stackFixerFunction(line
)
761 self
.log
.info(line
.rstrip())
762 if self
.UNIXISH
and not debuggerInfo
and not self
.haveDumpedScreen
and "TEST-UNEXPECTED-FAIL" in line
and "Test timed out" in line
:
763 self
.dumpScreen(utilityPath
)
765 (line
, didTimeout
) = self
.readWithTimeout(logsource
, timeout
)
766 if not hitMaxTime
and maxTime
and datetime
.now() - startTime
> timedelta(seconds
= maxTime
):
767 # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
769 self
.log
.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self
.lastTestSeen
, int(maxTime
))
770 self
.killAndGetStack(proc
, utilityPath
, debuggerInfo
)
772 self
.log
.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self
.lastTestSeen
, int(timeout
))
773 self
.killAndGetStack(proc
, utilityPath
, debuggerInfo
)
777 self
.lastTestSeen
= "Main app process exited normally"
778 if status
!= 0 and not didTimeout
and not hitMaxTime
:
779 self
.log
.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self
.lastTestSeen
, status
)
780 if stackFixerProcess
is not None:
781 fixerStatus
= stackFixerProcess
.wait()
782 if fixerStatus
!= 0 and not didTimeout
and not hitMaxTime
:
783 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus
)
786 def buildCommandLine(self
, app
, debuggerInfo
, profileDir
, testURL
, extraArgs
):
787 """ build the application command line """
790 if self
.IS_MAC
and not self
.IS_CAMINO
and not cmd
.endswith("-bin"):
792 cmd
= os
.path
.abspath(cmd
)
797 args
.extend(debuggerInfo
["args"])
799 cmd
= os
.path
.abspath(debuggerInfo
["path"])
802 args
.append("-foreground")
805 profileDirectory
= commands
.getoutput("cygpath -w \"" + profileDir
+ "/\"")
807 profileDirectory
= profileDir
+ "/"
809 args
.extend(("-no-remote", "-profile", profileDirectory
))
810 if testURL
is not None:
812 args
.extend(("-url", testURL
))
814 args
.append((testURL
))
815 args
.extend(extraArgs
)
818 def checkForZombies(self
, processLog
):
819 """ Look for hung processes """
820 if not os
.path
.exists(processLog
):
821 self
.log
.info('INFO | automation.py | PID log not found: %s', processLog
)
823 self
.log
.info('INFO | automation.py | Reading PID log: %s', processLog
)
825 pidRE
= re
.compile(r
'launched child process (\d+)$')
826 processLogFD
= open(processLog
)
827 for line
in processLogFD
:
828 self
.log
.info(line
.rstrip())
829 m
= pidRE
.search(line
)
831 processList
.append(int(m
.group(1)))
834 for processPID
in processList
:
835 self
.log
.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID
)
836 if self
.isPidAlive(processPID
):
837 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID
)
838 self
.killPid(processPID
)
840 def runApp(self
, testURL
, env
, app
, profileDir
, extraArgs
,
841 runSSLTunnel
= False, utilityPath
= None,
842 xrePath
= None, certPath
= None,
843 debuggerInfo
= None, symbolsPath
= None,
844 timeout
= -1, maxTime
= None):
846 Run the app, log the duration it took to execute, return the status code.
847 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
850 if utilityPath
== None:
851 utilityPath
= self
.DIST_BIN
853 xrePath
= self
.DIST_BIN
855 certPath
= self
.CERTS_SRC_DIR
857 timeout
= self
.DEFAULT_TIMEOUT
859 # copy env so we don't munge the caller's environment
861 env
["NO_EM_RESTART"] = "1"
862 tmpfd
, processLog
= tempfile
.mkstemp(suffix
='pidlog')
864 env
["MOZ_PROCESS_LOG"] = processLog
866 if self
.IS_TEST_BUILD
and runSSLTunnel
:
867 # create certificate database for the profile
868 certificateStatus
= self
.fillCertificateDB(profileDir
, certPath
, utilityPath
, xrePath
)
869 if certificateStatus
!= 0:
870 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | Certificate integration failed")
871 return certificateStatus
873 # start ssltunnel to provide https:// URLs capability
874 ssltunnel
= os
.path
.join(utilityPath
, "ssltunnel" + self
.BIN_SUFFIX
)
875 ssltunnelProcess
= self
.Process([ssltunnel
,
876 os
.path
.join(profileDir
, "ssltunnel.cfg")],
877 env
= self
.environment(xrePath
= xrePath
))
878 self
.log
.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess
.pid
)
880 cmd
, args
= self
.buildCommandLine(app
, debuggerInfo
, profileDir
, testURL
, extraArgs
)
881 startTime
= datetime
.now()
883 if debuggerInfo
and debuggerInfo
["interactive"]:
884 # If an interactive debugger is attached, don't redirect output,
885 # don't use timeouts, and don't capture ctrl-c.
889 signal
.signal(signal
.SIGINT
, lambda sigid
, frame
: None)
891 outputPipe
= subprocess
.PIPE
893 self
.lastTestSeen
= "automation.py"
894 proc
= self
.Process([cmd
] + args
,
895 env
= self
.environment(env
, xrePath
= xrePath
,
896 crashreporter
= not debuggerInfo
),
898 stderr
= subprocess
.STDOUT
)
899 self
.log
.info("INFO | automation.py | Application pid: %d", proc
.pid
)
901 status
= self
.waitForFinish(proc
, utilityPath
, timeout
, maxTime
, startTime
, debuggerInfo
, symbolsPath
)
902 self
.log
.info("INFO | automation.py | Application ran for: %s", str(datetime
.now() - startTime
))
904 # Do a final check for zombie child processes.
905 self
.checkForZombies(processLog
)
906 automationutils
.checkForCrashes(os
.path
.join(profileDir
, "minidumps"), symbolsPath
, self
.lastTestSeen
)
908 if os
.path
.exists(processLog
):
909 os
.unlink(processLog
)
911 if self
.IS_TEST_BUILD
and runSSLTunnel
:
912 ssltunnelProcess
.kill()
917 Copies an "installed" extension into the extensions directory of the given profile
918 extensionSource - the source location of the extension files. This can be either
919 a directory or a path to an xpi file.
920 profileDir - the profile directory we are copying into. We will create the
921 "extensions" directory there if it doesn't exist
922 extensionID - the id of the extension to be used as the containing directory for the
924 this is the name of the folder in the <profileDir>/extensions/<extensionID>
926 def installExtension(self
, extensionSource
, profileDir
, extensionID
):
927 if (not os
.path
.exists(extensionSource
)):
928 self
.log
.info("INFO | automation.py | Cannot install extension no source at: %s", extensionSource
)
931 if (not os
.path
.exists(profileDir
)):
932 self
.log
.info("INFO | automation.py | Cannot install extension invalid profileDir at: %s", profileDir
)
935 # See if we have an XPI or a directory
936 if (os
.path
.isfile(extensionSource
)):
937 tmpd
= tempfile
.mkdtemp()
938 extrootdir
= self
.extractZip(extensionSource
, tmpd
)
940 extrootdir
= extensionSource
941 extnsdir
= os
.path
.join(profileDir
, "extensions")
942 extnshome
= os
.path
.join(extnsdir
, extensionID
)
944 # Now we copy the extension source into the extnshome
945 shutil
.copytree(extrootdir
, extnshome
)
947 def extractZip(self
, filename
, dest
):
948 z
= zipfile
.ZipFile(filename
, 'r')
949 for n
in z
.namelist():
950 fullpath
= os
.path
.join(dest
, n
)
951 parentdir
= os
.path
.dirname(fullpath
)
952 if not os
.path
.isdir(parentdir
):
953 os
.makedirs(parentdir
)
954 if (not n
.endswith(os
.sep
)):
956 f
= open(fullpath
, 'w')