[JAEGER] Fixed bugs in commit bumping (bug 586886 fallout).
[mozilla-central.git] / build / automation.py.in
blobd8d905e52300379a5b017027cbd97f265b9f526d
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
13 # License.
15 # The Original Code is mozilla.org code.
17 # The Initial Developer of the Original Code is
18 # Mozilla Foundation.
19 # Portions created by the Initial Developer are Copyright (C) 2008
20 # the Initial Developer. All Rights Reserved.
22 # Contributor(s):
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 *****
40 import codecs
41 from datetime import datetime, timedelta
42 import itertools
43 import logging
44 import os
45 import re
46 import select
47 import shutil
48 import signal
49 import subprocess
50 import sys
51 import threading
52 import tempfile
53 import zipfile
55 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
56 sys.path.insert(0, SCRIPT_DIR)
57 import automationutils
59 _DEFAULT_WEB_SERVER = "127.0.0.1"
60 _DEFAULT_HTTP_PORT = 8888
61 _DEFAULT_SSL_PORT = 4443
62 _DEFAULT_WEBSOCKET_PORT = 9988
64 #expand _DIST_BIN = __XPC_BIN_PATH__
65 #expand _IS_WIN32 = len("__WIN32__") != 0
66 #expand _IS_MAC = __IS_MAC__ != 0
67 #expand _IS_LINUX = __IS_LINUX__ != 0
68 #ifdef IS_CYGWIN
69 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
70 #else
71 _IS_CYGWIN = False
72 #endif
73 #expand _IS_CAMINO = __IS_CAMINO__ != 0
74 #expand _BIN_SUFFIX = __BIN_SUFFIX__
75 #expand _PERL = __PERL__
77 #expand _DEFAULT_APP = "./" + __BROWSER_PATH__
78 #expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
79 #expand _IS_TEST_BUILD = __IS_TEST_BUILD__
80 #expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
81 #expand _CRASHREPORTER = __CRASHREPORTER__ == 1
84 if _IS_WIN32:
85 import ctypes, ctypes.wintypes, time, msvcrt
86 else:
87 import errno
90 # We use the logging system here primarily because it'll handle multiple
91 # threads, which is needed to process the output of the server and application
92 # processes simultaneously.
93 _log = logging.getLogger()
94 handler = logging.StreamHandler(sys.stdout)
95 _log.setLevel(logging.INFO)
96 _log.addHandler(handler)
99 #################
100 # PROFILE SETUP #
101 #################
103 class SyntaxError(Exception):
104 "Signifies a syntax error on a particular line in server-locations.txt."
106 def __init__(self, lineno, msg = None):
107 self.lineno = lineno
108 self.msg = msg
110 def __str__(self):
111 s = "Syntax error on line " + str(self.lineno)
112 if self.msg:
113 s += ": %s." % self.msg
114 else:
115 s += "."
116 return s
119 class Location:
120 "Represents a location line in server-locations.txt."
122 def __init__(self, scheme, host, port, options):
123 self.scheme = scheme
124 self.host = host
125 self.port = port
126 self.options = options
128 class Automation(object):
130 Runs the browser from a script, and provides useful utilities
131 for setting up the browser environment.
134 DIST_BIN = _DIST_BIN
135 IS_WIN32 = _IS_WIN32
136 IS_MAC = _IS_MAC
137 IS_LINUX = _IS_LINUX
138 IS_CYGWIN = _IS_CYGWIN
139 IS_CAMINO = _IS_CAMINO
140 BIN_SUFFIX = _BIN_SUFFIX
141 PERL = _PERL
143 UNIXISH = not IS_WIN32 and not IS_MAC
145 DEFAULT_APP = _DEFAULT_APP
146 CERTS_SRC_DIR = _CERTS_SRC_DIR
147 IS_TEST_BUILD = _IS_TEST_BUILD
148 IS_DEBUG_BUILD = _IS_DEBUG_BUILD
149 CRASHREPORTER = _CRASHREPORTER
151 # timeout, in seconds
152 DEFAULT_TIMEOUT = 60.0
153 DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
154 DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
155 DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
156 DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
158 def __init__(self):
159 self.log = _log
160 self.lastTestSeen = "automation.py"
162 def setServerInfo(self,
163 webServer = _DEFAULT_WEB_SERVER,
164 httpPort = _DEFAULT_HTTP_PORT,
165 sslPort = _DEFAULT_SSL_PORT,
166 webSocketPort = _DEFAULT_WEBSOCKET_PORT):
167 self.webServer = webServer
168 self.httpPort = httpPort
169 self.sslPort = sslPort
170 self.webSocketPort = webSocketPort
172 @property
173 def __all__(self):
174 return [
175 "UNIXISH",
176 "IS_WIN32",
177 "IS_MAC",
178 "log",
179 "runApp",
180 "Process",
181 "addCommonOptions",
182 "initializeProfile",
183 "DIST_BIN",
184 "DEFAULT_APP",
185 "CERTS_SRC_DIR",
186 "environment",
187 "IS_TEST_BUILD",
188 "IS_DEBUG_BUILD",
189 "DEFAULT_TIMEOUT",
192 class Process(subprocess.Popen):
194 Represents our view of a subprocess.
195 It adds a kill() method which allows it to be stopped explicitly.
198 def __init__(self,
199 args,
200 bufsize=0,
201 executable=None,
202 stdin=None,
203 stdout=None,
204 stderr=None,
205 preexec_fn=None,
206 close_fds=False,
207 shell=False,
208 cwd=None,
209 env=None,
210 universal_newlines=False,
211 startupinfo=None,
212 creationflags=0):
213 subprocess.Popen.__init__(self, args, bufsize, executable,
214 stdin, stdout, stderr,
215 preexec_fn, close_fds,
216 shell, cwd, env,
217 universal_newlines, startupinfo, creationflags)
218 self.log = _log
220 def kill(self):
221 if Automation().IS_WIN32:
222 import platform
223 pid = "%i" % self.pid
224 if platform.release() == "2000":
225 # Windows 2000 needs 'kill.exe' from the
226 #'Windows 2000 Resource Kit tools'. (See bug 475455.)
227 try:
228 subprocess.Popen(["kill", "-f", pid]).wait()
229 except:
230 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
231 else:
232 # Windows XP and later.
233 subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
234 else:
235 os.kill(self.pid, signal.SIGKILL)
237 def readLocations(self, locationsPath = "server-locations.txt"):
239 Reads the locations at which the Mochitest HTTP server is available from
240 server-locations.txt.
243 locationFile = codecs.open(locationsPath, "r", "UTF-8")
245 # Perhaps more detail than necessary, but it's the easiest way to make sure
246 # we get exactly the format we want. See server-locations.txt for the exact
247 # format guaranteed here.
248 lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
249 r"://"
250 r"(?P<host>"
251 r"\d+\.\d+\.\d+\.\d+"
252 r"|"
253 r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
254 r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
255 r")"
256 r":"
257 r"(?P<port>\d+)"
258 r"(?:"
259 r"\s+"
260 r"(?P<options>\S+(?:,\S+)*)"
261 r")?$")
262 locations = []
263 lineno = 0
264 seenPrimary = False
265 for line in locationFile:
266 lineno += 1
267 if line.startswith("#") or line == "\n":
268 continue
270 match = lineRe.match(line)
271 if not match:
272 raise SyntaxError(lineno)
274 options = match.group("options")
275 if options:
276 options = options.split(",")
277 if "primary" in options:
278 if seenPrimary:
279 raise SyntaxError(lineno, "multiple primary locations")
280 seenPrimary = True
281 else:
282 options = []
284 locations.append(Location(match.group("scheme"), match.group("host"),
285 match.group("port"), options))
287 if not seenPrimary:
288 raise SyntaxError(lineno + 1, "missing primary location")
290 return locations
292 def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
293 " Sets up the standard testing profile."
295 prefs = []
296 # Start with a clean slate.
297 shutil.rmtree(profileDir, True)
298 os.mkdir(profileDir)
300 part = """\
301 user_pref("browser.dom.window.dump.enabled", true);
302 user_pref("dom.allow_scripts_to_close_windows", true);
303 user_pref("dom.disable_open_during_load", false);
304 user_pref("dom.max_script_run_time", 0); // no slow script dialogs
305 user_pref("dom.max_chrome_script_run_time", 0);
306 user_pref("dom.popup_maximum", -1);
307 user_pref("signed.applets.codebase_principal_support", true);
308 user_pref("security.warn_submit_insecure", false);
309 user_pref("browser.shell.checkDefaultBrowser", false);
310 user_pref("shell.checkDefaultClient", false);
311 user_pref("browser.warnOnQuit", false);
312 user_pref("accessibility.typeaheadfind.autostart", false);
313 user_pref("javascript.options.showInConsole", true);
314 user_pref("layout.debug.enable_data_xbl", true);
315 user_pref("browser.EULA.override", true);
316 user_pref("javascript.options.jit.content", false);
317 user_pref("javascript.options.methodjit.content", true);
318 user_pref("gfx.color_management.force_srgb", true);
319 user_pref("network.manage-offline-status", false);
320 user_pref("test.mousescroll", true);
321 user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs
322 user_pref("network.http.prompt-temp-redirect", false);
323 user_pref("media.cache_size", 100);
324 user_pref("security.warn_viewing_mixed", false);
326 // Only load extensions from the application and user profile
327 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
328 user_pref("extensions.enabledScopes", 5);
330 user_pref("extensions.testpilot.runStudies", false);
332 user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
333 user_pref("geo.wifi.testing", true);
335 user_pref("camino.warn_when_closing", false); // Camino-only, harmless to others
337 // Make url-classifier updates so rare that they won't affect tests
338 user_pref("urlclassifier.updateinterval", 172800);
339 // Point the url-classifier to the local testing server for fast failures
340 user_pref("browser.safebrowsing.provider.0.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
341 user_pref("browser.safebrowsing.provider.0.keyURL", "http://%(server)s/safebrowsing-dummy/newkey");
342 user_pref("browser.safebrowsing.provider.0.updateURL", "http://%(server)s/safebrowsing-dummy/update");
343 """ % { "server" : self.webServer + ":" + str(self.httpPort) }
344 prefs.append(part)
346 if useServerLocations == False:
347 part = """
348 user_pref("capability.principal.codebase.p1.granted",
349 "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
350 UniversalPreferencesRead UniversalPreferencesWrite \
351 UniversalFileRead");
352 user_pref("capability.principal.codebase.p1.id", "%(origin)s");
353 user_pref("capability.principal.codebase.p1.subjectName", "");
354 """ % { "origin": "http://" + self.webServer + ":" + str(self.httpPort) }
355 prefs.append(part)
356 else:
357 locations = self.readLocations()
359 # Grant God-power to all the privileged servers on which tests run.
360 privileged = filter(lambda loc: "privileged" in loc.options, locations)
361 for (i, l) in itertools.izip(itertools.count(1), privileged):
362 part = """
363 user_pref("capability.principal.codebase.p%(i)d.granted",
364 "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
365 UniversalPreferencesRead UniversalPreferencesWrite \
366 UniversalFileRead");
367 user_pref("capability.principal.codebase.p%(i)d.id", "%(origin)s");
368 user_pref("capability.principal.codebase.p%(i)d.subjectName", "");
369 """ % { "i": i,
370 "origin": (l.scheme + "://" + l.host + ":" + str(l.port)) }
371 prefs.append(part)
373 # We need to proxy every server but the primary one.
374 origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port)
375 for l in filter(lambda l: "primary" not in l.options, locations)]
376 origins = ", ".join(origins)
378 pacURL = """data:text/plain,
379 function FindProxyForURL(url, host)
381 var origins = [%(origins)s];
382 var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
383 '://' +
384 '(?:[^/@]*@)?' +
385 '(.*?)' +
386 '(?::(\\\\\\\\d+))?/');
387 var matches = regex.exec(url);
388 if (!matches)
389 return 'DIRECT';
390 var isHttp = matches[1] == 'http';
391 var isHttps = matches[1] == 'https';
392 var isWebSocket = matches[1] == 'ws';
393 if (!matches[3])
395 if (isHttp | isWebSocket) matches[3] = '80';
396 if (isHttps) matches[3] = '443';
398 if (isWebSocket)
399 matches[1] = 'http';
401 var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
402 if (origins.indexOf(origin) < 0)
403 return 'DIRECT';
404 if (isHttp)
405 return 'PROXY %(remote)s:%(httpport)s';
406 if (isHttps || isWebSocket)
407 return 'PROXY %(remote)s:%(sslport)s';
408 return 'DIRECT';
409 }""" % { "origins": origins,
410 "remote": self.webServer,
411 "httpport":self.httpPort,
412 "sslport": self.sslPort }
413 pacURL = "".join(pacURL.splitlines())
415 part += """
416 user_pref("network.proxy.type", 2);
417 user_pref("network.proxy.autoconfig_url", "%(pacURL)s");
419 user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others
420 """ % {"pacURL": pacURL}
421 prefs.append(part)
423 for v in extraPrefs:
424 thispref = v.split("=")
425 if len(thispref) < 2:
426 print "Error: syntax error in --setpref=" + v
427 sys.exit(1)
428 part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
429 prefs.append(part)
431 # write the preferences
432 prefsFile = open(profileDir + "/" + "user.js", "a")
433 prefsFile.write("".join(prefs))
434 prefsFile.close()
436 def addCommonOptions(self, parser):
437 "Adds command-line options which are common to mochitest and reftest."
439 parser.add_option("--setpref",
440 action = "append", type = "string",
441 default = [],
442 dest = "extraPrefs", metavar = "PREF=VALUE",
443 help = "defines an extra user preference")
445 def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
446 pwfilePath = os.path.join(profileDir, ".crtdbpw")
448 pwfile = open(pwfilePath, "w")
449 pwfile.write("\n")
450 pwfile.close()
452 # Create head of the ssltunnel configuration file
453 sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
454 sslTunnelConfig = open(sslTunnelConfigPath, "w")
456 sslTunnelConfig.write("httpproxy:1\n")
457 sslTunnelConfig.write("certdbdir:%s\n" % certPath)
458 sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
459 sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
460 sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
462 # Configure automatic certificate and bind custom certificates, client authentication
463 locations = self.readLocations()
464 locations.pop(0)
465 for loc in locations:
466 if loc.scheme == "https" and "nocert" not in loc.options:
467 customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
468 clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
469 for option in loc.options:
470 match = customCertRE.match(option)
471 if match:
472 customcert = match.group("nickname");
473 sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
474 (loc.host, loc.port, self.sslPort, customcert))
476 match = clientAuthRE.match(option)
477 if match:
478 clientauth = match.group("clientauth");
479 sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
480 (loc.host, loc.port, self.sslPort, clientauth))
482 sslTunnelConfig.close()
484 # Pre-create the certification database for the profile
485 env = self.environment(xrePath = xrePath)
486 certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
487 pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)
489 status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
490 if status != 0:
491 return status
493 # Walk the cert directory and add custom CAs and client certs
494 files = os.listdir(certPath)
495 for item in files:
496 root, ext = os.path.splitext(item)
497 if ext == ".ca":
498 trustBits = "CT,,"
499 if root.endswith("-object"):
500 trustBits = "CT,,CT"
501 self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
502 "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
503 env = env).wait()
504 if ext == ".client":
505 self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
506 pwfilePath, "-d", profileDir],
507 env = env).wait()
509 os.unlink(pwfilePath)
510 return 0
512 def environment(self, env = None, xrePath = None, crashreporter = True):
513 if xrePath == None:
514 xrePath = self.DIST_BIN
515 if env == None:
516 env = dict(os.environ)
518 ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
519 if self.UNIXISH or self.IS_MAC:
520 envVar = "LD_LIBRARY_PATH"
521 if self.IS_MAC:
522 envVar = "DYLD_LIBRARY_PATH"
523 else: # unixish
524 env['MOZILLA_FIVE_HOME'] = xrePath
525 if envVar in env:
526 ldLibraryPath = ldLibraryPath + ":" + env[envVar]
527 env[envVar] = ldLibraryPath
528 elif self.IS_WIN32:
529 env["PATH"] = env["PATH"] + ";" + ldLibraryPath
531 if crashreporter:
532 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
533 env['MOZ_CRASHREPORTER'] = '1'
534 else:
535 env['MOZ_CRASHREPORTER_DISABLE'] = '1'
537 env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
538 env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
539 return env
541 if IS_WIN32:
542 PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
543 GetLastError = ctypes.windll.kernel32.GetLastError
545 def readWithTimeout(self, f, timeout):
546 """Try to read a line of output from the file object |f|.
547 |f| must be a pipe, like the |stdout| member of a subprocess.Popen
548 object created with stdout=PIPE. If no output
549 is received within |timeout| seconds, return a blank line.
550 Returns a tuple (line, did_timeout), where |did_timeout| is True
551 if the read timed out, and False otherwise."""
552 if timeout is None:
553 # shortcut to allow callers to pass in "None" for no timeout.
554 return (f.readline(), False)
555 x = msvcrt.get_osfhandle(f.fileno())
556 l = ctypes.c_long()
557 done = time.time() + timeout
558 while time.time() < done:
559 if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
560 err = self.GetLastError()
561 if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
562 return ('', False)
563 else:
564 log.error("readWithTimeout got error: %d", err)
565 if l.value > 0:
566 # we're assuming that the output is line-buffered,
567 # which is not unreasonable
568 return (f.readline(), False)
569 time.sleep(0.01)
570 return ('', True)
572 def isPidAlive(self, pid):
573 STILL_ACTIVE = 259
574 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
575 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
576 if not pHandle:
577 return False
578 pExitCode = ctypes.wintypes.DWORD()
579 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
580 ctypes.windll.kernel32.CloseHandle(pHandle)
581 if (pExitCode.value == STILL_ACTIVE):
582 return True
583 else:
584 return False
586 def killPid(self, pid):
587 PROCESS_TERMINATE = 0x0001
588 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
589 if not pHandle:
590 return
591 success = ctypes.windll.kernel32.TerminateProcess(pHandle, 1)
592 ctypes.windll.kernel32.CloseHandle(pHandle)
594 else:
596 def readWithTimeout(self, f, timeout):
597 """Try to read a line of output from the file object |f|. If no output
598 is received within |timeout| seconds, return a blank line.
599 Returns a tuple (line, did_timeout), where |did_timeout| is True
600 if the read timed out, and False otherwise."""
601 (r, w, e) = select.select([f], [], [], timeout)
602 if len(r) == 0:
603 return ('', True)
604 return (f.readline(), False)
606 def isPidAlive(self, pid):
607 try:
608 # kill(pid, 0) checks for a valid PID without actually sending a signal
609 # The method throws OSError if the PID is invalid, which we catch below.
610 os.kill(pid, 0)
612 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
613 # the process terminates before we get to this point.
614 wpid, wstatus = os.waitpid(pid, os.WNOHANG)
615 if wpid == 0:
616 return True
618 return False
619 except OSError, err:
620 # Catch the errors we might expect from os.kill/os.waitpid,
621 # and re-raise any others
622 if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
623 return False
624 raise
626 def killPid(self, pid):
627 os.kill(pid, signal.SIGKILL)
629 def killAndGetStack(self, proc, utilityPath, debuggerInfo):
630 """Kill the process, preferrably in a way that gets us a stack trace."""
631 if self.CRASHREPORTER and not debuggerInfo:
632 if self.UNIXISH:
633 # ABRT will get picked up by Breakpad's signal handler
634 os.kill(proc.pid, signal.SIGABRT)
635 return
636 elif self.IS_WIN32:
637 # We should have a "crashinject" program in our utility path
638 crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
639 if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
640 return
641 #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
642 self.log.info("Can't trigger Breakpad, just killing process")
643 proc.kill()
645 def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
646 """ Look for timeout or crashes and return the status after the process terminates """
647 stackFixerProcess = None
648 stackFixerFunction = None
649 didTimeout = False
650 if proc.stdout is None:
651 self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
652 else:
653 logsource = proc.stdout
655 if self.IS_DEBUG_BUILD and (self.IS_MAC or self.IS_LINUX) and symbolsPath and os.path.exists(symbolsPath):
656 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
657 # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
658 sys.path.insert(0, utilityPath)
659 import fix_stack_using_bpsyms as stackFixerModule
660 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
661 del sys.path[0]
662 elif self.IS_DEBUG_BUILD and self.IS_MAC and False:
663 # Run each line through a function in fix_macosx_stack.py (uses atos)
664 sys.path.insert(0, utilityPath)
665 import fix_macosx_stack as stackFixerModule
666 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
667 del sys.path[0]
668 elif self.IS_DEBUG_BUILD and self.IS_LINUX:
669 # Run logsource through fix-linux-stack.pl (uses addr2line)
670 # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
671 stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")],
672 stdin=logsource,
673 stdout=subprocess.PIPE)
674 logsource = stackFixerProcess.stdout
676 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
677 hitMaxTime = False
678 while line != "" and not didTimeout:
679 if "TEST-START" in line and "|" in line:
680 self.lastTestSeen = line.split("|")[1].strip()
681 if stackFixerFunction:
682 line = stackFixerFunction(line)
683 self.log.info(line.rstrip())
684 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
685 if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
686 # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
687 hitMaxTime = True
688 self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
689 self.killAndGetStack(proc, utilityPath, debuggerInfo)
690 if didTimeout:
691 self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
692 self.killAndGetStack(proc, utilityPath, debuggerInfo)
694 status = proc.wait()
695 if status == 0:
696 self.lastTestSeen = "Main app process exited normally"
697 if status != 0 and not didTimeout and not hitMaxTime:
698 self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
699 if stackFixerProcess is not None:
700 fixerStatus = stackFixerProcess.wait()
701 if fixerStatus != 0 and not didTimeout and not hitMaxTime:
702 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
703 return status
705 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
706 """ build the application command line """
708 cmd = app
709 if self.IS_MAC and not self.IS_CAMINO and not cmd.endswith("-bin"):
710 cmd += "-bin"
711 cmd = os.path.abspath(cmd)
713 args = []
715 if debuggerInfo:
716 args.extend(debuggerInfo["args"])
717 args.append(cmd)
718 cmd = os.path.abspath(debuggerInfo["path"])
720 if self.IS_MAC:
721 args.append("-foreground")
723 if self.IS_CYGWIN:
724 profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
725 else:
726 profileDirectory = profileDir + "/"
728 args.extend(("-no-remote", "-profile", profileDirectory))
729 if testURL is not None:
730 if self.IS_CAMINO:
731 args.extend(("-url", testURL))
732 else:
733 args.append((testURL))
734 args.extend(extraArgs)
735 return cmd, args
737 def checkForZombies(self, processLog):
738 """ Look for hung processes """
739 if not os.path.exists(processLog):
740 self.log.info('INFO | automation.py | PID log not found: %s', processLog)
741 else:
742 self.log.info('INFO | automation.py | Reading PID log: %s', processLog)
743 processList = []
744 pidRE = re.compile(r'launched child process (\d+)$')
745 processLogFD = open(processLog)
746 for line in processLogFD:
747 self.log.info(line.rstrip())
748 m = pidRE.search(line)
749 if m:
750 processList.append(int(m.group(1)))
751 processLogFD.close()
753 for processPID in processList:
754 self.log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID)
755 if self.isPidAlive(processPID):
756 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
757 self.killPid(processPID)
759 def runApp(self, testURL, env, app, profileDir, extraArgs,
760 runSSLTunnel = False, utilityPath = None,
761 xrePath = None, certPath = None,
762 debuggerInfo = None, symbolsPath = None,
763 timeout = -1, maxTime = None):
765 Run the app, log the duration it took to execute, return the status code.
766 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
769 if utilityPath == None:
770 utilityPath = self.DIST_BIN
771 if xrePath == None:
772 xrePath = self.DIST_BIN
773 if certPath == None:
774 certPath = self.CERTS_SRC_DIR
775 if timeout == -1:
776 timeout = self.DEFAULT_TIMEOUT
778 # copy env so we don't munge the caller's environment
779 env = dict(env);
780 env["NO_EM_RESTART"] = "1"
781 tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
782 os.close(tmpfd)
783 env["MOZ_PROCESS_LOG"] = processLog
785 if self.IS_TEST_BUILD and runSSLTunnel:
786 # create certificate database for the profile
787 certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
788 if certificateStatus != 0:
789 self.log.info("TEST-UNEXPECTED FAIL | automation.py | Certificate integration failed")
790 return certificateStatus
792 # start ssltunnel to provide https:// URLs capability
793 ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
794 ssltunnelProcess = self.Process([ssltunnel,
795 os.path.join(profileDir, "ssltunnel.cfg")],
796 env = self.environment(xrePath = xrePath))
797 self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
799 cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
800 startTime = datetime.now()
802 if debuggerInfo and debuggerInfo["interactive"]:
803 # If an interactive debugger is attached, don't redirect output
804 # and don't use timeouts.
805 timeout = None
806 maxTime = None
807 outputPipe = None
808 else:
809 outputPipe = subprocess.PIPE
811 self.lastTestSeen = "automation.py"
812 proc = self.Process([cmd] + args,
813 env = self.environment(env, xrePath = xrePath,
814 crashreporter = not debuggerInfo),
815 stdout = outputPipe,
816 stderr = subprocess.STDOUT)
817 self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
819 status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
820 self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
822 # Do a final check for zombie child processes.
823 self.checkForZombies(processLog)
824 automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath, self.lastTestSeen)
826 if os.path.exists(processLog):
827 os.unlink(processLog)
829 if self.IS_TEST_BUILD and runSSLTunnel:
830 ssltunnelProcess.kill()
832 return status
834 """
835 Copies an "installed" extension into the extensions directory of the given profile
836 extensionSource - the source location of the extension files. This can be either
837 a directory or a path to an xpi file.
838 profileDir - the profile directory we are copying into. We will create the
839 "extensions" directory there if it doesn't exist
840 extensionID - the id of the extension to be used as the containing directory for the
841 extension, i.e.
842 this is the name of the folder in the <profileDir>/extensions/<extensionID>
844 def installExtension(self, extensionSource, profileDir, extensionID):
845 if (not os.path.exists(extensionSource)):
846 self.log.info("INFO | automation.py | Cannot install extension no source at: %s", extensionSource)
848 if (not os.path.exists(profileDir)):
849 self.log.info("INFO | automation.py | Cannot install extension invalid profileDir at: %s", profileDir)
851 # See if we have an XPI or a directory
852 if (os.path.isfile(extensionSource)):
853 tmpd = tempfile.mkdtemp()
854 extrootdir = self.extractZip(extensionSource, tmpd)
855 else:
856 extrootdir = extensionSource
857 extnsdir = os.path.join(profileDir, "extensions")
858 extnshome = os.path.join(extnsdir, extensionID)
860 # Now we copy the extension source into the extnshome
861 shutil.copytree(extrootdir, extnshome)
863 def extractZip(self, filename, dest):
864 z = zipfile.ZipFile(filename, 'r')
865 for n in z.namelist():
866 fullpath = os.path.join(dest, n)
867 parentdir = os.path.dirname(fullpath)
868 if not os.path.isdir(parentdir):
869 os.makedirs(parentdir)
870 if (not n.endswith(os.sep)):
871 data = z.read(n)
872 f = open(fullpath, 'w')
873 f.write(data)
874 f.close()
875 z.close()
876 return dest