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
8
from datetime import datetime, timedelta
21
from string import Template
23
SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
24
sys.path.insert(0, SCRIPT_DIR)
25
import automationutils
27
_DEFAULT_WEB_SERVER = "127.0.0.1"
28
_DEFAULT_HTTP_PORT = 8888
29
_DEFAULT_SSL_PORT = 4443
30
_DEFAULT_WEBSOCKET_PORT = 9988
32
#expand _DIST_BIN = __XPC_BIN_PATH__
33
#expand _IS_WIN32 = len("__WIN32__") != 0
34
#expand _IS_MAC = __IS_MAC__ != 0
35
#expand _IS_LINUX = __IS_LINUX__ != 0
37
#expand _IS_CYGWIN = __IS_CYGWIN__ == 1
41
#expand _IS_CAMINO = __IS_CAMINO__ != 0
42
#expand _BIN_SUFFIX = __BIN_SUFFIX__
43
#expand _PERL = __PERL__
45
#expand _DEFAULT_APP = "./" + __BROWSER_PATH__
46
#expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
47
#expand _IS_TEST_BUILD = __IS_TEST_BUILD__
48
#expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
49
#expand _CRASHREPORTER = __CRASHREPORTER__ == 1
53
import ctypes, ctypes.wintypes, time, msvcrt
58
# We use the logging system here primarily because it'll handle multiple
59
# threads, which is needed to process the output of the server and application
60
# processes simultaneously.
61
_log = logging.getLogger()
62
handler = logging.StreamHandler(sys.stdout)
63
_log.setLevel(logging.INFO)
64
_log.addHandler(handler)
71
class SyntaxError(Exception):
72
"Signifies a syntax error on a particular line in server-locations.txt."
74
def __init__(self, lineno, msg = None):
79
s = "Syntax error on line " + str(self.lineno)
81
s += ": %s." % self.msg
88
"Represents a location line in server-locations.txt."
90
def __init__(self, scheme, host, port, options):
94
self.options = options
96
class Automation(object):
98
Runs the browser from a script, and provides useful utilities
99
for setting up the browser environment.
106
IS_CYGWIN = _IS_CYGWIN
107
IS_CAMINO = _IS_CAMINO
108
BIN_SUFFIX = _BIN_SUFFIX
111
UNIXISH = not IS_WIN32 and not IS_MAC
113
DEFAULT_APP = _DEFAULT_APP
114
CERTS_SRC_DIR = _CERTS_SRC_DIR
115
IS_TEST_BUILD = _IS_TEST_BUILD
116
IS_DEBUG_BUILD = _IS_DEBUG_BUILD
117
CRASHREPORTER = _CRASHREPORTER
119
# timeout, in seconds
120
DEFAULT_TIMEOUT = 60.0
121
DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
122
DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
123
DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
124
DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
128
self.lastTestSeen = "automation.py"
129
self.haveDumpedScreen = False
131
def setServerInfo(self,
132
webServer = _DEFAULT_WEB_SERVER,
133
httpPort = _DEFAULT_HTTP_PORT,
134
sslPort = _DEFAULT_SSL_PORT,
135
webSocketPort = _DEFAULT_WEBSOCKET_PORT):
136
self.webServer = webServer
137
self.httpPort = httpPort
138
self.sslPort = sslPort
139
self.webSocketPort = webSocketPort
161
class Process(subprocess.Popen):
163
Represents our view of a subprocess.
164
It adds a kill() method which allows it to be stopped explicitly.
179
universal_newlines=False,
182
args = automationutils.wrapCommand(args)
183
print "args: %s" % args
184
subprocess.Popen.__init__(self, args, bufsize, executable,
185
stdin, stdout, stderr,
186
preexec_fn, close_fds,
188
universal_newlines, startupinfo, creationflags)
192
if Automation().IS_WIN32:
194
pid = "%i" % self.pid
195
if platform.release() == "2000":
196
# Windows 2000 needs 'kill.exe' from the
197
#'Windows 2000 Resource Kit tools'. (See bug 475455.)
199
subprocess.Popen(["kill", "-f", pid]).wait()
201
self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
203
# Windows XP and later.
204
subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
206
os.kill(self.pid, signal.SIGKILL)
208
def readLocations(self, locationsPath = "server-locations.txt"):
210
Reads the locations at which the Mochitest HTTP server is available from
211
server-locations.txt.
214
locationFile = codecs.open(locationsPath, "r", "UTF-8")
216
# Perhaps more detail than necessary, but it's the easiest way to make sure
217
# we get exactly the format we want. See server-locations.txt for the exact
218
# format guaranteed here.
219
lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
222
r"\d+\.\d+\.\d+\.\d+"
224
r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
225
r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
231
r"(?P<options>\S+(?:,\S+)*)"
236
for line in locationFile:
238
if line.startswith("#") or line == "\n":
241
match = lineRe.match(line)
243
raise SyntaxError(lineno)
245
options = match.group("options")
247
options = options.split(",")
248
if "primary" in options:
250
raise SyntaxError(lineno, "multiple primary locations")
255
locations.append(Location(match.group("scheme"), match.group("host"),
256
match.group("port"), options))
259
raise SyntaxError(lineno + 1, "missing primary location")
263
def setupPermissionsDatabase(self, profileDir, permissions):
264
# Open database and create table
265
permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite"))
266
cursor = permDB.cursor();
268
cursor.execute("PRAGMA user_version=3");
270
# SQL copied from nsPermissionManager.cpp
271
cursor.execute("""CREATE TABLE moz_hosts (
272
id INTEGER PRIMARY KEY,
279
isInBrowserElement INTEGER)""")
281
# Insert desired permissions
283
for perm in permissions.keys():
284
for host,allow in permissions[perm]:
286
cursor.execute("INSERT INTO moz_hosts values(?, ?, ?, ?, 0, 0, 0, 0)",
287
(c, host, perm, 1 if allow else 2))
293
def setupTestApps(self, profileDir, apps):
294
webappJSONTemplate = Template(""""$name": {
296
"installOrigin": "$origin",
298
"installTime": 132333986000,
299
"manifestURL": "$manifestURL",
303
manifestTemplate = Template("""{
305
"description": "$description",
309
"url": "https://mozilla.org/"
316
"description": "$description"
319
"default_locale": "en-US",
325
# Create webapps/webapps.json
326
webappsDir = os.path.join(profileDir, "webapps")
327
os.mkdir(webappsDir);
330
for localId, app in enumerate(apps):
331
app['localId'] = localId + 1 # Has to be 1..n
332
webappsJSON.append(webappJSONTemplate.substitute(app))
333
webappsJSON = '{\n' + ',\n'.join(webappsJSON) + '\n}\n'
335
webappsJSONFile = open(os.path.join(webappsDir, "webapps.json"), "a")
336
webappsJSONFile.write(webappsJSON)
337
webappsJSONFile.close()
339
# Create manifest file for each app.
341
manifest = manifestTemplate.substitute(app)
343
manifestDir = os.path.join(webappsDir, app['name'])
344
os.mkdir(manifestDir)
346
manifestFile = open(os.path.join(manifestDir, "manifest.webapp"), "a")
347
manifestFile.write(manifest)
350
def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
351
" Sets up the standard testing profile."
354
# Start with a clean slate.
355
shutil.rmtree(profileDir, True)
358
# Set up permissions database
359
locations = self.readLocations()
360
self.setupPermissionsDatabase(profileDir,
361
{'allowXULXBL':[(l.host, 'noxul' not in l.options) for l in locations]});
364
user_pref("social.skipLoadingProviders", true);
365
user_pref("browser.console.showInPanel", true);
366
user_pref("browser.dom.window.dump.enabled", true);
367
user_pref("browser.firstrun.show.localepicker", false);
368
user_pref("browser.firstrun.show.uidiscovery", false);
369
user_pref("browser.startup.page", 0); // use about:blank, not browser.startup.homepage
370
user_pref("browser.ui.layout.tablet", 0); // force tablet UI off
371
user_pref("dom.allow_scripts_to_close_windows", true);
372
user_pref("dom.disable_open_during_load", false);
373
user_pref("dom.max_script_run_time", 0); // no slow script dialogs
374
user_pref("hangmonitor.timeout", 0); // no hang monitor
375
user_pref("dom.max_chrome_script_run_time", 0);
376
user_pref("dom.popup_maximum", -1);
377
user_pref("dom.send_after_paint_to_content", true);
378
user_pref("dom.successive_dialog_time_limit", 0);
379
user_pref("signed.applets.codebase_principal_support", true);
380
user_pref("security.warn_submit_insecure", false);
381
user_pref("browser.shell.checkDefaultBrowser", false);
382
user_pref("shell.checkDefaultClient", false);
383
user_pref("browser.warnOnQuit", false);
384
user_pref("accessibility.typeaheadfind.autostart", false);
385
user_pref("javascript.options.showInConsole", true);
386
user_pref("devtools.errorconsole.enabled", true);
387
user_pref("layout.debug.enable_data_xbl", true);
388
user_pref("browser.EULA.override", true);
389
user_pref("javascript.options.jit_hardening", true);
390
user_pref("gfx.color_management.force_srgb", true);
391
user_pref("network.manage-offline-status", false);
392
user_pref("dom.min_background_timeout_value", 1000);
393
user_pref("test.mousescroll", true);
394
user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs
395
user_pref("network.http.prompt-temp-redirect", false);
396
user_pref("media.cache_size", 100);
397
user_pref("security.warn_viewing_mixed", false);
398
user_pref("app.update.enabled", false);
399
user_pref("app.update.staging.enabled", false);
400
user_pref("browser.panorama.experienced_first_run", true); // Assume experienced
401
user_pref("dom.w3c_touch_events.enabled", true);
402
user_pref("toolkit.telemetry.prompted", 2);
403
// Existing tests assume there is no font size inflation.
404
user_pref("font.size.inflation.emPerLine", 0);
405
user_pref("font.size.inflation.minTwips", 0);
407
// Only load extensions from the application and user profile
408
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
409
user_pref("extensions.enabledScopes", 5);
410
// Disable metadata caching for installed add-ons by default
411
user_pref("extensions.getAddons.cache.enabled", false);
412
// Disable intalling any distribution add-ons
413
user_pref("extensions.installDistroAddons", false);
415
user_pref("extensions.testpilot.runStudies", false);
417
user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
418
user_pref("geo.wifi.testing", true);
419
user_pref("geo.ignore.location_filter", true);
421
user_pref("camino.warn_when_closing", false); // Camino-only, harmless to others
423
// Make url-classifier updates so rare that they won't affect tests
424
user_pref("urlclassifier.updateinterval", 172800);
425
// Point the url-classifier to the local testing server for fast failures
426
user_pref("browser.safebrowsing.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
427
user_pref("browser.safebrowsing.keyURL", "http://%(server)s/safebrowsing-dummy/newkey");
428
user_pref("browser.safebrowsing.updateURL", "http://%(server)s/safebrowsing-dummy/update");
429
// Point update checks to the local testing server for fast failures
430
user_pref("extensions.update.url", "http://%(server)s/extensions-dummy/updateURL");
431
user_pref("extensions.update.background.url", "http://%(server)s/extensions-dummy/updateBackgroundURL");
432
user_pref("extensions.blocklist.url", "http://%(server)s/extensions-dummy/blocklistURL");
433
user_pref("extensions.hotfix.url", "http://%(server)s/extensions-dummy/hotfixURL");
434
// Make sure opening about:addons won't hit the network
435
user_pref("extensions.webservice.discoverURL", "http://%(server)s/extensions-dummy/discoveryURL");
436
// Make sure AddonRepository won't hit the network
437
user_pref("extensions.getAddons.maxResults", 0);
438
user_pref("extensions.getAddons.get.url", "http://%(server)s/extensions-dummy/repositoryGetURL");
439
user_pref("extensions.getAddons.getWithPerformance.url", "http://%(server)s/extensions-dummy/repositoryGetWithPerformanceURL");
440
user_pref("extensions.getAddons.search.browseURL", "http://%(server)s/extensions-dummy/repositoryBrowseURL");
441
user_pref("extensions.getAddons.search.url", "http://%(server)s/extensions-dummy/repositorySearchURL");
443
// Make enablePrivilege continue to work for test code. :-(
444
user_pref("security.enablePrivilege.enable_for_tests", true);
445
""" % { "server" : self.webServer + ":" + str(self.httpPort) }
448
if useServerLocations:
449
# We need to proxy every server but the primary one.
450
origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port)
451
for l in filter(lambda l: "primary" not in l.options, locations)]
452
origins = ", ".join(origins)
454
pacURL = """data:text/plain,
455
function FindProxyForURL(url, host)
457
var origins = [%(origins)s];
458
var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
462
'(?::(\\\\\\\\d+))?/');
463
var matches = regex.exec(url);
466
var isHttp = matches[1] == 'http';
467
var isHttps = matches[1] == 'https';
468
var isWebSocket = matches[1] == 'ws';
469
var isWebSocketSSL = matches[1] == 'wss';
472
if (isHttp | isWebSocket) matches[3] = '80';
473
if (isHttps | isWebSocketSSL) matches[3] = '443';
478
matches[1] = 'https';
480
var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
481
if (origins.indexOf(origin) < 0)
484
return 'PROXY %(remote)s:%(httpport)s';
485
if (isHttps || isWebSocket || isWebSocketSSL)
486
return 'PROXY %(remote)s:%(sslport)s';
488
}""" % { "origins": origins,
489
"remote": self.webServer,
490
"httpport":self.httpPort,
491
"sslport": self.sslPort }
492
pacURL = "".join(pacURL.splitlines())
495
user_pref("network.proxy.type", 2);
496
user_pref("network.proxy.autoconfig_url", "%(pacURL)s");
498
user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others
499
""" % {"pacURL": pacURL}
502
part = 'user_pref("network.proxy.type", 0);\n'
506
thispref = v.split("=", 1)
507
if len(thispref) < 2:
508
print "Error: syntax error in --setpref=" + v
510
part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
513
# write the preferences
514
prefsFile = open(profileDir + "/" + "user.js", "a")
515
prefsFile.write("".join(prefs))
520
'name': 'http_example_org',
521
'origin': 'http://example.org',
522
'manifestURL': 'http://example.org/manifest.webapp',
523
'description': 'http://example.org App'
526
'name': 'https_example_com',
527
'origin': 'https://example.com',
528
'manifestURL': 'https://example.com/manifest.webapp',
529
'description': 'https://example.com App'
532
'name': 'http_test1_example_org',
533
'origin': 'http://test1.example.org',
534
'manifestURL': 'http://test1.example.org/manifest.webapp',
535
'description': 'http://test1.example.org App'
538
'name': 'http_test1_example_org_8000',
539
'origin': 'http://test1.example.org:8000',
540
'manifestURL': 'http://test1.example.org:8000/manifest.webapp',
541
'description': 'http://test1.example.org:8000 App'
544
'name': 'http_sub1_test1_example_org',
545
'origin': 'http://sub1.test1.example.org',
546
'manifestURL': 'http://sub1.test1.example.org/manifest.webapp',
547
'description': 'http://sub1.test1.example.org App'
550
self.setupTestApps(profileDir, apps)
552
def addCommonOptions(self, parser):
553
"Adds command-line options which are common to mochitest and reftest."
555
parser.add_option("--setpref",
556
action = "append", type = "string",
558
dest = "extraPrefs", metavar = "PREF=VALUE",
559
help = "defines an extra user preference")
561
def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
562
pwfilePath = os.path.join(profileDir, ".crtdbpw")
564
pwfile = open(pwfilePath, "w")
568
# Create head of the ssltunnel configuration file
569
sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
570
sslTunnelConfig = open(sslTunnelConfigPath, "w")
572
sslTunnelConfig.write("httpproxy:1\n")
573
sslTunnelConfig.write("certdbdir:%s\n" % certPath)
574
sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
575
sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
576
sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
578
# Configure automatic certificate and bind custom certificates, client authentication
579
locations = self.readLocations()
581
for loc in locations:
582
if loc.scheme == "https" and "nocert" not in loc.options:
583
customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
584
clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
585
redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
586
for option in loc.options:
587
match = customCertRE.match(option)
589
customcert = match.group("nickname");
590
sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
591
(loc.host, loc.port, self.sslPort, customcert))
593
match = clientAuthRE.match(option)
595
clientauth = match.group("clientauth");
596
sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
597
(loc.host, loc.port, self.sslPort, clientauth))
599
match = redirRE.match(option)
601
redirhost = match.group("redirhost")
602
sslTunnelConfig.write("redirhost:%s:%s:%s:%s\n" %
603
(loc.host, loc.port, self.sslPort, redirhost))
605
sslTunnelConfig.close()
607
# Pre-create the certification database for the profile
608
env = self.environment(xrePath = xrePath)
609
certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
610
pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)
612
status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
616
# Walk the cert directory and add custom CAs and client certs
617
files = os.listdir(certPath)
619
root, ext = os.path.splitext(item)
622
if root.endswith("-object"):
624
self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
625
"-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
628
self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
629
pwfilePath, "-d", profileDir],
632
os.unlink(pwfilePath)
635
def environment(self, env = None, xrePath = None, crashreporter = True):
637
xrePath = self.DIST_BIN
639
env = dict(os.environ)
641
ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
642
if self.UNIXISH or self.IS_MAC:
643
envVar = "LD_LIBRARY_PATH"
645
envVar = "DYLD_LIBRARY_PATH"
647
env['MOZILLA_FIVE_HOME'] = xrePath
649
ldLibraryPath = ldLibraryPath + ":" + env[envVar]
650
env[envVar] = ldLibraryPath
652
env["PATH"] = env["PATH"] + ";" + ldLibraryPath
655
env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
656
env['MOZ_CRASHREPORTER'] = '1'
658
env['MOZ_CRASHREPORTER_DISABLE'] = '1'
660
env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
661
env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
662
env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
666
PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
667
GetLastError = ctypes.windll.kernel32.GetLastError
669
def readWithTimeout(self, f, timeout):
670
"""Try to read a line of output from the file object |f|.
671
|f| must be a pipe, like the |stdout| member of a subprocess.Popen
672
object created with stdout=PIPE. If no output
673
is received within |timeout| seconds, return a blank line.
674
Returns a tuple (line, did_timeout), where |did_timeout| is True
675
if the read timed out, and False otherwise."""
677
# shortcut to allow callers to pass in "None" for no timeout.
678
return (f.readline(), False)
679
x = msvcrt.get_osfhandle(f.fileno())
681
done = time.time() + timeout
682
while time.time() < done:
683
if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
684
err = self.GetLastError()
685
if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
688
log.error("readWithTimeout got error: %d", err)
690
# we're assuming that the output is line-buffered,
691
# which is not unreasonable
692
return (f.readline(), False)
696
def isPidAlive(self, pid):
698
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
699
pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
702
pExitCode = ctypes.wintypes.DWORD()
703
ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
704
ctypes.windll.kernel32.CloseHandle(pHandle)
705
return pExitCode.value == STILL_ACTIVE
707
def killPid(self, pid):
708
PROCESS_TERMINATE = 0x0001
709
pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
712
success = ctypes.windll.kernel32.TerminateProcess(pHandle, 1)
713
ctypes.windll.kernel32.CloseHandle(pHandle)
717
def readWithTimeout(self, f, timeout):
718
"""Try to read a line of output from the file object |f|. If no output
719
is received within |timeout| seconds, return a blank line.
720
Returns a tuple (line, did_timeout), where |did_timeout| is True
721
if the read timed out, and False otherwise."""
722
(r, w, e) = select.select([f], [], [], timeout)
725
return (f.readline(), False)
727
def isPidAlive(self, pid):
729
# kill(pid, 0) checks for a valid PID without actually sending a signal
730
# The method throws OSError if the PID is invalid, which we catch below.
733
# Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
734
# the process terminates before we get to this point.
735
wpid, wstatus = os.waitpid(pid, os.WNOHANG)
738
# Catch the errors we might expect from os.kill/os.waitpid,
739
# and re-raise any others
740
if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
744
def killPid(self, pid):
745
os.kill(pid, signal.SIGKILL)
747
def dumpScreen(self, utilityPath):
748
self.haveDumpedScreen = True;
750
# Need to figure out what tool and whether it write to a file or stdout
752
utility = [os.path.join(utilityPath, "screentopng")]
755
utility = ['/usr/sbin/screencapture', '-C', '-x', '-t', 'png']
758
utility = [os.path.join(utilityPath, "screenshot.exe")]
761
# Run the capture correctly for the type of capture
763
if imgoutput == 'file':
764
tmpfd, imgfilename = tempfile.mkstemp(prefix='mozilla-test-fail_')
766
dumper = self.Process(utility + [imgfilename])
767
elif imgoutput == 'stdout':
768
dumper = self.Process(utility, bufsize=-1,
769
stdout=subprocess.PIPE, close_fds=True)
771
self.log.info("Failed to start %s for screenshot: %s",
772
utility[0], err.strerror)
775
# Check whether the capture utility ran successfully
776
dumper_out, dumper_err = dumper.communicate()
777
if dumper.returncode != 0:
778
self.log.info("%s exited with code %d", utility, dumper.returncode)
782
if imgoutput == 'stdout':
784
elif imgoutput == 'file':
785
with open(imgfilename, 'rb') as imgfile:
786
image = imgfile.read()
788
self.log.info("Failed to read image from %s", imgoutput)
791
encoded = base64.b64encode(image)
792
self.log.info("SCREENSHOT: data:image/png;base64,%s", encoded)
794
def killAndGetStack(self, proc, utilityPath, debuggerInfo):
795
"""Kill the process, preferrably in a way that gets us a stack trace."""
797
if self.haveDumpedScreen:
798
self.log.info("Not taking screenshot here: see the one that was previously logged")
800
self.dumpScreen(utilityPath)
802
if self.CRASHREPORTER and not debuggerInfo:
804
# ABRT will get picked up by Breakpad's signal handler
805
os.kill(proc.pid, signal.SIGABRT)
808
# We should have a "crashinject" program in our utility path
809
crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
810
if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
812
#TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
813
self.log.info("Can't trigger Breakpad, just killing process")
816
def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
817
""" Look for timeout or crashes and return the status after the process terminates """
818
stackFixerProcess = None
819
stackFixerFunction = None
822
if proc.stdout is None:
823
self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
825
logsource = proc.stdout
827
if self.IS_DEBUG_BUILD and (self.IS_MAC or self.IS_LINUX) and symbolsPath and os.path.exists(symbolsPath):
828
# Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
829
# This method is preferred for Tinderbox builds, since native symbols may have been stripped.
830
sys.path.insert(0, utilityPath)
831
import fix_stack_using_bpsyms as stackFixerModule
832
stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
834
elif self.IS_DEBUG_BUILD and self.IS_MAC and False:
835
# Run each line through a function in fix_macosx_stack.py (uses atos)
836
sys.path.insert(0, utilityPath)
837
import fix_macosx_stack as stackFixerModule
838
stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
840
elif self.IS_DEBUG_BUILD and self.IS_LINUX:
841
# Run logsource through fix-linux-stack.pl (uses addr2line)
842
# This method is preferred for developer machines, so we don't have to run "make buildsymbols".
843
stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")],
845
stdout=subprocess.PIPE)
846
logsource = stackFixerProcess.stdout
848
(line, didTimeout) = self.readWithTimeout(logsource, timeout)
849
while line != "" and not didTimeout:
850
if stackFixerFunction:
851
line = stackFixerFunction(line)
852
self.log.info(line.rstrip().decode("UTF-8", "ignore"))
853
if "TEST-START" in line and "|" in line:
854
self.lastTestSeen = line.split("|")[1].strip()
855
if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
856
if self.haveDumpedScreen:
857
self.log.info("Not taking screenshot here: see the one that was previously logged")
859
self.dumpScreen(utilityPath)
861
(line, didTimeout) = self.readWithTimeout(logsource, timeout)
862
if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
863
# Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
865
self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
866
self.killAndGetStack(proc, utilityPath, debuggerInfo)
868
self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
869
self.killAndGetStack(proc, utilityPath, debuggerInfo)
873
self.lastTestSeen = "Main app process exited normally"
874
if status != 0 and not didTimeout and not hitMaxTime:
875
self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
876
if stackFixerProcess is not None:
877
fixerStatus = stackFixerProcess.wait()
878
if fixerStatus != 0 and not didTimeout and not hitMaxTime:
879
self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
882
def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
883
""" build the application command line """
885
cmd = os.path.abspath(app)
886
if self.IS_MAC and not self.IS_CAMINO and os.path.exists(cmd + "-bin"):
887
# Prefer 'app-bin' in case 'app' is a shell script.
888
# We can remove this hack once bug 673899 etc are fixed.
894
args.extend(debuggerInfo["args"])
896
cmd = os.path.abspath(debuggerInfo["path"])
899
args.append("-foreground")
902
profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
904
profileDirectory = profileDir + "/"
906
args.extend(("-no-remote", "-profile", profileDirectory))
907
if testURL is not None:
909
args.extend(("-url", testURL))
911
args.append((testURL))
912
args.extend(extraArgs)
915
def checkForZombies(self, processLog):
916
""" Look for hung processes """
917
if not os.path.exists(processLog):
918
self.log.info('INFO | automation.py | PID log not found: %s', processLog)
920
self.log.info('INFO | automation.py | Reading PID log: %s', processLog)
922
pidRE = re.compile(r'launched child process (\d+)$')
923
processLogFD = open(processLog)
924
for line in processLogFD:
925
self.log.info(line.rstrip())
926
m = pidRE.search(line)
928
processList.append(int(m.group(1)))
931
for processPID in processList:
932
self.log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID)
933
if self.isPidAlive(processPID):
934
self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
935
self.killPid(processPID)
937
def checkForCrashes(self, profileDir, symbolsPath):
938
automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath, self.lastTestSeen)
940
def runApp(self, testURL, env, app, profileDir, extraArgs,
941
runSSLTunnel = False, utilityPath = None,
942
xrePath = None, certPath = None,
943
debuggerInfo = None, symbolsPath = None,
944
timeout = -1, maxTime = None):
946
Run the app, log the duration it took to execute, return the status code.
947
Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
950
if utilityPath == None:
951
utilityPath = self.DIST_BIN
953
xrePath = self.DIST_BIN
955
certPath = self.CERTS_SRC_DIR
957
timeout = self.DEFAULT_TIMEOUT
959
# copy env so we don't munge the caller's environment
961
env["NO_EM_RESTART"] = "1"
962
tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
964
env["MOZ_PROCESS_LOG"] = processLog
966
if self.IS_TEST_BUILD and runSSLTunnel:
967
# create certificate database for the profile
968
certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
969
if certificateStatus != 0:
970
self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Certificate integration failed")
971
return certificateStatus
973
# start ssltunnel to provide https:// URLs capability
974
ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
975
ssltunnelProcess = self.Process([ssltunnel,
976
os.path.join(profileDir, "ssltunnel.cfg")],
977
env = self.environment(xrePath = xrePath))
978
self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
980
cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
981
startTime = datetime.now()
983
if debuggerInfo and debuggerInfo["interactive"]:
984
# If an interactive debugger is attached, don't redirect output,
985
# don't use timeouts, and don't capture ctrl-c.
989
signal.signal(signal.SIGINT, lambda sigid, frame: None)
991
outputPipe = subprocess.PIPE
993
self.lastTestSeen = "automation.py"
994
proc = self.Process([cmd] + args,
995
env = self.environment(env, xrePath = xrePath,
996
crashreporter = not debuggerInfo),
998
stderr = subprocess.STDOUT)
999
self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
1001
status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
1002
self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
1004
# Do a final check for zombie child processes.
1005
self.checkForZombies(processLog)
1006
self.checkForCrashes(profileDir, symbolsPath)
1008
if os.path.exists(processLog):
1009
os.unlink(processLog)
1011
if self.IS_TEST_BUILD and runSSLTunnel:
1012
ssltunnelProcess.kill()
1016
def getExtensionIDFromRDF(self, rdfSource):
1018
Retrieves the extension id from an install.rdf file (or string).
1020
from xml.dom.minidom import parse, parseString, Node
1022
if isinstance(rdfSource, file):
1023
document = parse(rdfSource)
1025
document = parseString(rdfSource)
1027
# Find the <em:id> element. There can be multiple <em:id> tags
1028
# within <em:targetApplication> tags, so we have to check this way.
1029
for rdfChild in document.documentElement.childNodes:
1030
if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description":
1031
for descChild in rdfChild.childNodes:
1032
if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id":
1033
return descChild.childNodes[0].data
1037
def installExtension(self, extensionSource, profileDir, extensionID = None):
1039
Copies an extension into the extensions directory of the given profile.
1040
extensionSource - the source location of the extension files. This can be either
1041
a directory or a path to an xpi file.
1042
profileDir - the profile directory we are copying into. We will create the
1043
"extensions" directory there if it doesn't exist.
1044
extensionID - the id of the extension to be used as the containing directory for the
1045
extension, if extensionSource is a directory, i.e.
1046
this is the name of the folder in the <profileDir>/extensions/<extensionID>
1048
if not os.path.isdir(profileDir):
1049
self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir)
1052
installRDFFilename = "install.rdf"
1054
extensionsRootDir = os.path.join(profileDir, "extensions", "staged")
1055
if not os.path.isdir(extensionsRootDir):
1056
os.makedirs(extensionsRootDir)
1058
if os.path.isfile(extensionSource):
1059
reader = automationutils.ZipFileReader(extensionSource)
1061
for filename in reader.namelist():
1062
# Sanity check the zip file.
1063
if os.path.isabs(filename):
1064
self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi")
1067
# We may need to dig the extensionID out of the zip file...
1068
if extensionID is None and filename == installRDFFilename:
1069
extensionID = self.getExtensionIDFromRDF(reader.read(filename))
1071
# We must know the extensionID now.
1072
if extensionID is None:
1073
self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
1076
# Make the extension directory.
1077
extensionDir = os.path.join(extensionsRootDir, extensionID)
1078
os.mkdir(extensionDir)
1080
# Extract all files.
1081
reader.extractall(extensionDir)
1083
elif os.path.isdir(extensionSource):
1084
if extensionID is None:
1085
filename = os.path.join(extensionSource, installRDFFilename)
1086
if os.path.isfile(filename):
1087
with open(filename, "r") as installRDF:
1088
extensionID = self.getExtensionIDFromRDF(installRDF)
1090
if extensionID is None:
1091
self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
1094
# Copy extension tree into its own directory.
1095
# "destination directory must not already exist".
1096
shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID))
1099
self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource)