Bumping manifests a=b2g-bump
[gecko.git] / build / automation.py.in
blob2d46460ce54230a041905df9d20b1485fcebcdab
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 from __future__ import with_statement
7 import codecs
8 import itertools
9 import json
10 import logging
11 import os
12 import re
13 import select
14 import shutil
15 import signal
16 import subprocess
17 import sys
18 import threading
19 import tempfile
20 import sqlite3
21 import zipfile
22 from datetime import datetime, timedelta
23 from string import Template
25 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
26 sys.path.insert(0, SCRIPT_DIR)
27 import automationutils
29 # --------------------------------------------------------------
30 # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900
31 # These paths refer to relative locations to test.zip, not the OBJDIR or SRCDIR
32 here = os.path.dirname(os.path.realpath(__file__))
33 mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase'))
35 if os.path.isdir(mozbase):
36 for package in os.listdir(mozbase):
37 package_path = os.path.join(mozbase, package)
38 if package_path not in sys.path:
39 sys.path.append(package_path)
41 import mozcrash
42 from mozprofile import Profile, Preferences
43 from mozprofile.permissions import ServerLocations
45 # ---------------------------------------------------------------
47 _DEFAULT_PREFERENCE_FILE = os.path.join(SCRIPT_DIR, 'prefs_general.js')
48 _DEFAULT_APPS_FILE = os.path.join(SCRIPT_DIR, 'webapps_mochitest.json')
50 _DEFAULT_WEB_SERVER = "127.0.0.1"
51 _DEFAULT_HTTP_PORT = 8888
52 _DEFAULT_SSL_PORT = 4443
53 _DEFAULT_WEBSOCKET_PORT = 9988
55 # from nsIPrincipal.idl
56 _APP_STATUS_NOT_INSTALLED = 0
57 _APP_STATUS_INSTALLED = 1
58 _APP_STATUS_PRIVILEGED = 2
59 _APP_STATUS_CERTIFIED = 3
61 #expand _DIST_BIN = __XPC_BIN_PATH__
62 #expand _IS_WIN32 = len("__WIN32__") != 0
63 #expand _IS_MAC = __IS_MAC__ != 0
64 #expand _IS_LINUX = __IS_LINUX__ != 0
65 #ifdef IS_CYGWIN
66 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
67 #else
68 _IS_CYGWIN = False
69 #endif
70 #expand _BIN_SUFFIX = __BIN_SUFFIX__
72 #expand _DEFAULT_APP = "./" + __BROWSER_PATH__
73 #expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
74 #expand _IS_TEST_BUILD = __IS_TEST_BUILD__
75 #expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
76 #expand _CRASHREPORTER = __CRASHREPORTER__ == 1
77 #expand _IS_ASAN = __IS_ASAN__ == 1
80 if _IS_WIN32:
81 import ctypes, ctypes.wintypes, time, msvcrt
82 else:
83 import errno
86 def getGlobalLog():
87 return _log
89 def resetGlobalLog(log):
90 while _log.handlers:
91 _log.removeHandler(_log.handlers[0])
92 handler = logging.StreamHandler(log)
93 _log.setLevel(logging.INFO)
94 _log.addHandler(handler)
96 # We use the logging system here primarily because it'll handle multiple
97 # threads, which is needed to process the output of the server and application
98 # processes simultaneously.
99 _log = logging.getLogger()
100 resetGlobalLog(sys.stdout)
103 #################
104 # PROFILE SETUP #
105 #################
107 class SyntaxError(Exception):
108 "Signifies a syntax error on a particular line in server-locations.txt."
110 def __init__(self, lineno, msg = None):
111 self.lineno = lineno
112 self.msg = msg
114 def __str__(self):
115 s = "Syntax error on line " + str(self.lineno)
116 if self.msg:
117 s += ": %s." % self.msg
118 else:
119 s += "."
120 return s
123 class Location:
124 "Represents a location line in server-locations.txt."
126 def __init__(self, scheme, host, port, options):
127 self.scheme = scheme
128 self.host = host
129 self.port = port
130 self.options = options
132 class Automation(object):
134 Runs the browser from a script, and provides useful utilities
135 for setting up the browser environment.
138 DIST_BIN = _DIST_BIN
139 IS_WIN32 = _IS_WIN32
140 IS_MAC = _IS_MAC
141 IS_LINUX = _IS_LINUX
142 IS_CYGWIN = _IS_CYGWIN
143 BIN_SUFFIX = _BIN_SUFFIX
145 UNIXISH = not IS_WIN32 and not IS_MAC
147 DEFAULT_APP = _DEFAULT_APP
148 CERTS_SRC_DIR = _CERTS_SRC_DIR
149 IS_TEST_BUILD = _IS_TEST_BUILD
150 IS_DEBUG_BUILD = _IS_DEBUG_BUILD
151 CRASHREPORTER = _CRASHREPORTER
152 IS_ASAN = _IS_ASAN
154 # timeout, in seconds
155 DEFAULT_TIMEOUT = 60.0
156 DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
157 DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
158 DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
159 DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
161 def __init__(self):
162 self.log = _log
163 self.lastTestSeen = "automation.py"
164 self.haveDumpedScreen = False
166 def setServerInfo(self,
167 webServer = _DEFAULT_WEB_SERVER,
168 httpPort = _DEFAULT_HTTP_PORT,
169 sslPort = _DEFAULT_SSL_PORT,
170 webSocketPort = _DEFAULT_WEBSOCKET_PORT):
171 self.webServer = webServer
172 self.httpPort = httpPort
173 self.sslPort = sslPort
174 self.webSocketPort = webSocketPort
176 @property
177 def __all__(self):
178 return [
179 "UNIXISH",
180 "IS_WIN32",
181 "IS_MAC",
182 "log",
183 "runApp",
184 "Process",
185 "initializeProfile",
186 "DIST_BIN",
187 "DEFAULT_APP",
188 "CERTS_SRC_DIR",
189 "environment",
190 "IS_TEST_BUILD",
191 "IS_DEBUG_BUILD",
192 "DEFAULT_TIMEOUT",
195 class Process(subprocess.Popen):
197 Represents our view of a subprocess.
198 It adds a kill() method which allows it to be stopped explicitly.
201 def __init__(self,
202 args,
203 bufsize=0,
204 executable=None,
205 stdin=None,
206 stdout=None,
207 stderr=None,
208 preexec_fn=None,
209 close_fds=False,
210 shell=False,
211 cwd=None,
212 env=None,
213 universal_newlines=False,
214 startupinfo=None,
215 creationflags=0):
216 _log.info("INFO | automation.py | Launching: %s", subprocess.list2cmdline(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 # Included for reftest compatibility;
298 # see https://bugzilla.mozilla.org/show_bug.cgi?id=688667
300 # Open database and create table
301 permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite"))
302 cursor = permDB.cursor();
304 cursor.execute("PRAGMA user_version=4");
306 # SQL copied from nsPermissionManager.cpp
307 cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
308 id INTEGER PRIMARY KEY,
309 host TEXT,
310 type TEXT,
311 permission INTEGER,
312 expireType INTEGER,
313 expireTime INTEGER,
314 modificationTime INTEGER,
315 appId INTEGER,
316 isInBrowserElement INTEGER)""")
318 # Insert desired permissions
319 for perm in permissions.keys():
320 for host,allow in permissions[perm]:
321 cursor.execute("INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0, 0)",
322 (host, perm, 1 if allow else 2))
324 # Commit and close
325 permDB.commit()
326 cursor.close()
328 def initializeProfile(self, profileDir,
329 extraPrefs=None,
330 useServerLocations=False,
331 prefsPath=_DEFAULT_PREFERENCE_FILE,
332 appsPath=_DEFAULT_APPS_FILE,
333 addons=None):
334 " Sets up the standard testing profile."
336 extraPrefs = extraPrefs or []
338 # create the profile
339 prefs = {}
340 locations = None
341 if useServerLocations:
342 locations = ServerLocations()
343 locations.read(os.path.abspath('server-locations.txt'), True)
344 else:
345 prefs['network.proxy.type'] = 0
347 prefs.update(Preferences.read_prefs(prefsPath))
349 for v in extraPrefs:
350 thispref = v.split("=", 1)
351 if len(thispref) < 2:
352 print "Error: syntax error in --setpref=" + v
353 sys.exit(1)
354 prefs[thispref[0]] = thispref[1]
357 interpolation = {"server": "%s:%s" % (self.webServer, self.httpPort)}
358 prefs = json.loads(json.dumps(prefs) % interpolation)
359 for pref in prefs:
360 prefs[pref] = Preferences.cast(prefs[pref])
362 # load apps
363 apps = None
364 if appsPath and os.path.exists(appsPath):
365 with open(appsPath, 'r') as apps_file:
366 apps = json.load(apps_file)
368 proxy = {'remote': str(self.webServer),
369 'http': str(self.httpPort),
370 'https': str(self.sslPort),
371 # use SSL port for legacy compatibility; see
372 # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
373 # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
374 # 'ws': str(self.webSocketPort)
375 'ws': str(self.sslPort)
378 # return profile object
379 profile = Profile(profile=profileDir,
380 addons=addons,
381 locations=locations,
382 preferences=prefs,
383 restore=False,
384 apps=apps,
385 proxy=proxy)
386 return profile
388 def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
389 pwfilePath = os.path.join(profileDir, ".crtdbpw")
390 pwfile = open(pwfilePath, "w")
391 pwfile.write("\n")
392 pwfile.close()
394 # Create head of the ssltunnel configuration file
395 sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
396 sslTunnelConfig = open(sslTunnelConfigPath, "w")
398 sslTunnelConfig.write("httpproxy:1\n")
399 sslTunnelConfig.write("certdbdir:%s\n" % certPath)
400 sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
401 sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
402 sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
404 # Configure automatic certificate and bind custom certificates, client authentication
405 locations = self.readLocations()
406 locations.pop(0)
407 for loc in locations:
408 if loc.scheme == "https" and "nocert" not in loc.options:
409 customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
410 clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
411 redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
412 for option in loc.options:
413 match = customCertRE.match(option)
414 if match:
415 customcert = match.group("nickname");
416 sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
417 (loc.host, loc.port, self.sslPort, customcert))
419 match = clientAuthRE.match(option)
420 if match:
421 clientauth = match.group("clientauth");
422 sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
423 (loc.host, loc.port, self.sslPort, clientauth))
425 match = redirRE.match(option)
426 if match:
427 redirhost = match.group("redirhost")
428 sslTunnelConfig.write("redirhost:%s:%s:%s:%s\n" %
429 (loc.host, loc.port, self.sslPort, redirhost))
431 sslTunnelConfig.close()
433 # Pre-create the certification database for the profile
434 env = self.environment(xrePath = xrePath)
435 certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
436 pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)
438 status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
439 automationutils.printstatus(status, "certutil")
440 if status != 0:
441 return status
443 # Walk the cert directory and add custom CAs and client certs
444 files = os.listdir(certPath)
445 for item in files:
446 root, ext = os.path.splitext(item)
447 if ext == ".ca":
448 trustBits = "CT,,"
449 if root.endswith("-object"):
450 trustBits = "CT,,CT"
451 status = self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
452 "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
453 env = env).wait()
454 automationutils.printstatus(status, "certutil")
455 if ext == ".client":
456 status = self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
457 pwfilePath, "-d", profileDir],
458 env = env).wait()
459 automationutils.printstatus(status, "pk12util")
461 os.unlink(pwfilePath)
462 return 0
464 def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None, lsanPath=None):
465 if xrePath == None:
466 xrePath = self.DIST_BIN
467 if env == None:
468 env = dict(os.environ)
470 ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
471 dmdLibrary = None
472 preloadEnvVar = None
473 if self.UNIXISH or self.IS_MAC:
474 envVar = "LD_LIBRARY_PATH"
475 preloadEnvVar = "LD_PRELOAD"
476 if self.IS_MAC:
477 envVar = "DYLD_LIBRARY_PATH"
478 dmdLibrary = "libdmd.dylib"
479 else: # unixish
480 env['MOZILLA_FIVE_HOME'] = xrePath
481 dmdLibrary = "libdmd.so"
482 if envVar in env:
483 ldLibraryPath = ldLibraryPath + ":" + env[envVar]
484 env[envVar] = ldLibraryPath
485 elif self.IS_WIN32:
486 env["PATH"] = env["PATH"] + ";" + str(ldLibraryPath)
487 dmdLibrary = "dmd.dll"
488 preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB"
490 if dmdPath and dmdLibrary and preloadEnvVar:
491 env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary)
493 if crashreporter and not debugger:
494 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
495 env['MOZ_CRASHREPORTER'] = '1'
496 else:
497 env['MOZ_CRASHREPORTER_DISABLE'] = '1'
499 # Crash on non-local network connections by default.
500 # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
501 # enable non-local connections for the purposes of local testing. Don't
502 # override the user's choice here. See bug 1049688.
503 env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1')
505 env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
506 env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
508 # Set WebRTC logging in case it is not set yet
509 env.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:5,datachannel:5,jsep:5,MediaPipelineFactory:5')
510 env.setdefault('R_LOG_LEVEL', '6')
511 env.setdefault('R_LOG_DESTINATION', 'stderr')
512 env.setdefault('R_LOG_VERBOSE', '1')
514 # ASan specific environment stuff
515 if self.IS_ASAN and (self.IS_LINUX or self.IS_MAC):
516 # Symbolizer support
517 llvmsym = os.path.join(xrePath, "llvm-symbolizer")
518 if os.path.isfile(llvmsym):
519 env["ASAN_SYMBOLIZER_PATH"] = llvmsym
520 self.log.info("INFO | automation.py | ASan using symbolizer at %s", llvmsym)
521 else:
522 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Failed to find ASan symbolizer at %s", llvmsym)
524 try:
525 totalMemory = int(os.popen("free").readlines()[1].split()[1])
527 # Only 4 GB RAM or less available? Use custom ASan options to reduce
528 # the amount of resources required to do the tests. Standard options
529 # will otherwise lead to OOM conditions on the current test slaves.
530 if totalMemory <= 1024 * 1024 * 4:
531 self.log.info("INFO | automation.py | ASan running in low-memory configuration")
532 env["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5"
533 else:
534 self.log.info("INFO | automation.py | ASan running in default memory configuration")
535 except OSError,err:
536 self.log.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err.strerror)
537 except:
538 self.log.info("Failed determine available memory, disabling ASan low-memory configuration")
540 return env
542 def killPid(self, pid):
543 try:
544 os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
545 except WindowsError:
546 self.log.info("Failed to kill process %d." % pid)
548 if IS_WIN32:
549 PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
550 GetLastError = ctypes.windll.kernel32.GetLastError
552 def readWithTimeout(self, f, timeout):
554 Try to read a line of output from the file object |f|. |f| must be a
555 pipe, like the |stdout| member of a subprocess.Popen object created
556 with stdout=PIPE. Returns a tuple (line, did_timeout), where |did_timeout|
557 is True if the read timed out, and False otherwise. If no output is
558 received within |timeout| seconds, returns a blank line.
561 if timeout is None:
562 timeout = 0
564 x = msvcrt.get_osfhandle(f.fileno())
565 l = ctypes.c_long()
566 done = time.time() + timeout
568 buffer = ""
569 while timeout == 0 or time.time() < done:
570 if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
571 err = self.GetLastError()
572 if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
573 return ('', False)
574 else:
575 self.log.error("readWithTimeout got error: %d", err)
576 # read a character at a time, checking for eol. Return once we get there.
577 index = 0
578 while index < l.value:
579 char = f.read(1)
580 buffer += char
581 if char == '\n':
582 return (buffer, False)
583 index = index + 1
584 time.sleep(0.01)
585 return (buffer, True)
587 def isPidAlive(self, pid):
588 STILL_ACTIVE = 259
589 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
590 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
591 if not pHandle:
592 return False
593 pExitCode = ctypes.wintypes.DWORD()
594 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
595 ctypes.windll.kernel32.CloseHandle(pHandle)
596 return pExitCode.value == STILL_ACTIVE
598 else:
600 def readWithTimeout(self, f, timeout):
601 """Try to read a line of output from the file object |f|. If no output
602 is received within |timeout| seconds, return a blank line.
603 Returns a tuple (line, did_timeout), where |did_timeout| is True
604 if the read timed out, and False otherwise."""
605 (r, w, e) = select.select([f], [], [], timeout)
606 if len(r) == 0:
607 return ('', True)
608 return (f.readline(), False)
610 def isPidAlive(self, pid):
611 try:
612 # kill(pid, 0) checks for a valid PID without actually sending a signal
613 # The method throws OSError if the PID is invalid, which we catch below.
614 os.kill(pid, 0)
616 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
617 # the process terminates before we get to this point.
618 wpid, wstatus = os.waitpid(pid, os.WNOHANG)
619 return wpid == 0
620 except OSError, err:
621 # Catch the errors we might expect from os.kill/os.waitpid,
622 # and re-raise any others
623 if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
624 return False
625 raise
627 def dumpScreen(self, utilityPath):
628 if self.haveDumpedScreen:
629 self.log.info("Not taking screenshot here: see the one that was previously logged")
630 return
632 self.haveDumpedScreen = True;
633 automationutils.dumpScreen(utilityPath)
636 def killAndGetStack(self, processPID, utilityPath, debuggerInfo):
637 """Kill the process, preferrably in a way that gets us a stack trace.
638 Also attempts to obtain a screenshot before killing the process."""
639 if not debuggerInfo:
640 self.dumpScreen(utilityPath)
641 self.killAndGetStackNoScreenshot(processPID, utilityPath, debuggerInfo)
643 def killAndGetStackNoScreenshot(self, processPID, utilityPath, debuggerInfo):
644 """Kill the process, preferrably in a way that gets us a stack trace."""
645 if self.CRASHREPORTER and not debuggerInfo:
646 if not self.IS_WIN32:
647 # ABRT will get picked up by Breakpad's signal handler
648 os.kill(processPID, signal.SIGABRT)
649 return
650 else:
651 # We should have a "crashinject" program in our utility path
652 crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
653 if os.path.exists(crashinject):
654 status = subprocess.Popen([crashinject, str(processPID)]).wait()
655 automationutils.printstatus(status, "crashinject")
656 if status == 0:
657 return
658 self.log.info("Can't trigger Breakpad, just killing process")
659 self.killPid(processPID)
661 def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
662 """ Look for timeout or crashes and return the status after the process terminates """
663 stackFixerFunction = None
664 didTimeout = False
665 hitMaxTime = False
666 if proc.stdout is None:
667 self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
668 else:
669 logsource = proc.stdout
671 if self.IS_DEBUG_BUILD and symbolsPath and os.path.exists(symbolsPath):
672 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
673 # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
674 sys.path.insert(0, utilityPath)
675 import fix_stack_using_bpsyms as stackFixerModule
676 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
677 del sys.path[0]
678 elif self.IS_DEBUG_BUILD and self.IS_MAC:
679 # Run each line through a function in fix_macosx_stack.py (uses atos)
680 sys.path.insert(0, utilityPath)
681 import fix_macosx_stack as stackFixerModule
682 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
683 del sys.path[0]
684 elif self.IS_DEBUG_BUILD and self.IS_LINUX:
685 # Run each line through a function in fix_linux_stack.py (uses addr2line)
686 # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
687 sys.path.insert(0, utilityPath)
688 import fix_linux_stack as stackFixerModule
689 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
690 del sys.path[0]
692 # With metro browser runs this script launches the metro test harness which launches the browser.
693 # The metro test harness hands back the real browser process id via log output which we need to
694 # pick up on and parse out. This variable tracks the real browser process id if we find it.
695 browserProcessId = -1
697 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
698 while line != "" and not didTimeout:
699 if stackFixerFunction:
700 line = stackFixerFunction(line)
701 self.log.info(line.rstrip().decode("UTF-8", "ignore"))
702 if "TEST-START" in line and "|" in line:
703 self.lastTestSeen = line.split("|")[1].strip()
704 if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
705 self.dumpScreen(utilityPath)
707 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
709 if "METRO_BROWSER_PROCESS" in line:
710 index = line.find("=")
711 if index:
712 browserProcessId = line[index+1:].rstrip()
713 self.log.info("INFO | automation.py | metro browser sub process id detected: %s", browserProcessId)
715 if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
716 # Kill the application.
717 hitMaxTime = True
718 self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
719 self.killAndGetStack(proc.pid, utilityPath, debuggerInfo)
720 if didTimeout:
721 if line:
722 self.log.info(line.rstrip().decode("UTF-8", "ignore"))
723 self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
724 if browserProcessId == -1:
725 browserProcessId = proc.pid
726 self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo)
728 status = proc.wait()
729 automationutils.printstatus(status, "Main app process")
730 if status == 0:
731 self.lastTestSeen = "Main app process exited normally"
732 if status != 0 and not didTimeout and not hitMaxTime:
733 self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
734 return status
736 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
737 """ build the application command line """
739 cmd = os.path.abspath(app)
740 if self.IS_MAC and os.path.exists(cmd + "-bin"):
741 # Prefer 'app-bin' in case 'app' is a shell script.
742 # We can remove this hack once bug 673899 etc are fixed.
743 cmd += "-bin"
745 args = []
747 if debuggerInfo:
748 args.extend(debuggerInfo.args)
749 args.append(cmd)
750 cmd = os.path.abspath(debuggerInfo.path)
752 if self.IS_MAC:
753 args.append("-foreground")
755 if self.IS_CYGWIN:
756 profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
757 else:
758 profileDirectory = profileDir + "/"
760 args.extend(("-no-remote", "-profile", profileDirectory))
761 if testURL is not None:
762 args.append((testURL))
763 args.extend(extraArgs)
764 return cmd, args
766 def checkForZombies(self, processLog, utilityPath, debuggerInfo):
767 """ Look for hung processes """
768 if not os.path.exists(processLog):
769 self.log.info('Automation Error: PID log not found: %s', processLog)
770 # Whilst no hung process was found, the run should still display as a failure
771 return True
773 foundZombie = False
774 self.log.info('INFO | zombiecheck | Reading PID log: %s', processLog)
775 processList = []
776 pidRE = re.compile(r'launched child process (\d+)$')
777 processLogFD = open(processLog)
778 for line in processLogFD:
779 self.log.info(line.rstrip())
780 m = pidRE.search(line)
781 if m:
782 processList.append(int(m.group(1)))
783 processLogFD.close()
785 for processPID in processList:
786 self.log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID)
787 if self.isPidAlive(processPID):
788 foundZombie = True
789 self.log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID)
790 self.killAndGetStack(processPID, utilityPath, debuggerInfo)
791 return foundZombie
793 def checkForCrashes(self, minidumpDir, symbolsPath):
794 return mozcrash.check_for_crashes(minidumpDir, symbolsPath, test_name=self.lastTestSeen)
796 def runApp(self, testURL, env, app, profileDir, extraArgs,
797 runSSLTunnel = False, utilityPath = None,
798 xrePath = None, certPath = None,
799 debuggerInfo = None, symbolsPath = None,
800 timeout = -1, maxTime = None, onLaunch = None,
801 detectShutdownLeaks = False, screenshotOnFail=False, testPath=None, bisectChunk=None):
803 Run the app, log the duration it took to execute, return the status code.
804 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
807 if utilityPath == None:
808 utilityPath = self.DIST_BIN
809 if xrePath == None:
810 xrePath = self.DIST_BIN
811 if certPath == None:
812 certPath = self.CERTS_SRC_DIR
813 if timeout == -1:
814 timeout = self.DEFAULT_TIMEOUT
816 # copy env so we don't munge the caller's environment
817 env = dict(env);
818 env["NO_EM_RESTART"] = "1"
819 tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
820 os.close(tmpfd)
821 env["MOZ_PROCESS_LOG"] = processLog
823 if self.IS_TEST_BUILD and runSSLTunnel:
824 # create certificate database for the profile
825 certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
826 if certificateStatus != 0:
827 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Certificate integration failed")
828 return certificateStatus
830 # start ssltunnel to provide https:// URLs capability
831 ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
832 ssltunnelProcess = self.Process([ssltunnel,
833 os.path.join(profileDir, "ssltunnel.cfg")],
834 env = self.environment(xrePath = xrePath))
835 self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
837 cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
838 startTime = datetime.now()
840 if debuggerInfo and debuggerInfo.interactive:
841 # If an interactive debugger is attached, don't redirect output,
842 # don't use timeouts, and don't capture ctrl-c.
843 timeout = None
844 maxTime = None
845 outputPipe = None
846 signal.signal(signal.SIGINT, lambda sigid, frame: None)
847 else:
848 outputPipe = subprocess.PIPE
850 self.lastTestSeen = "automation.py"
851 proc = self.Process([cmd] + args,
852 env = self.environment(env, xrePath = xrePath,
853 crashreporter = not debuggerInfo),
854 stdout = outputPipe,
855 stderr = subprocess.STDOUT)
856 self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
858 if onLaunch is not None:
859 # Allow callers to specify an onLaunch callback to be fired after the
860 # app is launched.
861 onLaunch()
863 status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
864 self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
866 # Do a final check for zombie child processes.
867 zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo)
869 crashed = self.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath)
871 if crashed or zombieProcesses:
872 status = 1
874 if os.path.exists(processLog):
875 os.unlink(processLog)
877 if self.IS_TEST_BUILD and runSSLTunnel:
878 ssltunnelProcess.kill()
880 return status
882 def getExtensionIDFromRDF(self, rdfSource):
884 Retrieves the extension id from an install.rdf file (or string).
886 from xml.dom.minidom import parse, parseString, Node
888 if isinstance(rdfSource, file):
889 document = parse(rdfSource)
890 else:
891 document = parseString(rdfSource)
893 # Find the <em:id> element. There can be multiple <em:id> tags
894 # within <em:targetApplication> tags, so we have to check this way.
895 for rdfChild in document.documentElement.childNodes:
896 if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description":
897 for descChild in rdfChild.childNodes:
898 if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id":
899 return descChild.childNodes[0].data
901 return None
903 def installExtension(self, extensionSource, profileDir, extensionID = None):
905 Copies an extension into the extensions directory of the given profile.
906 extensionSource - the source location of the extension files. This can be either
907 a directory or a path to an xpi file.
908 profileDir - the profile directory we are copying into. We will create the
909 "extensions" directory there if it doesn't exist.
910 extensionID - the id of the extension to be used as the containing directory for the
911 extension, if extensionSource is a directory, i.e.
912 this is the name of the folder in the <profileDir>/extensions/<extensionID>
914 if not os.path.isdir(profileDir):
915 self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir)
916 return
918 installRDFFilename = "install.rdf"
920 extensionsRootDir = os.path.join(profileDir, "extensions", "staged")
921 if not os.path.isdir(extensionsRootDir):
922 os.makedirs(extensionsRootDir)
924 if os.path.isfile(extensionSource):
925 reader = zipfile.ZipFile(extensionSource, "r")
927 for filename in reader.namelist():
928 # Sanity check the zip file.
929 if os.path.isabs(filename):
930 self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi")
931 return
933 # We may need to dig the extensionID out of the zip file...
934 if extensionID is None and filename == installRDFFilename:
935 extensionID = self.getExtensionIDFromRDF(reader.read(filename))
937 # We must know the extensionID now.
938 if extensionID is None:
939 self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
940 return
942 # Make the extension directory.
943 extensionDir = os.path.join(extensionsRootDir, extensionID)
944 os.mkdir(extensionDir)
946 # Extract all files.
947 reader.extractall(extensionDir)
949 elif os.path.isdir(extensionSource):
950 if extensionID is None:
951 filename = os.path.join(extensionSource, installRDFFilename)
952 if os.path.isfile(filename):
953 with open(filename, "r") as installRDF:
954 extensionID = self.getExtensionIDFromRDF(installRDF)
956 if extensionID is None:
957 self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
958 return
960 # Copy extension tree into its own directory.
961 # "destination directory must not already exist".
962 shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID))
964 else:
965 self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource)
967 def elf_arm(self, filename):
968 data = open(filename, 'rb').read(20)
969 return data[:4] == "\x7fELF" and ord(data[18]) == 40 # EM_ARM