bug 816059 - properly reset blocklist in browser-chrome tests r=jaws
[gecko.git] / build / automation.py.in
blobe70c5558336088c67109a8623684bc3bd54e263f
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 from datetime import datetime, timedelta
9 import itertools
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 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 # from nsIPrincipal.idl
33 _APP_STATUS_NOT_INSTALLED = 0
34 _APP_STATUS_INSTALLED = 1
35 _APP_STATUS_PRIVILEGED = 2
36 _APP_STATUS_CERTIFIED = 3
38 #expand _DIST_BIN = __XPC_BIN_PATH__
39 #expand _IS_WIN32 = len("__WIN32__") != 0
40 #expand _IS_MAC = __IS_MAC__ != 0
41 #expand _IS_LINUX = __IS_LINUX__ != 0
42 #ifdef IS_CYGWIN
43 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
44 #else
45 _IS_CYGWIN = False
46 #endif
47 #expand _IS_CAMINO = __IS_CAMINO__ != 0
48 #expand _BIN_SUFFIX = __BIN_SUFFIX__
49 #expand _PERL = __PERL__
51 #expand _DEFAULT_APP = "./" + __BROWSER_PATH__
52 #expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
53 #expand _IS_TEST_BUILD = __IS_TEST_BUILD__
54 #expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
55 #expand _CRASHREPORTER = __CRASHREPORTER__ == 1
58 if _IS_WIN32:
59 import ctypes, ctypes.wintypes, time, msvcrt
60 else:
61 import errno
64 # We use the logging system here primarily because it'll handle multiple
65 # threads, which is needed to process the output of the server and application
66 # processes simultaneously.
67 _log = logging.getLogger()
68 handler = logging.StreamHandler(sys.stdout)
69 _log.setLevel(logging.INFO)
70 _log.addHandler(handler)
73 #################
74 # PROFILE SETUP #
75 #################
77 class SyntaxError(Exception):
78 "Signifies a syntax error on a particular line in server-locations.txt."
80 def __init__(self, lineno, msg = None):
81 self.lineno = lineno
82 self.msg = msg
84 def __str__(self):
85 s = "Syntax error on line " + str(self.lineno)
86 if self.msg:
87 s += ": %s." % self.msg
88 else:
89 s += "."
90 return s
93 class Location:
94 "Represents a location line in server-locations.txt."
96 def __init__(self, scheme, host, port, options):
97 self.scheme = scheme
98 self.host = host
99 self.port = port
100 self.options = options
102 class Automation(object):
104 Runs the browser from a script, and provides useful utilities
105 for setting up the browser environment.
108 DIST_BIN = _DIST_BIN
109 IS_WIN32 = _IS_WIN32
110 IS_MAC = _IS_MAC
111 IS_LINUX = _IS_LINUX
112 IS_CYGWIN = _IS_CYGWIN
113 IS_CAMINO = _IS_CAMINO
114 BIN_SUFFIX = _BIN_SUFFIX
115 PERL = _PERL
117 UNIXISH = not IS_WIN32 and not IS_MAC
119 DEFAULT_APP = _DEFAULT_APP
120 CERTS_SRC_DIR = _CERTS_SRC_DIR
121 IS_TEST_BUILD = _IS_TEST_BUILD
122 IS_DEBUG_BUILD = _IS_DEBUG_BUILD
123 CRASHREPORTER = _CRASHREPORTER
125 # timeout, in seconds
126 DEFAULT_TIMEOUT = 60.0
127 DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
128 DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
129 DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
130 DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
132 def __init__(self):
133 self.log = _log
134 self.lastTestSeen = "automation.py"
135 self.haveDumpedScreen = False
137 def setServerInfo(self,
138 webServer = _DEFAULT_WEB_SERVER,
139 httpPort = _DEFAULT_HTTP_PORT,
140 sslPort = _DEFAULT_SSL_PORT,
141 webSocketPort = _DEFAULT_WEBSOCKET_PORT):
142 self.webServer = webServer
143 self.httpPort = httpPort
144 self.sslPort = sslPort
145 self.webSocketPort = webSocketPort
147 @property
148 def __all__(self):
149 return [
150 "UNIXISH",
151 "IS_WIN32",
152 "IS_MAC",
153 "log",
154 "runApp",
155 "Process",
156 "addCommonOptions",
157 "initializeProfile",
158 "DIST_BIN",
159 "DEFAULT_APP",
160 "CERTS_SRC_DIR",
161 "environment",
162 "IS_TEST_BUILD",
163 "IS_DEBUG_BUILD",
164 "DEFAULT_TIMEOUT",
167 class Process(subprocess.Popen):
169 Represents our view of a subprocess.
170 It adds a kill() method which allows it to be stopped explicitly.
173 def __init__(self,
174 args,
175 bufsize=0,
176 executable=None,
177 stdin=None,
178 stdout=None,
179 stderr=None,
180 preexec_fn=None,
181 close_fds=False,
182 shell=False,
183 cwd=None,
184 env=None,
185 universal_newlines=False,
186 startupinfo=None,
187 creationflags=0):
188 args = automationutils.wrapCommand(args)
189 print "args: %s" % args
190 subprocess.Popen.__init__(self, args, bufsize, executable,
191 stdin, stdout, stderr,
192 preexec_fn, close_fds,
193 shell, cwd, env,
194 universal_newlines, startupinfo, creationflags)
195 self.log = _log
197 def kill(self):
198 if Automation().IS_WIN32:
199 import platform
200 pid = "%i" % self.pid
201 if platform.release() == "2000":
202 # Windows 2000 needs 'kill.exe' from the
203 #'Windows 2000 Resource Kit tools'. (See bug 475455.)
204 try:
205 subprocess.Popen(["kill", "-f", pid]).wait()
206 except:
207 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
208 else:
209 # Windows XP and later.
210 subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
211 else:
212 os.kill(self.pid, signal.SIGKILL)
214 def readLocations(self, locationsPath = "server-locations.txt"):
216 Reads the locations at which the Mochitest HTTP server is available from
217 server-locations.txt.
220 locationFile = codecs.open(locationsPath, "r", "UTF-8")
222 # Perhaps more detail than necessary, but it's the easiest way to make sure
223 # we get exactly the format we want. See server-locations.txt for the exact
224 # format guaranteed here.
225 lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
226 r"://"
227 r"(?P<host>"
228 r"\d+\.\d+\.\d+\.\d+"
229 r"|"
230 r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
231 r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
232 r")"
233 r":"
234 r"(?P<port>\d+)"
235 r"(?:"
236 r"\s+"
237 r"(?P<options>\S+(?:,\S+)*)"
238 r")?$")
239 locations = []
240 lineno = 0
241 seenPrimary = False
242 for line in locationFile:
243 lineno += 1
244 if line.startswith("#") or line == "\n":
245 continue
247 match = lineRe.match(line)
248 if not match:
249 raise SyntaxError(lineno)
251 options = match.group("options")
252 if options:
253 options = options.split(",")
254 if "primary" in options:
255 if seenPrimary:
256 raise SyntaxError(lineno, "multiple primary locations")
257 seenPrimary = True
258 else:
259 options = []
261 locations.append(Location(match.group("scheme"), match.group("host"),
262 match.group("port"), options))
264 if not seenPrimary:
265 raise SyntaxError(lineno + 1, "missing primary location")
267 return locations
269 def setupPermissionsDatabase(self, profileDir, permissions):
270 # Open database and create table
271 permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite"))
272 cursor = permDB.cursor();
274 cursor.execute("PRAGMA user_version=3");
276 # SQL copied from nsPermissionManager.cpp
277 cursor.execute("""CREATE TABLE moz_hosts (
278 id INTEGER PRIMARY KEY,
279 host TEXT,
280 type TEXT,
281 permission INTEGER,
282 expireType INTEGER,
283 expireTime INTEGER,
284 appId INTEGER,
285 isInBrowserElement INTEGER)""")
287 # Insert desired permissions
288 c = 0
289 for perm in permissions.keys():
290 for host,allow in permissions[perm]:
291 c += 1
292 cursor.execute("INSERT INTO moz_hosts values(?, ?, ?, ?, 0, 0, 0, 0)",
293 (c, host, perm, 1 if allow else 2))
295 # Commit and close
296 permDB.commit()
297 cursor.close()
299 def setupTestApps(self, profileDir, apps):
300 webappJSONTemplate = Template(""""$name": {
301 "origin": "$origin",
302 "installOrigin": "$origin",
303 "receipt": null,
304 "installTime": 132333986000,
305 "manifestURL": "$manifestURL",
306 "localId": $localId,
307 "appStatus": $appStatus,
308 "csp": "$csp"
309 }""")
311 manifestTemplate = Template("""{
312 "name": "$name",
313 "csp": "$csp",
314 "description": "$description",
315 "launch_path": "/",
316 "developer": {
317 "name": "Mozilla",
318 "url": "https://mozilla.org/"
320 "permissions": [
322 "locales": {
323 "en-US": {
324 "name": "$name",
325 "description": "$description"
328 "default_locale": "en-US",
329 "icons": {
332 """)
334 # Create webapps/webapps.json
335 webappsDir = os.path.join(profileDir, "webapps")
336 os.mkdir(webappsDir);
338 webappsJSON = []
339 for localId, app in enumerate(apps):
340 app['localId'] = localId + 1 # Has to be 1..n
341 webappsJSON.append(webappJSONTemplate.substitute(app))
342 webappsJSON = '{\n' + ',\n'.join(webappsJSON) + '\n}\n'
344 webappsJSONFile = open(os.path.join(webappsDir, "webapps.json"), "a")
345 webappsJSONFile.write(webappsJSON)
346 webappsJSONFile.close()
348 # Create manifest file for each app.
349 for app in apps:
350 manifest = manifestTemplate.substitute(app)
352 manifestDir = os.path.join(webappsDir, app['name'])
353 os.mkdir(manifestDir)
355 manifestFile = open(os.path.join(manifestDir, "manifest.webapp"), "a")
356 manifestFile.write(manifest)
357 manifestFile.close()
359 def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
360 " Sets up the standard testing profile."
362 prefs = []
363 # Start with a clean slate.
364 shutil.rmtree(profileDir, True)
365 os.mkdir(profileDir)
367 # Set up permissions database
368 locations = self.readLocations()
369 self.setupPermissionsDatabase(profileDir,
370 {'allowXULXBL':[(l.host, 'noxul' not in l.options) for l in locations]});
372 part = """\
373 user_pref("social.skipLoadingProviders", true);
374 user_pref("browser.console.showInPanel", true);
375 user_pref("browser.dom.window.dump.enabled", true);
376 user_pref("browser.firstrun.show.localepicker", false);
377 user_pref("browser.firstrun.show.uidiscovery", false);
378 user_pref("browser.startup.page", 0); // use about:blank, not browser.startup.homepage
379 user_pref("browser.ui.layout.tablet", 0); // force tablet UI off
380 user_pref("dom.allow_scripts_to_close_windows", true);
381 user_pref("dom.disable_open_during_load", false);
382 user_pref("dom.max_script_run_time", 0); // no slow script dialogs
383 user_pref("hangmonitor.timeout", 0); // no hang monitor
384 user_pref("dom.max_chrome_script_run_time", 0);
385 user_pref("dom.popup_maximum", -1);
386 user_pref("dom.send_after_paint_to_content", true);
387 user_pref("dom.successive_dialog_time_limit", 0);
388 user_pref("signed.applets.codebase_principal_support", true);
389 user_pref("browser.shell.checkDefaultBrowser", false);
390 user_pref("shell.checkDefaultClient", false);
391 user_pref("browser.warnOnQuit", false);
392 user_pref("accessibility.typeaheadfind.autostart", false);
393 user_pref("javascript.options.showInConsole", true);
394 user_pref("devtools.errorconsole.enabled", true);
395 user_pref("layout.debug.enable_data_xbl", true);
396 user_pref("browser.EULA.override", true);
397 user_pref("javascript.options.jit_hardening", true);
398 user_pref("gfx.color_management.force_srgb", true);
399 user_pref("network.manage-offline-status", false);
400 user_pref("dom.min_background_timeout_value", 1000);
401 user_pref("test.mousescroll", true);
402 user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs
403 user_pref("network.http.prompt-temp-redirect", false);
404 user_pref("media.cache_size", 100);
405 user_pref("security.warn_viewing_mixed", false);
406 user_pref("app.update.enabled", false);
407 user_pref("app.update.staging.enabled", false);
408 user_pref("browser.panorama.experienced_first_run", true); // Assume experienced
409 user_pref("dom.w3c_touch_events.enabled", 1);
410 user_pref("toolkit.telemetry.prompted", 2);
411 // Existing tests assume there is no font size inflation.
412 user_pref("font.size.inflation.emPerLine", 0);
413 user_pref("font.size.inflation.minTwips", 0);
415 // Only load extensions from the application and user profile
416 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
417 user_pref("extensions.enabledScopes", 5);
418 // Disable metadata caching for installed add-ons by default
419 user_pref("extensions.getAddons.cache.enabled", false);
420 // Disable intalling any distribution add-ons
421 user_pref("extensions.installDistroAddons", false);
423 user_pref("extensions.testpilot.runStudies", false);
425 user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
426 user_pref("geo.wifi.testing", true);
427 user_pref("geo.ignore.location_filter", true);
429 user_pref("camino.warn_when_closing", false); // Camino-only, harmless to others
431 // Make url-classifier updates so rare that they won't affect tests
432 user_pref("urlclassifier.updateinterval", 172800);
433 // Point the url-classifier to the local testing server for fast failures
434 user_pref("browser.safebrowsing.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
435 user_pref("browser.safebrowsing.keyURL", "http://%(server)s/safebrowsing-dummy/newkey");
436 user_pref("browser.safebrowsing.updateURL", "http://%(server)s/safebrowsing-dummy/update");
437 // Point update checks to the local testing server for fast failures
438 user_pref("extensions.update.url", "http://%(server)s/extensions-dummy/updateURL");
439 user_pref("extensions.update.background.url", "http://%(server)s/extensions-dummy/updateBackgroundURL");
440 user_pref("extensions.blocklist.url", "http://%(server)s/extensions-dummy/blocklistURL");
441 user_pref("extensions.hotfix.url", "http://%(server)s/extensions-dummy/hotfixURL");
442 // Turn off extension updates so they don't bother tests
443 user_pref("extensions.update.enabled", false);
444 // Make sure opening about:addons won't hit the network
445 user_pref("extensions.webservice.discoverURL", "http://%(server)s/extensions-dummy/discoveryURL");
446 // Make sure AddonRepository won't hit the network
447 user_pref("extensions.getAddons.maxResults", 0);
448 user_pref("extensions.getAddons.get.url", "http://%(server)s/extensions-dummy/repositoryGetURL");
449 user_pref("extensions.getAddons.getWithPerformance.url", "http://%(server)s/extensions-dummy/repositoryGetWithPerformanceURL");
450 user_pref("extensions.getAddons.search.browseURL", "http://%(server)s/extensions-dummy/repositoryBrowseURL");
451 user_pref("extensions.getAddons.search.url", "http://%(server)s/extensions-dummy/repositorySearchURL");
453 // Make enablePrivilege continue to work for test code. :-(
454 user_pref("security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", true);
456 // Get network events.
457 user_pref("network.activity.blipIntervalMilliseconds", 250);
458 """ % { "server" : self.webServer + ":" + str(self.httpPort) }
459 prefs.append(part)
461 if useServerLocations:
462 # We need to proxy every server but the primary one.
463 origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port)
464 for l in filter(lambda l: "primary" not in l.options, locations)]
465 origins = ", ".join(origins)
467 pacURL = """data:text/plain,
468 function FindProxyForURL(url, host)
470 var origins = [%(origins)s];
471 var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
472 '://' +
473 '(?:[^/@]*@)?' +
474 '(.*?)' +
475 '(?::(\\\\\\\\d+))?/');
476 var matches = regex.exec(url);
477 if (!matches)
478 return 'DIRECT';
479 var isHttp = matches[1] == 'http';
480 var isHttps = matches[1] == 'https';
481 var isWebSocket = matches[1] == 'ws';
482 var isWebSocketSSL = matches[1] == 'wss';
483 if (!matches[3])
485 if (isHttp | isWebSocket) matches[3] = '80';
486 if (isHttps | isWebSocketSSL) matches[3] = '443';
488 if (isWebSocket)
489 matches[1] = 'http';
490 if (isWebSocketSSL)
491 matches[1] = 'https';
493 var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
494 if (origins.indexOf(origin) < 0)
495 return 'DIRECT';
496 if (isHttp)
497 return 'PROXY %(remote)s:%(httpport)s';
498 if (isHttps || isWebSocket || isWebSocketSSL)
499 return 'PROXY %(remote)s:%(sslport)s';
500 return 'DIRECT';
501 }""" % { "origins": origins,
502 "remote": self.webServer,
503 "httpport":self.httpPort,
504 "sslport": self.sslPort }
505 pacURL = "".join(pacURL.splitlines())
507 part += """
508 user_pref("network.proxy.type", 2);
509 user_pref("network.proxy.autoconfig_url", "%(pacURL)s");
511 user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others
512 """ % {"pacURL": pacURL}
513 prefs.append(part)
514 else:
515 part = 'user_pref("network.proxy.type", 0);\n'
516 prefs.append(part)
518 for v in extraPrefs:
519 thispref = v.split("=", 1)
520 if len(thispref) < 2:
521 print "Error: syntax error in --setpref=" + v
522 sys.exit(1)
523 part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
524 prefs.append(part)
526 # write the preferences
527 prefsFile = open(profileDir + "/" + "user.js", "a")
528 prefsFile.write("".join(prefs))
529 prefsFile.close()
531 apps = [
533 'name': 'http_example_org',
534 'csp': '',
535 'origin': 'http://example.org',
536 'manifestURL': 'http://example.org/manifest.webapp',
537 'description': 'http://example.org App',
538 'appStatus': _APP_STATUS_INSTALLED
541 'name': 'https_example_com',
542 'csp': '',
543 'origin': 'https://example.com',
544 'manifestURL': 'https://example.com/manifest.webapp',
545 'description': 'https://example.com App',
546 'appStatus': _APP_STATUS_INSTALLED
549 'name': 'http_test1_example_org',
550 'csp': '',
551 'origin': 'http://test1.example.org',
552 'manifestURL': 'http://test1.example.org/manifest.webapp',
553 'description': 'http://test1.example.org App',
554 'appStatus': _APP_STATUS_INSTALLED
557 'name': 'http_test1_example_org_8000',
558 'csp': '',
559 'origin': 'http://test1.example.org:8000',
560 'manifestURL': 'http://test1.example.org:8000/manifest.webapp',
561 'description': 'http://test1.example.org:8000 App',
562 'appStatus': _APP_STATUS_INSTALLED
565 'name': 'http_sub1_test1_example_org',
566 'csp': '',
567 'origin': 'http://sub1.test1.example.org',
568 'manifestURL': 'http://sub1.test1.example.org/manifest.webapp',
569 'description': 'http://sub1.test1.example.org App',
570 'appStatus': _APP_STATUS_INSTALLED
573 'name': 'https_example_com_privileged',
574 'csp': '',
575 'origin': 'https://example.com',
576 'manifestURL': 'https://example.com/manifest_priv.webapp',
577 'description': 'https://example.com Privileged App',
578 'appStatus': _APP_STATUS_PRIVILEGED
581 'name': 'https_example_com_certified',
582 'csp': '',
583 'origin': 'https://example.com',
584 'manifestURL': 'https://example.com/manifest_cert.webapp',
585 'description': 'https://example.com Certified App',
586 'appStatus': _APP_STATUS_CERTIFIED
589 'name': 'https_example_csp_certified',
590 'csp': "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'",
591 'origin': 'https://example.com',
592 'manifestURL': 'https://example.com/manifest_csp_cert.webapp',
593 'description': 'https://example.com Certified App with manifest policy',
594 'appStatus': _APP_STATUS_CERTIFIED
597 'name': 'https_example_csp_installed',
598 'csp': "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'",
599 'origin': 'https://example.com',
600 'manifestURL': 'https://example.com/manifest_csp_inst.webapp',
601 'description': 'https://example.com Installed App with manifest policy',
602 'appStatus': _APP_STATUS_INSTALLED
605 'name': 'https_example_csp_privileged',
606 'csp': "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'",
607 'origin': 'https://example.com',
608 'manifestURL': 'https://example.com/manifest_csp_priv.webapp',
609 'description': 'https://example.com Privileged App with manifest policy',
610 'appStatus': _APP_STATUS_PRIVILEGED
613 self.setupTestApps(profileDir, apps)
615 def addCommonOptions(self, parser):
616 "Adds command-line options which are common to mochitest and reftest."
618 parser.add_option("--setpref",
619 action = "append", type = "string",
620 default = [],
621 dest = "extraPrefs", metavar = "PREF=VALUE",
622 help = "defines an extra user preference")
624 def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
625 pwfilePath = os.path.join(profileDir, ".crtdbpw")
627 pwfile = open(pwfilePath, "w")
628 pwfile.write("\n")
629 pwfile.close()
631 # Create head of the ssltunnel configuration file
632 sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
633 sslTunnelConfig = open(sslTunnelConfigPath, "w")
635 sslTunnelConfig.write("httpproxy:1\n")
636 sslTunnelConfig.write("certdbdir:%s\n" % certPath)
637 sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
638 sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
639 sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
641 # Configure automatic certificate and bind custom certificates, client authentication
642 locations = self.readLocations()
643 locations.pop(0)
644 for loc in locations:
645 if loc.scheme == "https" and "nocert" not in loc.options:
646 customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
647 clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
648 redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
649 for option in loc.options:
650 match = customCertRE.match(option)
651 if match:
652 customcert = match.group("nickname");
653 sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
654 (loc.host, loc.port, self.sslPort, customcert))
656 match = clientAuthRE.match(option)
657 if match:
658 clientauth = match.group("clientauth");
659 sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
660 (loc.host, loc.port, self.sslPort, clientauth))
662 match = redirRE.match(option)
663 if match:
664 redirhost = match.group("redirhost")
665 sslTunnelConfig.write("redirhost:%s:%s:%s:%s\n" %
666 (loc.host, loc.port, self.sslPort, redirhost))
668 sslTunnelConfig.close()
670 # Pre-create the certification database for the profile
671 env = self.environment(xrePath = xrePath)
672 certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
673 pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)
675 status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
676 if status != 0:
677 return status
679 # Walk the cert directory and add custom CAs and client certs
680 files = os.listdir(certPath)
681 for item in files:
682 root, ext = os.path.splitext(item)
683 if ext == ".ca":
684 trustBits = "CT,,"
685 if root.endswith("-object"):
686 trustBits = "CT,,CT"
687 self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
688 "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
689 env = env).wait()
690 if ext == ".client":
691 self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
692 pwfilePath, "-d", profileDir],
693 env = env).wait()
695 os.unlink(pwfilePath)
696 return 0
698 def environment(self, env = None, xrePath = None, crashreporter = True):
699 if xrePath == None:
700 xrePath = self.DIST_BIN
701 if env == None:
702 env = dict(os.environ)
704 ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
705 if self.UNIXISH or self.IS_MAC:
706 envVar = "LD_LIBRARY_PATH"
707 if self.IS_MAC:
708 envVar = "DYLD_LIBRARY_PATH"
709 else: # unixish
710 env['MOZILLA_FIVE_HOME'] = xrePath
711 if envVar in env:
712 ldLibraryPath = ldLibraryPath + ":" + env[envVar]
713 env[envVar] = ldLibraryPath
714 elif self.IS_WIN32:
715 env["PATH"] = env["PATH"] + ";" + ldLibraryPath
717 if crashreporter:
718 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
719 env['MOZ_CRASHREPORTER'] = '1'
720 else:
721 env['MOZ_CRASHREPORTER_DISABLE'] = '1'
723 env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
724 env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
725 env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
726 return env
728 if IS_WIN32:
729 PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
730 GetLastError = ctypes.windll.kernel32.GetLastError
732 def readWithTimeout(self, f, timeout):
733 """Try to read a line of output from the file object |f|.
734 |f| must be a pipe, like the |stdout| member of a subprocess.Popen
735 object created with stdout=PIPE. If no output
736 is received within |timeout| seconds, return a blank line.
737 Returns a tuple (line, did_timeout), where |did_timeout| is True
738 if the read timed out, and False otherwise."""
739 if timeout is None:
740 # shortcut to allow callers to pass in "None" for no timeout.
741 return (f.readline(), False)
742 x = msvcrt.get_osfhandle(f.fileno())
743 l = ctypes.c_long()
744 done = time.time() + timeout
745 while time.time() < done:
746 if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
747 err = self.GetLastError()
748 if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
749 return ('', False)
750 else:
751 log.error("readWithTimeout got error: %d", err)
752 if l.value > 0:
753 # we're assuming that the output is line-buffered,
754 # which is not unreasonable
755 return (f.readline(), False)
756 time.sleep(0.01)
757 return ('', True)
759 def isPidAlive(self, pid):
760 STILL_ACTIVE = 259
761 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
762 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
763 if not pHandle:
764 return False
765 pExitCode = ctypes.wintypes.DWORD()
766 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
767 ctypes.windll.kernel32.CloseHandle(pHandle)
768 return pExitCode.value == STILL_ACTIVE
770 def killPid(self, pid):
771 PROCESS_TERMINATE = 0x0001
772 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
773 if not pHandle:
774 return
775 success = ctypes.windll.kernel32.TerminateProcess(pHandle, 1)
776 ctypes.windll.kernel32.CloseHandle(pHandle)
778 else:
780 def readWithTimeout(self, f, timeout):
781 """Try to read a line of output from the file object |f|. If no output
782 is received within |timeout| seconds, return a blank line.
783 Returns a tuple (line, did_timeout), where |did_timeout| is True
784 if the read timed out, and False otherwise."""
785 (r, w, e) = select.select([f], [], [], timeout)
786 if len(r) == 0:
787 return ('', True)
788 return (f.readline(), False)
790 def isPidAlive(self, pid):
791 try:
792 # kill(pid, 0) checks for a valid PID without actually sending a signal
793 # The method throws OSError if the PID is invalid, which we catch below.
794 os.kill(pid, 0)
796 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
797 # the process terminates before we get to this point.
798 wpid, wstatus = os.waitpid(pid, os.WNOHANG)
799 return wpid == 0
800 except OSError, err:
801 # Catch the errors we might expect from os.kill/os.waitpid,
802 # and re-raise any others
803 if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
804 return False
805 raise
807 def killPid(self, pid):
808 os.kill(pid, signal.SIGKILL)
810 def dumpScreen(self, utilityPath):
811 self.haveDumpedScreen = True;
813 # Need to figure out what tool and whether it write to a file or stdout
814 if self.UNIXISH:
815 utility = [os.path.join(utilityPath, "screentopng")]
816 imgoutput = 'stdout'
817 elif self.IS_MAC:
818 utility = ['/usr/sbin/screencapture', '-C', '-x', '-t', 'png']
819 imgoutput = 'file'
820 elif self.IS_WIN32:
821 utility = [os.path.join(utilityPath, "screenshot.exe")]
822 imgoutput = 'file'
824 # Run the capture correctly for the type of capture
825 try:
826 if imgoutput == 'file':
827 tmpfd, imgfilename = tempfile.mkstemp(prefix='mozilla-test-fail_')
828 os.close(tmpfd)
829 dumper = self.Process(utility + [imgfilename])
830 elif imgoutput == 'stdout':
831 dumper = self.Process(utility, bufsize=-1,
832 stdout=subprocess.PIPE, close_fds=True)
833 except OSError, err:
834 self.log.info("Failed to start %s for screenshot: %s",
835 utility[0], err.strerror)
836 return
838 # Check whether the capture utility ran successfully
839 dumper_out, dumper_err = dumper.communicate()
840 if dumper.returncode != 0:
841 self.log.info("%s exited with code %d", utility, dumper.returncode)
842 return
844 try:
845 if imgoutput == 'stdout':
846 image = dumper_out
847 elif imgoutput == 'file':
848 with open(imgfilename, 'rb') as imgfile:
849 image = imgfile.read()
850 except IOError, err:
851 self.log.info("Failed to read image from %s", imgoutput)
853 import base64
854 encoded = base64.b64encode(image)
855 self.log.info("SCREENSHOT: data:image/png;base64,%s", encoded)
857 def killAndGetStack(self, proc, utilityPath, debuggerInfo):
858 """Kill the process, preferrably in a way that gets us a stack trace."""
859 if not debuggerInfo:
860 if self.haveDumpedScreen:
861 self.log.info("Not taking screenshot here: see the one that was previously logged")
862 else:
863 self.dumpScreen(utilityPath)
865 if self.CRASHREPORTER and not debuggerInfo:
866 if self.UNIXISH:
867 # ABRT will get picked up by Breakpad's signal handler
868 os.kill(proc.pid, signal.SIGABRT)
869 return
870 elif self.IS_WIN32:
871 # We should have a "crashinject" program in our utility path
872 crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
873 if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
874 return
875 #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
876 self.log.info("Can't trigger Breakpad, just killing process")
877 proc.kill()
879 def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
880 """ Look for timeout or crashes and return the status after the process terminates """
881 stackFixerProcess = None
882 stackFixerFunction = None
883 didTimeout = False
884 hitMaxTime = False
885 if proc.stdout is None:
886 self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
887 else:
888 logsource = proc.stdout
890 if self.IS_DEBUG_BUILD and (self.IS_MAC or self.IS_LINUX) and symbolsPath and os.path.exists(symbolsPath):
891 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
892 # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
893 sys.path.insert(0, utilityPath)
894 import fix_stack_using_bpsyms as stackFixerModule
895 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
896 del sys.path[0]
897 elif self.IS_DEBUG_BUILD and self.IS_MAC and False:
898 # Run each line through a function in fix_macosx_stack.py (uses atos)
899 sys.path.insert(0, utilityPath)
900 import fix_macosx_stack as stackFixerModule
901 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
902 del sys.path[0]
903 elif self.IS_DEBUG_BUILD and self.IS_LINUX:
904 # Run logsource through fix-linux-stack.pl (uses addr2line)
905 # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
906 stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")],
907 stdin=logsource,
908 stdout=subprocess.PIPE)
909 logsource = stackFixerProcess.stdout
911 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
912 while line != "" and not didTimeout:
913 if stackFixerFunction:
914 line = stackFixerFunction(line)
915 self.log.info(line.rstrip().decode("UTF-8", "ignore"))
916 if "TEST-START" in line and "|" in line:
917 self.lastTestSeen = line.split("|")[1].strip()
918 if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
919 if self.haveDumpedScreen:
920 self.log.info("Not taking screenshot here: see the one that was previously logged")
921 else:
922 self.dumpScreen(utilityPath)
924 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
925 if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
926 # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
927 hitMaxTime = True
928 self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
929 self.killAndGetStack(proc, utilityPath, debuggerInfo)
930 if didTimeout:
931 self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
932 self.killAndGetStack(proc, utilityPath, debuggerInfo)
934 status = proc.wait()
935 if status == 0:
936 self.lastTestSeen = "Main app process exited normally"
937 if status != 0 and not didTimeout and not hitMaxTime:
938 self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
939 if stackFixerProcess is not None:
940 fixerStatus = stackFixerProcess.wait()
941 if fixerStatus != 0 and not didTimeout and not hitMaxTime:
942 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
943 return status
945 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
946 """ build the application command line """
948 cmd = os.path.abspath(app)
949 if self.IS_MAC and not self.IS_CAMINO and os.path.exists(cmd + "-bin"):
950 # Prefer 'app-bin' in case 'app' is a shell script.
951 # We can remove this hack once bug 673899 etc are fixed.
952 cmd += "-bin"
954 args = []
956 if debuggerInfo:
957 args.extend(debuggerInfo["args"])
958 args.append(cmd)
959 cmd = os.path.abspath(debuggerInfo["path"])
961 if self.IS_MAC:
962 args.append("-foreground")
964 if self.IS_CYGWIN:
965 profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
966 else:
967 profileDirectory = profileDir + "/"
969 args.extend(("-no-remote", "-profile", profileDirectory))
970 if testURL is not None:
971 if self.IS_CAMINO:
972 args.extend(("-url", testURL))
973 else:
974 args.append((testURL))
975 args.extend(extraArgs)
976 return cmd, args
978 def checkForZombies(self, processLog):
979 """ Look for hung processes """
980 if not os.path.exists(processLog):
981 self.log.info('Automation Error: PID log not found: %s', processLog)
982 # Whilst no hung process was found, the run should still display as a failure
983 return True
985 foundZombie = False
986 self.log.info('INFO | automation.py | Reading PID log: %s', processLog)
987 processList = []
988 pidRE = re.compile(r'launched child process (\d+)$')
989 processLogFD = open(processLog)
990 for line in processLogFD:
991 self.log.info(line.rstrip())
992 m = pidRE.search(line)
993 if m:
994 processList.append(int(m.group(1)))
995 processLogFD.close()
997 for processPID in processList:
998 self.log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID)
999 if self.isPidAlive(processPID):
1000 foundZombie = True
1001 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
1002 self.killPid(processPID)
1003 return foundZombie
1005 def checkForCrashes(self, profileDir, symbolsPath):
1006 return automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath, self.lastTestSeen)
1008 def runApp(self, testURL, env, app, profileDir, extraArgs,
1009 runSSLTunnel = False, utilityPath = None,
1010 xrePath = None, certPath = None,
1011 debuggerInfo = None, symbolsPath = None,
1012 timeout = -1, maxTime = None):
1014 Run the app, log the duration it took to execute, return the status code.
1015 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
1018 if utilityPath == None:
1019 utilityPath = self.DIST_BIN
1020 if xrePath == None:
1021 xrePath = self.DIST_BIN
1022 if certPath == None:
1023 certPath = self.CERTS_SRC_DIR
1024 if timeout == -1:
1025 timeout = self.DEFAULT_TIMEOUT
1027 # copy env so we don't munge the caller's environment
1028 env = dict(env);
1029 env["NO_EM_RESTART"] = "1"
1030 tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
1031 os.close(tmpfd)
1032 env["MOZ_PROCESS_LOG"] = processLog
1034 if self.IS_TEST_BUILD and runSSLTunnel:
1035 # create certificate database for the profile
1036 certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
1037 if certificateStatus != 0:
1038 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Certificate integration failed")
1039 return certificateStatus
1041 # start ssltunnel to provide https:// URLs capability
1042 ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
1043 ssltunnelProcess = self.Process([ssltunnel,
1044 os.path.join(profileDir, "ssltunnel.cfg")],
1045 env = self.environment(xrePath = xrePath))
1046 self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
1048 cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
1049 startTime = datetime.now()
1051 if debuggerInfo and debuggerInfo["interactive"]:
1052 # If an interactive debugger is attached, don't redirect output,
1053 # don't use timeouts, and don't capture ctrl-c.
1054 timeout = None
1055 maxTime = None
1056 outputPipe = None
1057 signal.signal(signal.SIGINT, lambda sigid, frame: None)
1058 else:
1059 outputPipe = subprocess.PIPE
1061 self.lastTestSeen = "automation.py"
1062 proc = self.Process([cmd] + args,
1063 env = self.environment(env, xrePath = xrePath,
1064 crashreporter = not debuggerInfo),
1065 stdout = outputPipe,
1066 stderr = subprocess.STDOUT)
1067 self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
1069 status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
1070 self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
1072 # Do a final check for zombie child processes.
1073 zombieProcesses = self.checkForZombies(processLog)
1075 crashed = self.checkForCrashes(profileDir, symbolsPath)
1077 if crashed or zombieProcesses:
1078 status = 1
1080 if os.path.exists(processLog):
1081 os.unlink(processLog)
1083 if self.IS_TEST_BUILD and runSSLTunnel:
1084 ssltunnelProcess.kill()
1086 return status
1088 def getExtensionIDFromRDF(self, rdfSource):
1090 Retrieves the extension id from an install.rdf file (or string).
1092 from xml.dom.minidom import parse, parseString, Node
1094 if isinstance(rdfSource, file):
1095 document = parse(rdfSource)
1096 else:
1097 document = parseString(rdfSource)
1099 # Find the <em:id> element. There can be multiple <em:id> tags
1100 # within <em:targetApplication> tags, so we have to check this way.
1101 for rdfChild in document.documentElement.childNodes:
1102 if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description":
1103 for descChild in rdfChild.childNodes:
1104 if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id":
1105 return descChild.childNodes[0].data
1107 return None
1109 def installExtension(self, extensionSource, profileDir, extensionID = None):
1111 Copies an extension into the extensions directory of the given profile.
1112 extensionSource - the source location of the extension files. This can be either
1113 a directory or a path to an xpi file.
1114 profileDir - the profile directory we are copying into. We will create the
1115 "extensions" directory there if it doesn't exist.
1116 extensionID - the id of the extension to be used as the containing directory for the
1117 extension, if extensionSource is a directory, i.e.
1118 this is the name of the folder in the <profileDir>/extensions/<extensionID>
1120 if not os.path.isdir(profileDir):
1121 self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir)
1122 return
1124 installRDFFilename = "install.rdf"
1126 extensionsRootDir = os.path.join(profileDir, "extensions", "staged")
1127 if not os.path.isdir(extensionsRootDir):
1128 os.makedirs(extensionsRootDir)
1130 if os.path.isfile(extensionSource):
1131 reader = automationutils.ZipFileReader(extensionSource)
1133 for filename in reader.namelist():
1134 # Sanity check the zip file.
1135 if os.path.isabs(filename):
1136 self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi")
1137 return
1139 # We may need to dig the extensionID out of the zip file...
1140 if extensionID is None and filename == installRDFFilename:
1141 extensionID = self.getExtensionIDFromRDF(reader.read(filename))
1143 # We must know the extensionID now.
1144 if extensionID is None:
1145 self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
1146 return
1148 # Make the extension directory.
1149 extensionDir = os.path.join(extensionsRootDir, extensionID)
1150 os.mkdir(extensionDir)
1152 # Extract all files.
1153 reader.extractall(extensionDir)
1155 elif os.path.isdir(extensionSource):
1156 if extensionID is None:
1157 filename = os.path.join(extensionSource, installRDFFilename)
1158 if os.path.isfile(filename):
1159 with open(filename, "r") as installRDF:
1160 extensionID = self.getExtensionIDFromRDF(installRDF)
1162 if extensionID is None:
1163 self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
1164 return
1166 # Copy extension tree into its own directory.
1167 # "destination directory must not already exist".
1168 shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID))
1170 else:
1171 self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource)