Bug 575870 - Enable the firefox button on xp themed, classic, and aero basic. r=dao...
[mozilla-central.git] / build / automation.py.in
blob26d7a518e650012a7383be89de5ff618766325af
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 subprocess.Popen.__init__(self, args, bufsize, executable,
216 stdin, stdout, stderr,
217 preexec_fn, close_fds,
218 shell, cwd, env,
219 universal_newlines, startupinfo, creationflags)
220 self.log = _log
222 def kill(self):
223 if Automation().IS_WIN32:
224 import platform
225 pid = "%i" % self.pid
226 if platform.release() == "2000":
227 # Windows 2000 needs 'kill.exe' from the
228 #'Windows 2000 Resource Kit tools'. (See bug 475455.)
229 try:
230 subprocess.Popen(["kill", "-f", pid]).wait()
231 except:
232 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
233 else:
234 # Windows XP and later.
235 subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
236 else:
237 os.kill(self.pid, signal.SIGKILL)
239 def readLocations(self, locationsPath = "server-locations.txt"):
241 Reads the locations at which the Mochitest HTTP server is available from
242 server-locations.txt.
245 locationFile = codecs.open(locationsPath, "r", "UTF-8")
247 # Perhaps more detail than necessary, but it's the easiest way to make sure
248 # we get exactly the format we want. See server-locations.txt for the exact
249 # format guaranteed here.
250 lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
251 r"://"
252 r"(?P<host>"
253 r"\d+\.\d+\.\d+\.\d+"
254 r"|"
255 r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
256 r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
257 r")"
258 r":"
259 r"(?P<port>\d+)"
260 r"(?:"
261 r"\s+"
262 r"(?P<options>\S+(?:,\S+)*)"
263 r")?$")
264 locations = []
265 lineno = 0
266 seenPrimary = False
267 for line in locationFile:
268 lineno += 1
269 if line.startswith("#") or line == "\n":
270 continue
272 match = lineRe.match(line)
273 if not match:
274 raise SyntaxError(lineno)
276 options = match.group("options")
277 if options:
278 options = options.split(",")
279 if "primary" in options:
280 if seenPrimary:
281 raise SyntaxError(lineno, "multiple primary locations")
282 seenPrimary = True
283 else:
284 options = []
286 locations.append(Location(match.group("scheme"), match.group("host"),
287 match.group("port"), options))
289 if not seenPrimary:
290 raise SyntaxError(lineno + 1, "missing primary location")
292 return locations
294 def setupPermissionsDatabase(self, profileDir, permissions):
295 # Open database and create table
296 permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite"))
297 cursor = permDB.cursor();
298 # SQL copied from nsPermissionManager.cpp
299 cursor.execute("""CREATE TABLE moz_hosts (
300 id INTEGER PRIMARY KEY,
301 host TEXT,
302 type TEXT,
303 permission INTEGER,
304 expireType INTEGER,
305 expireTime INTEGER)""")
307 # Insert desired permissions
308 c = 0
309 for perm in permissions.keys():
310 for host in permissions[perm]:
311 c += 1
312 cursor.execute("INSERT INTO moz_hosts values(?, ?, ?, 1, 0, 0)",
313 (c, host, perm))
315 # Commit and close
316 permDB.commit()
317 cursor.close()
319 def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
320 " Sets up the standard testing profile."
322 prefs = []
323 # Start with a clean slate.
324 shutil.rmtree(profileDir, True)
325 os.mkdir(profileDir)
327 # Set up permissions database
328 locations = self.readLocations()
329 self.setupPermissionsDatabase(profileDir,
330 {'allowXULXBL':map(lambda l: l.host, locations)});
332 part = """\
333 user_pref("browser.dom.window.dump.enabled", true);
334 user_pref("dom.allow_scripts_to_close_windows", true);
335 user_pref("dom.disable_open_during_load", false);
336 user_pref("dom.max_script_run_time", 0); // no slow script dialogs
337 user_pref("dom.max_chrome_script_run_time", 0);
338 user_pref("dom.popup_maximum", -1);
339 user_pref("signed.applets.codebase_principal_support", true);
340 user_pref("security.warn_submit_insecure", false);
341 user_pref("browser.shell.checkDefaultBrowser", false);
342 user_pref("shell.checkDefaultClient", false);
343 user_pref("browser.warnOnQuit", false);
344 user_pref("accessibility.typeaheadfind.autostart", false);
345 user_pref("javascript.options.showInConsole", true);
346 user_pref("layout.debug.enable_data_xbl", true);
347 user_pref("browser.EULA.override", true);
348 user_pref("javascript.options.jit.content", true);
349 user_pref("gfx.color_management.force_srgb", true);
350 user_pref("network.manage-offline-status", false);
351 user_pref("test.mousescroll", true);
352 user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs
353 user_pref("network.http.prompt-temp-redirect", false);
354 user_pref("media.cache_size", 100);
355 user_pref("security.warn_viewing_mixed", false);
356 user_pref("app.update.enabled", false);
358 // Only load extensions from the application and user profile
359 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
360 user_pref("extensions.enabledScopes", 5);
362 user_pref("extensions.testpilot.runStudies", false);
364 user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
365 user_pref("geo.wifi.testing", true);
367 user_pref("camino.warn_when_closing", false); // Camino-only, harmless to others
369 // Make url-classifier updates so rare that they won't affect tests
370 user_pref("urlclassifier.updateinterval", 172800);
371 // Point the url-classifier to the local testing server for fast failures
372 user_pref("browser.safebrowsing.provider.0.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
373 user_pref("browser.safebrowsing.provider.0.keyURL", "http://%(server)s/safebrowsing-dummy/newkey");
374 user_pref("browser.safebrowsing.provider.0.updateURL", "http://%(server)s/safebrowsing-dummy/update");
375 """ % { "server" : self.webServer + ":" + str(self.httpPort) }
376 prefs.append(part)
378 if useServerLocations == False:
379 part = """
380 user_pref("capability.principal.codebase.p1.granted",
381 "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
382 UniversalPreferencesRead UniversalPreferencesWrite \
383 UniversalFileRead");
384 user_pref("capability.principal.codebase.p1.id", "%(origin)s");
385 user_pref("capability.principal.codebase.p1.subjectName", "");
386 """ % { "origin": "http://" + self.webServer + ":" + str(self.httpPort) }
387 prefs.append(part)
388 else:
389 # Grant God-power to all the privileged servers on which tests run.
390 privileged = filter(lambda loc: "privileged" in loc.options, locations)
391 for (i, l) in itertools.izip(itertools.count(1), privileged):
392 part = """
393 user_pref("capability.principal.codebase.p%(i)d.granted",
394 "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
395 UniversalPreferencesRead UniversalPreferencesWrite \
396 UniversalFileRead");
397 user_pref("capability.principal.codebase.p%(i)d.id", "%(origin)s");
398 user_pref("capability.principal.codebase.p%(i)d.subjectName", "");
399 """ % { "i": i,
400 "origin": (l.scheme + "://" + l.host + ":" + str(l.port)) }
401 prefs.append(part)
403 # We need to proxy every server but the primary one.
404 origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port)
405 for l in filter(lambda l: "primary" not in l.options, locations)]
406 origins = ", ".join(origins)
408 pacURL = """data:text/plain,
409 function FindProxyForURL(url, host)
411 var origins = [%(origins)s];
412 var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
413 '://' +
414 '(?:[^/@]*@)?' +
415 '(.*?)' +
416 '(?::(\\\\\\\\d+))?/');
417 var matches = regex.exec(url);
418 if (!matches)
419 return 'DIRECT';
420 var isHttp = matches[1] == 'http';
421 var isHttps = matches[1] == 'https';
422 var isWebSocket = matches[1] == 'ws';
423 if (!matches[3])
425 if (isHttp | isWebSocket) matches[3] = '80';
426 if (isHttps) matches[3] = '443';
428 if (isWebSocket)
429 matches[1] = 'http';
431 var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
432 if (origins.indexOf(origin) < 0)
433 return 'DIRECT';
434 if (isHttp)
435 return 'PROXY %(remote)s:%(httpport)s';
436 if (isHttps || isWebSocket)
437 return 'PROXY %(remote)s:%(sslport)s';
438 return 'DIRECT';
439 }""" % { "origins": origins,
440 "remote": self.webServer,
441 "httpport":self.httpPort,
442 "sslport": self.sslPort }
443 pacURL = "".join(pacURL.splitlines())
445 part += """
446 user_pref("network.proxy.type", 2);
447 user_pref("network.proxy.autoconfig_url", "%(pacURL)s");
449 user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others
450 """ % {"pacURL": pacURL}
451 prefs.append(part)
453 for v in extraPrefs:
454 thispref = v.split("=")
455 if len(thispref) < 2:
456 print "Error: syntax error in --setpref=" + v
457 sys.exit(1)
458 part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
459 prefs.append(part)
461 # write the preferences
462 prefsFile = open(profileDir + "/" + "user.js", "a")
463 prefsFile.write("".join(prefs))
464 prefsFile.close()
466 def addCommonOptions(self, parser):
467 "Adds command-line options which are common to mochitest and reftest."
469 parser.add_option("--setpref",
470 action = "append", type = "string",
471 default = [],
472 dest = "extraPrefs", metavar = "PREF=VALUE",
473 help = "defines an extra user preference")
475 def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
476 pwfilePath = os.path.join(profileDir, ".crtdbpw")
478 pwfile = open(pwfilePath, "w")
479 pwfile.write("\n")
480 pwfile.close()
482 # Create head of the ssltunnel configuration file
483 sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
484 sslTunnelConfig = open(sslTunnelConfigPath, "w")
486 sslTunnelConfig.write("httpproxy:1\n")
487 sslTunnelConfig.write("certdbdir:%s\n" % certPath)
488 sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
489 sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
490 sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
492 # Configure automatic certificate and bind custom certificates, client authentication
493 locations = self.readLocations()
494 locations.pop(0)
495 for loc in locations:
496 if loc.scheme == "https" and "nocert" not in loc.options:
497 customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
498 clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
499 for option in loc.options:
500 match = customCertRE.match(option)
501 if match:
502 customcert = match.group("nickname");
503 sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
504 (loc.host, loc.port, self.sslPort, customcert))
506 match = clientAuthRE.match(option)
507 if match:
508 clientauth = match.group("clientauth");
509 sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
510 (loc.host, loc.port, self.sslPort, clientauth))
512 sslTunnelConfig.close()
514 # Pre-create the certification database for the profile
515 env = self.environment(xrePath = xrePath)
516 certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
517 pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)
519 status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
520 if status != 0:
521 return status
523 # Walk the cert directory and add custom CAs and client certs
524 files = os.listdir(certPath)
525 for item in files:
526 root, ext = os.path.splitext(item)
527 if ext == ".ca":
528 trustBits = "CT,,"
529 if root.endswith("-object"):
530 trustBits = "CT,,CT"
531 self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
532 "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
533 env = env).wait()
534 if ext == ".client":
535 self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
536 pwfilePath, "-d", profileDir],
537 env = env).wait()
539 os.unlink(pwfilePath)
540 return 0
542 def environment(self, env = None, xrePath = None, crashreporter = True):
543 if xrePath == None:
544 xrePath = self.DIST_BIN
545 if env == None:
546 env = dict(os.environ)
548 ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
549 if self.UNIXISH or self.IS_MAC:
550 envVar = "LD_LIBRARY_PATH"
551 if self.IS_MAC:
552 envVar = "DYLD_LIBRARY_PATH"
553 else: # unixish
554 env['MOZILLA_FIVE_HOME'] = xrePath
555 if envVar in env:
556 ldLibraryPath = ldLibraryPath + ":" + env[envVar]
557 env[envVar] = ldLibraryPath
558 elif self.IS_WIN32:
559 env["PATH"] = env["PATH"] + ";" + ldLibraryPath
561 if crashreporter:
562 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
563 env['MOZ_CRASHREPORTER'] = '1'
564 else:
565 env['MOZ_CRASHREPORTER_DISABLE'] = '1'
567 env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
568 env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
569 return env
571 if IS_WIN32:
572 PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
573 GetLastError = ctypes.windll.kernel32.GetLastError
575 def readWithTimeout(self, f, timeout):
576 """Try to read a line of output from the file object |f|.
577 |f| must be a pipe, like the |stdout| member of a subprocess.Popen
578 object created with stdout=PIPE. If no output
579 is received within |timeout| seconds, return a blank line.
580 Returns a tuple (line, did_timeout), where |did_timeout| is True
581 if the read timed out, and False otherwise."""
582 if timeout is None:
583 # shortcut to allow callers to pass in "None" for no timeout.
584 return (f.readline(), False)
585 x = msvcrt.get_osfhandle(f.fileno())
586 l = ctypes.c_long()
587 done = time.time() + timeout
588 while time.time() < done:
589 if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
590 err = self.GetLastError()
591 if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
592 return ('', False)
593 else:
594 log.error("readWithTimeout got error: %d", err)
595 if l.value > 0:
596 # we're assuming that the output is line-buffered,
597 # which is not unreasonable
598 return (f.readline(), False)
599 time.sleep(0.01)
600 return ('', True)
602 def isPidAlive(self, pid):
603 STILL_ACTIVE = 259
604 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
605 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
606 if not pHandle:
607 return False
608 pExitCode = ctypes.wintypes.DWORD()
609 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
610 ctypes.windll.kernel32.CloseHandle(pHandle)
611 if (pExitCode.value == STILL_ACTIVE):
612 return True
613 else:
614 return False
616 def killPid(self, pid):
617 PROCESS_TERMINATE = 0x0001
618 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
619 if not pHandle:
620 return
621 success = ctypes.windll.kernel32.TerminateProcess(pHandle, 1)
622 ctypes.windll.kernel32.CloseHandle(pHandle)
624 else:
626 def readWithTimeout(self, f, timeout):
627 """Try to read a line of output from the file object |f|. If no output
628 is received within |timeout| seconds, return a blank line.
629 Returns a tuple (line, did_timeout), where |did_timeout| is True
630 if the read timed out, and False otherwise."""
631 (r, w, e) = select.select([f], [], [], timeout)
632 if len(r) == 0:
633 return ('', True)
634 return (f.readline(), False)
636 def isPidAlive(self, pid):
637 try:
638 # kill(pid, 0) checks for a valid PID without actually sending a signal
639 # The method throws OSError if the PID is invalid, which we catch below.
640 os.kill(pid, 0)
642 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
643 # the process terminates before we get to this point.
644 wpid, wstatus = os.waitpid(pid, os.WNOHANG)
645 if wpid == 0:
646 return True
648 return False
649 except OSError, err:
650 # Catch the errors we might expect from os.kill/os.waitpid,
651 # and re-raise any others
652 if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
653 return False
654 raise
656 def killPid(self, pid):
657 os.kill(pid, signal.SIGKILL)
659 if UNIXISH:
660 def dumpScreen(self, utilityPath):
661 self.haveDumpedScreen = True;
663 screentopng = os.path.join(utilityPath, "screentopng")
664 try:
665 dumper = self.Process([screentopng], bufsize=-1,
666 stdout=subprocess.PIPE, close_fds=True)
667 except OSError, err:
668 self.log.info("Failed to start %s for screenshot: %s",
669 screentopng, err.strerror)
670 return
672 image = dumper.stdout.read()
673 status = dumper.wait()
674 if status != 0:
675 self.log.info("screentopng exited with code %d", status)
676 return
678 import base64
679 encoded = base64.b64encode(image)
680 self.log.info("SCREENSHOT: data:image/png;base64,%s", encoded)
682 def killAndGetStack(self, proc, utilityPath, debuggerInfo):
683 """Kill the process, preferrably in a way that gets us a stack trace."""
684 if self.UNIXISH and not debuggerInfo and not self.haveDumpedScreen:
685 self.dumpScreen(utilityPath)
687 if self.CRASHREPORTER and not debuggerInfo:
688 if self.UNIXISH:
689 # ABRT will get picked up by Breakpad's signal handler
690 os.kill(proc.pid, signal.SIGABRT)
691 return
692 elif self.IS_WIN32:
693 # We should have a "crashinject" program in our utility path
694 crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
695 if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
696 return
697 #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
698 self.log.info("Can't trigger Breakpad, just killing process")
699 proc.kill()
701 def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
702 """ Look for timeout or crashes and return the status after the process terminates """
703 stackFixerProcess = None
704 stackFixerFunction = None
705 didTimeout = False
706 if proc.stdout is None:
707 self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
708 else:
709 logsource = proc.stdout
711 if self.IS_DEBUG_BUILD and (self.IS_MAC or self.IS_LINUX) and symbolsPath and os.path.exists(symbolsPath):
712 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
713 # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
714 sys.path.insert(0, utilityPath)
715 import fix_stack_using_bpsyms as stackFixerModule
716 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
717 del sys.path[0]
718 elif self.IS_DEBUG_BUILD and self.IS_MAC and False:
719 # Run each line through a function in fix_macosx_stack.py (uses atos)
720 sys.path.insert(0, utilityPath)
721 import fix_macosx_stack as stackFixerModule
722 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
723 del sys.path[0]
724 elif self.IS_DEBUG_BUILD and self.IS_LINUX:
725 # Run logsource through fix-linux-stack.pl (uses addr2line)
726 # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
727 stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")],
728 stdin=logsource,
729 stdout=subprocess.PIPE)
730 logsource = stackFixerProcess.stdout
732 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
733 hitMaxTime = False
734 while line != "" and not didTimeout:
735 if "TEST-START" in line and "|" in line:
736 self.lastTestSeen = line.split("|")[1].strip()
737 if stackFixerFunction:
738 line = stackFixerFunction(line)
739 self.log.info(line.rstrip())
740 if self.UNIXISH and not debuggerInfo and not self.haveDumpedScreen and "TEST-UNEXPECTED-FAIL" in line and "Test timed out." in line:
741 self.dumpScreen(utilityPath)
743 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
744 if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
745 # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
746 hitMaxTime = True
747 self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
748 self.killAndGetStack(proc, utilityPath, debuggerInfo)
749 if didTimeout:
750 self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
751 self.killAndGetStack(proc, utilityPath, debuggerInfo)
753 status = proc.wait()
754 if status == 0:
755 self.lastTestSeen = "Main app process exited normally"
756 if status != 0 and not didTimeout and not hitMaxTime:
757 self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
758 if stackFixerProcess is not None:
759 fixerStatus = stackFixerProcess.wait()
760 if fixerStatus != 0 and not didTimeout and not hitMaxTime:
761 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
762 return status
764 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
765 """ build the application command line """
767 cmd = app
768 if self.IS_MAC and not self.IS_CAMINO and not cmd.endswith("-bin"):
769 cmd += "-bin"
770 cmd = os.path.abspath(cmd)
772 args = []
774 if debuggerInfo:
775 args.extend(debuggerInfo["args"])
776 args.append(cmd)
777 cmd = os.path.abspath(debuggerInfo["path"])
779 if self.IS_MAC:
780 args.append("-foreground")
782 if self.IS_CYGWIN:
783 profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
784 else:
785 profileDirectory = profileDir + "/"
787 args.extend(("-no-remote", "-profile", profileDirectory))
788 if testURL is not None:
789 if self.IS_CAMINO:
790 args.extend(("-url", testURL))
791 else:
792 args.append((testURL))
793 args.extend(extraArgs)
794 return cmd, args
796 def checkForZombies(self, processLog):
797 """ Look for hung processes """
798 if not os.path.exists(processLog):
799 self.log.info('INFO | automation.py | PID log not found: %s', processLog)
800 else:
801 self.log.info('INFO | automation.py | Reading PID log: %s', processLog)
802 processList = []
803 pidRE = re.compile(r'launched child process (\d+)$')
804 processLogFD = open(processLog)
805 for line in processLogFD:
806 self.log.info(line.rstrip())
807 m = pidRE.search(line)
808 if m:
809 processList.append(int(m.group(1)))
810 processLogFD.close()
812 for processPID in processList:
813 self.log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID)
814 if self.isPidAlive(processPID):
815 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
816 self.killPid(processPID)
818 def runApp(self, testURL, env, app, profileDir, extraArgs,
819 runSSLTunnel = False, utilityPath = None,
820 xrePath = None, certPath = None,
821 debuggerInfo = None, symbolsPath = None,
822 timeout = -1, maxTime = None):
824 Run the app, log the duration it took to execute, return the status code.
825 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
828 if utilityPath == None:
829 utilityPath = self.DIST_BIN
830 if xrePath == None:
831 xrePath = self.DIST_BIN
832 if certPath == None:
833 certPath = self.CERTS_SRC_DIR
834 if timeout == -1:
835 timeout = self.DEFAULT_TIMEOUT
837 # copy env so we don't munge the caller's environment
838 env = dict(env);
839 env["NO_EM_RESTART"] = "1"
840 tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
841 os.close(tmpfd)
842 env["MOZ_PROCESS_LOG"] = processLog
844 if self.IS_TEST_BUILD and runSSLTunnel:
845 # create certificate database for the profile
846 certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
847 if certificateStatus != 0:
848 self.log.info("TEST-UNEXPECTED FAIL | automation.py | Certificate integration failed")
849 return certificateStatus
851 # start ssltunnel to provide https:// URLs capability
852 ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
853 ssltunnelProcess = self.Process([ssltunnel,
854 os.path.join(profileDir, "ssltunnel.cfg")],
855 env = self.environment(xrePath = xrePath))
856 self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
858 cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
859 startTime = datetime.now()
861 if debuggerInfo and debuggerInfo["interactive"]:
862 # If an interactive debugger is attached, don't redirect output
863 # and don't use timeouts.
864 timeout = None
865 maxTime = None
866 outputPipe = None
867 else:
868 outputPipe = subprocess.PIPE
870 self.lastTestSeen = "automation.py"
871 proc = self.Process([cmd] + args,
872 env = self.environment(env, xrePath = xrePath,
873 crashreporter = not debuggerInfo),
874 stdout = outputPipe,
875 stderr = subprocess.STDOUT)
876 self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
878 status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
879 self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
881 # Do a final check for zombie child processes.
882 self.checkForZombies(processLog)
883 automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath, self.lastTestSeen)
885 if os.path.exists(processLog):
886 os.unlink(processLog)
888 if self.IS_TEST_BUILD and runSSLTunnel:
889 ssltunnelProcess.kill()
891 return status
893 """
894 Copies an "installed" extension into the extensions directory of the given profile
895 extensionSource - the source location of the extension files. This can be either
896 a directory or a path to an xpi file.
897 profileDir - the profile directory we are copying into. We will create the
898 "extensions" directory there if it doesn't exist
899 extensionID - the id of the extension to be used as the containing directory for the
900 extension, i.e.
901 this is the name of the folder in the <profileDir>/extensions/<extensionID>
903 def installExtension(self, extensionSource, profileDir, extensionID):
904 if (not os.path.exists(extensionSource)):
905 self.log.info("INFO | automation.py | Cannot install extension no source at: %s", extensionSource)
907 if (not os.path.exists(profileDir)):
908 self.log.info("INFO | automation.py | Cannot install extension invalid profileDir at: %s", profileDir)
910 # See if we have an XPI or a directory
911 if (os.path.isfile(extensionSource)):
912 tmpd = tempfile.mkdtemp()
913 extrootdir = self.extractZip(extensionSource, tmpd)
914 else:
915 extrootdir = extensionSource
916 extnsdir = os.path.join(profileDir, "extensions")
917 extnshome = os.path.join(extnsdir, extensionID)
919 # Now we copy the extension source into the extnshome
920 shutil.copytree(extrootdir, extnshome)
922 def extractZip(self, filename, dest):
923 z = zipfile.ZipFile(filename, 'r')
924 for n in z.namelist():
925 fullpath = os.path.join(dest, n)
926 parentdir = os.path.dirname(fullpath)
927 if not os.path.isdir(parentdir):
928 os.makedirs(parentdir)
929 if (not n.endswith(os.sep)):
930 data = z.read(n)
931 f = open(fullpath, 'w')
932 f.write(data)
933 f.close()
934 z.close()
935 return dest