Fix opt failures using gczeal. (r=Waldo)
[mozilla-central.git] / build / automation.py.in
blob5795b6bb1dfa260e07f78fb1b0ced2df4ecfad20
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
54 import sqlite3
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
69 #ifdef IS_CYGWIN
70 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
71 #else
72 _IS_CYGWIN = False
73 #endif
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
85 if _IS_WIN32:
86 import ctypes, ctypes.wintypes, time, msvcrt
87 else:
88 import errno
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)
100 #################
101 # PROFILE SETUP #
102 #################
104 class SyntaxError(Exception):
105 "Signifies a syntax error on a particular line in server-locations.txt."
107 def __init__(self, lineno, msg = None):
108 self.lineno = lineno
109 self.msg = msg
111 def __str__(self):
112 s = "Syntax error on line " + str(self.lineno)
113 if self.msg:
114 s += ": %s." % self.msg
115 else:
116 s += "."
117 return s
120 class Location:
121 "Represents a location line in server-locations.txt."
123 def __init__(self, scheme, host, port, options):
124 self.scheme = scheme
125 self.host = host
126 self.port = port
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.
135 DIST_BIN = _DIST_BIN
136 IS_WIN32 = _IS_WIN32
137 IS_MAC = _IS_MAC
138 IS_LINUX = _IS_LINUX
139 IS_CYGWIN = _IS_CYGWIN
140 IS_CAMINO = _IS_CAMINO
141 BIN_SUFFIX = _BIN_SUFFIX
142 PERL = _PERL
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
159 def __init__(self):
160 self.log = _log
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
174 @property
175 def __all__(self):
176 return [
177 "UNIXISH",
178 "IS_WIN32",
179 "IS_MAC",
180 "log",
181 "runApp",
182 "Process",
183 "addCommonOptions",
184 "initializeProfile",
185 "DIST_BIN",
186 "DEFAULT_APP",
187 "CERTS_SRC_DIR",
188 "environment",
189 "IS_TEST_BUILD",
190 "IS_DEBUG_BUILD",
191 "DEFAULT_TIMEOUT",
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.
200 def __init__(self,
201 args,
202 bufsize=0,
203 executable=None,
204 stdin=None,
205 stdout=None,
206 stderr=None,
207 preexec_fn=None,
208 close_fds=False,
209 shell=False,
210 cwd=None,
211 env=None,
212 universal_newlines=False,
213 startupinfo=None,
214 creationflags=0):
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,
220 shell, cwd, env,
221 universal_newlines, startupinfo, creationflags)
222 self.log = _log
224 def kill(self):
225 if Automation().IS_WIN32:
226 import platform
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.)
231 try:
232 subprocess.Popen(["kill", "-f", pid]).wait()
233 except:
234 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
235 else:
236 # Windows XP and later.
237 subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
238 else:
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+.]*)"
253 r"://"
254 r"(?P<host>"
255 r"\d+\.\d+\.\d+\.\d+"
256 r"|"
257 r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
258 r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
259 r")"
260 r":"
261 r"(?P<port>\d+)"
262 r"(?:"
263 r"\s+"
264 r"(?P<options>\S+(?:,\S+)*)"
265 r")?$")
266 locations = []
267 lineno = 0
268 seenPrimary = False
269 for line in locationFile:
270 lineno += 1
271 if line.startswith("#") or line == "\n":
272 continue
274 match = lineRe.match(line)
275 if not match:
276 raise SyntaxError(lineno)
278 options = match.group("options")
279 if options:
280 options = options.split(",")
281 if "primary" in options:
282 if seenPrimary:
283 raise SyntaxError(lineno, "multiple primary locations")
284 seenPrimary = True
285 else:
286 options = []
288 locations.append(Location(match.group("scheme"), match.group("host"),
289 match.group("port"), options))
291 if not seenPrimary:
292 raise SyntaxError(lineno + 1, "missing primary location")
294 return locations
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,
303 host TEXT,
304 type TEXT,
305 permission INTEGER,
306 expireType INTEGER,
307 expireTime INTEGER)""")
309 # Insert desired permissions
310 c = 0
311 for perm in permissions.keys():
312 for host,allow in permissions[perm]:
313 c += 1
314 cursor.execute("INSERT INTO moz_hosts values(?, ?, ?, ?, 0, 0)",
315 (c, host, perm, 1 if allow else 2))
317 # Commit and close
318 permDB.commit()
319 cursor.close()
321 def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
322 " Sets up the standard testing profile."
324 prefs = []
325 # Start with a clean slate.
326 shutil.rmtree(profileDir, True)
327 os.mkdir(profileDir)
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]});
334 part = """\
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) }
392 prefs.append(part)
394 if useServerLocations == False:
395 part = """
396 user_pref("capability.principal.codebase.p1.granted",
397 "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
398 UniversalPreferencesRead UniversalPreferencesWrite \
399 UniversalFileRead");
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) }
403 prefs.append(part)
404 else:
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):
408 part = """
409 user_pref("capability.principal.codebase.p%(i)d.granted",
410 "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
411 UniversalPreferencesRead UniversalPreferencesWrite \
412 UniversalFileRead");
413 user_pref("capability.principal.codebase.p%(i)d.id", "%(origin)s");
414 user_pref("capability.principal.codebase.p%(i)d.subjectName", "");
415 """ % { "i": i,
416 "origin": (l.scheme + "://" + l.host + ":" + str(l.port)) }
417 prefs.append(part)
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+.]*)' +
429 '://' +
430 '(?:[^/@]*@)?' +
431 '(.*?)' +
432 '(?::(\\\\\\\\d+))?/');
433 var matches = regex.exec(url);
434 if (!matches)
435 return 'DIRECT';
436 var isHttp = matches[1] == 'http';
437 var isHttps = matches[1] == 'https';
438 var isWebSocket = matches[1] == 'ws';
439 if (!matches[3])
441 if (isHttp | isWebSocket) matches[3] = '80';
442 if (isHttps) matches[3] = '443';
444 if (isWebSocket)
445 matches[1] = 'http';
447 var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
448 if (origins.indexOf(origin) < 0)
449 return 'DIRECT';
450 if (isHttp)
451 return 'PROXY %(remote)s:%(httpport)s';
452 if (isHttps || isWebSocket)
453 return 'PROXY %(remote)s:%(sslport)s';
454 return 'DIRECT';
455 }""" % { "origins": origins,
456 "remote": self.webServer,
457 "httpport":self.httpPort,
458 "sslport": self.sslPort }
459 pacURL = "".join(pacURL.splitlines())
461 part += """
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}
467 prefs.append(part)
469 for v in extraPrefs:
470 thispref = v.split("=")
471 if len(thispref) < 2:
472 print "Error: syntax error in --setpref=" + v
473 sys.exit(1)
474 part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
475 prefs.append(part)
477 # write the preferences
478 prefsFile = open(profileDir + "/" + "user.js", "a")
479 prefsFile.write("".join(prefs))
480 prefsFile.close()
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",
487 default = [],
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")
495 pwfile.write("\n")
496 pwfile.close()
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()
510 locations.pop(0)
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)
517 if match:
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)
523 if match:
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()
536 if status != 0:
537 return status
539 # Walk the cert directory and add custom CAs and client certs
540 files = os.listdir(certPath)
541 for item in files:
542 root, ext = os.path.splitext(item)
543 if ext == ".ca":
544 trustBits = "CT,,"
545 if root.endswith("-object"):
546 trustBits = "CT,,CT"
547 self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
548 "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
549 env = env).wait()
550 if ext == ".client":
551 self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
552 pwfilePath, "-d", profileDir],
553 env = env).wait()
555 os.unlink(pwfilePath)
556 return 0
558 def environment(self, env = None, xrePath = None, crashreporter = True):
559 if xrePath == None:
560 xrePath = self.DIST_BIN
561 if env == None:
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"
567 if self.IS_MAC:
568 envVar = "DYLD_LIBRARY_PATH"
569 else: # unixish
570 env['MOZILLA_FIVE_HOME'] = xrePath
571 if envVar in env:
572 ldLibraryPath = ldLibraryPath + ":" + env[envVar]
573 env[envVar] = ldLibraryPath
574 elif self.IS_WIN32:
575 env["PATH"] = env["PATH"] + ";" + ldLibraryPath
577 if crashreporter:
578 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
579 env['MOZ_CRASHREPORTER'] = '1'
580 else:
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.)
589 if not self.IS_MAC:
590 env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
591 return env
593 if IS_WIN32:
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."""
604 if timeout is None:
605 # shortcut to allow callers to pass in "None" for no timeout.
606 return (f.readline(), False)
607 x = msvcrt.get_osfhandle(f.fileno())
608 l = ctypes.c_long()
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
614 return ('', False)
615 else:
616 log.error("readWithTimeout got error: %d", err)
617 if l.value > 0:
618 # we're assuming that the output is line-buffered,
619 # which is not unreasonable
620 return (f.readline(), False)
621 time.sleep(0.01)
622 return ('', True)
624 def isPidAlive(self, pid):
625 STILL_ACTIVE = 259
626 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
627 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
628 if not pHandle:
629 return False
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):
634 return True
635 else:
636 return False
638 def killPid(self, pid):
639 PROCESS_TERMINATE = 0x0001
640 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
641 if not pHandle:
642 return
643 success = ctypes.windll.kernel32.TerminateProcess(pHandle, 1)
644 ctypes.windll.kernel32.CloseHandle(pHandle)
646 else:
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)
654 if len(r) == 0:
655 return ('', True)
656 return (f.readline(), False)
658 def isPidAlive(self, pid):
659 try:
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.
662 os.kill(pid, 0)
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)
667 if wpid == 0:
668 return True
670 return False
671 except OSError, err:
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:
675 return False
676 raise
678 def killPid(self, pid):
679 os.kill(pid, signal.SIGKILL)
681 if UNIXISH:
682 def dumpScreen(self, utilityPath):
683 self.haveDumpedScreen = True;
685 screentopng = os.path.join(utilityPath, "screentopng")
686 try:
687 dumper = self.Process([screentopng], bufsize=-1,
688 stdout=subprocess.PIPE, close_fds=True)
689 except OSError, err:
690 self.log.info("Failed to start %s for screenshot: %s",
691 screentopng, err.strerror)
692 return
694 image = dumper.stdout.read()
695 status = dumper.wait()
696 if status != 0:
697 self.log.info("screentopng exited with code %d", status)
698 return
700 import base64
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:
710 if self.UNIXISH:
711 # ABRT will get picked up by Breakpad's signal handler
712 os.kill(proc.pid, signal.SIGABRT)
713 return
714 elif self.IS_WIN32:
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:
718 return
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")
721 proc.kill()
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
727 didTimeout = False
728 if proc.stdout is None:
729 self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
730 else:
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)
739 del sys.path[0]
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)
745 del sys.path[0]
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")],
750 stdin=logsource,
751 stdout=subprocess.PIPE)
752 logsource = stackFixerProcess.stdout
754 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
755 hitMaxTime = False
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().
768 hitMaxTime = True
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)
771 if didTimeout:
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)
775 status = proc.wait()
776 if status == 0:
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)
784 return status
786 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
787 """ build the application command line """
789 cmd = app
790 if self.IS_MAC and not self.IS_CAMINO and not cmd.endswith("-bin"):
791 cmd += "-bin"
792 cmd = os.path.abspath(cmd)
794 args = []
796 if debuggerInfo:
797 args.extend(debuggerInfo["args"])
798 args.append(cmd)
799 cmd = os.path.abspath(debuggerInfo["path"])
801 if self.IS_MAC:
802 args.append("-foreground")
804 if self.IS_CYGWIN:
805 profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
806 else:
807 profileDirectory = profileDir + "/"
809 args.extend(("-no-remote", "-profile", profileDirectory))
810 if testURL is not None:
811 if self.IS_CAMINO:
812 args.extend(("-url", testURL))
813 else:
814 args.append((testURL))
815 args.extend(extraArgs)
816 return cmd, args
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)
822 else:
823 self.log.info('INFO | automation.py | Reading PID log: %s', processLog)
824 processList = []
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)
830 if m:
831 processList.append(int(m.group(1)))
832 processLogFD.close()
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
852 if xrePath == None:
853 xrePath = self.DIST_BIN
854 if certPath == None:
855 certPath = self.CERTS_SRC_DIR
856 if timeout == -1:
857 timeout = self.DEFAULT_TIMEOUT
859 # copy env so we don't munge the caller's environment
860 env = dict(env);
861 env["NO_EM_RESTART"] = "1"
862 tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
863 os.close(tmpfd)
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.
886 timeout = None
887 maxTime = None
888 outputPipe = None
889 signal.signal(signal.SIGINT, lambda sigid, frame: None)
890 else:
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),
897 stdout = outputPipe,
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()
914 return status
916 """
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
923 extension, i.e.
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)
929 return
931 if (not os.path.exists(profileDir)):
932 self.log.info("INFO | automation.py | Cannot install extension invalid profileDir at: %s", profileDir)
933 return
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)
939 else:
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)):
955 data = z.read(n)
956 f = open(fullpath, 'w')
957 f.write(data)
958 f.close()
959 z.close()
960 return dest