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 # 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
43 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
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
59 import ctypes
, ctypes
.wintypes
, time
, msvcrt
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
)
77 class SyntaxError(Exception):
78 "Signifies a syntax error on a particular line in server-locations.txt."
80 def __init__(self
, lineno
, msg
= None):
85 s
= "Syntax error on line " + str(self
.lineno
)
87 s
+= ": %s." % self
.msg
94 "Represents a location line in server-locations.txt."
96 def __init__(self
, scheme
, host
, port
, options
):
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.
112 IS_CYGWIN
= _IS_CYGWIN
113 IS_CAMINO
= _IS_CAMINO
114 BIN_SUFFIX
= _BIN_SUFFIX
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
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
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.
185 universal_newlines
=False,
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
,
194 universal_newlines
, startupinfo
, creationflags
)
198 if Automation().IS_WIN32
:
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.)
205 subprocess
.Popen(["kill", "-f", pid
]).wait()
207 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid
)
209 # Windows XP and later.
210 subprocess
.Popen(["taskkill", "/F", "/PID", pid
]).wait()
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+.]*)"
228 r
"\d+\.\d+\.\d+\.\d+"
230 r
"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
231 r
"[a-z](?:[-a-z0-9]*[a-z0-9])?"
237 r
"(?P<options>\S+(?:,\S+)*)"
242 for line
in locationFile
:
244 if line
.startswith("#") or line
== "\n":
247 match
= lineRe
.match(line
)
249 raise SyntaxError(lineno
)
251 options
= match
.group("options")
253 options
= options
.split(",")
254 if "primary" in options
:
256 raise SyntaxError(lineno
, "multiple primary locations")
261 locations
.append(Location(match
.group("scheme"), match
.group("host"),
262 match
.group("port"), options
))
265 raise SyntaxError(lineno
+ 1, "missing primary location")
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,
285 isInBrowserElement INTEGER)""")
287 # Insert desired permissions
289 for perm
in permissions
.keys():
290 for host
,allow
in permissions
[perm
]:
292 cursor
.execute("INSERT INTO moz_hosts values(?, ?, ?, ?, 0, 0, 0, 0)",
293 (c
, host
, perm
, 1 if allow
else 2))
299 def setupTestApps(self
, profileDir
, apps
):
300 webappJSONTemplate
= Template(""""$name": {
302 "installOrigin": "$origin",
304 "installTime": 132333986000,
305 "manifestURL": "$manifestURL",
307 "appStatus": $appStatus,
311 manifestTemplate
= Template("""{
314 "description": "$description",
318 "url": "https://mozilla.org/"
325 "description": "$description"
328 "default_locale": "en-US",
334 # Create webapps/webapps.json
335 webappsDir
= os
.path
.join(profileDir
, "webapps")
336 os
.mkdir(webappsDir
);
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.
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
)
359 def initializeProfile(self
, profileDir
, extraPrefs
= [], useServerLocations
= False):
360 " Sets up the standard testing profile."
363 # Start with a clean slate.
364 shutil
.rmtree(profileDir
, True)
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
]});
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
) }
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+.]*)' +
475 '(?::(\\\\\\\\d+))?/');
476 var matches = regex.exec(url);
479 var isHttp = matches[1] == 'http';
480 var isHttps = matches[1] == 'https';
481 var isWebSocket = matches[1] == 'ws';
482 var isWebSocketSSL = matches[1] == 'wss';
485 if (isHttp | isWebSocket) matches[3] = '80';
486 if (isHttps | isWebSocketSSL) matches[3] = '443';
491 matches[1] = 'https';
493 var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
494 if (origins.indexOf(origin) < 0)
497 return 'PROXY %(remote)s:%(httpport)s';
498 if (isHttps || isWebSocket || isWebSocketSSL)
499 return 'PROXY %(remote)s:%(sslport)s';
501 }""" % { "origins": origins
,
502 "remote": self
.webServer
,
503 "httpport":self
.httpPort
,
504 "sslport": self
.sslPort
}
505 pacURL
= "".join(pacURL
.splitlines())
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
}
515 part
= 'user_pref("network.proxy.type", 0);\n'
519 thispref
= v
.split("=", 1)
520 if len(thispref
) < 2:
521 print "Error: syntax error in --setpref=" + v
523 part
= 'user_pref("%s", %s);\n' % (thispref
[0], thispref
[1])
526 # write the preferences
527 prefsFile
= open(profileDir
+ "/" + "user.js", "a")
528 prefsFile
.write("".join(prefs
))
533 'name': 'http_example_org',
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',
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',
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',
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',
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',
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',
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",
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")
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()
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
)
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
)
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
)
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()
679 # Walk the cert directory and add custom CAs and client certs
680 files
= os
.listdir(certPath
)
682 root
, ext
= os
.path
.splitext(item
)
685 if root
.endswith("-object"):
687 self
.Process([certutil
, "-A", "-i", os
.path
.join(certPath
, item
),
688 "-d", profileDir
, "-f", pwfilePath
, "-n", root
, "-t", trustBits
],
691 self
.Process([pk12util
, "-i", os
.path
.join(certPath
, item
), "-w",
692 pwfilePath
, "-d", profileDir
],
695 os
.unlink(pwfilePath
)
698 def environment(self
, env
= None, xrePath
= None, crashreporter
= True):
700 xrePath
= self
.DIST_BIN
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"
708 envVar
= "DYLD_LIBRARY_PATH"
710 env
['MOZILLA_FIVE_HOME'] = xrePath
712 ldLibraryPath
= ldLibraryPath
+ ":" + env
[envVar
]
713 env
[envVar
] = ldLibraryPath
715 env
["PATH"] = env
["PATH"] + ";" + ldLibraryPath
718 env
['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
719 env
['MOZ_CRASHREPORTER'] = '1'
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'
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."""
740 # shortcut to allow callers to pass in "None" for no timeout.
741 return (f
.readline(), False)
742 x
= msvcrt
.get_osfhandle(f
.fileno())
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
751 log
.error("readWithTimeout got error: %d", err
)
753 # we're assuming that the output is line-buffered,
754 # which is not unreasonable
755 return (f
.readline(), False)
759 def isPidAlive(self
, pid
):
761 PROCESS_QUERY_LIMITED_INFORMATION
= 0x1000
762 pHandle
= ctypes
.windll
.kernel32
.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION
, 0, pid
)
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
)
775 success
= ctypes
.windll
.kernel32
.TerminateProcess(pHandle
, 1)
776 ctypes
.windll
.kernel32
.CloseHandle(pHandle
)
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
)
788 return (f
.readline(), False)
790 def isPidAlive(self
, pid
):
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.
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
)
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
:
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
815 utility
= [os
.path
.join(utilityPath
, "screentopng")]
818 utility
= ['/usr/sbin/screencapture', '-C', '-x', '-t', 'png']
821 utility
= [os
.path
.join(utilityPath
, "screenshot.exe")]
824 # Run the capture correctly for the type of capture
826 if imgoutput
== 'file':
827 tmpfd
, imgfilename
= tempfile
.mkstemp(prefix
='mozilla-test-fail_')
829 dumper
= self
.Process(utility
+ [imgfilename
])
830 elif imgoutput
== 'stdout':
831 dumper
= self
.Process(utility
, bufsize
=-1,
832 stdout
=subprocess
.PIPE
, close_fds
=True)
834 self
.log
.info("Failed to start %s for screenshot: %s",
835 utility
[0], err
.strerror
)
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
)
845 if imgoutput
== 'stdout':
847 elif imgoutput
== 'file':
848 with
open(imgfilename
, 'rb') as imgfile
:
849 image
= imgfile
.read()
851 self
.log
.info("Failed to read image from %s", imgoutput
)
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."""
860 if self
.haveDumpedScreen
:
861 self
.log
.info("Not taking screenshot here: see the one that was previously logged")
863 self
.dumpScreen(utilityPath
)
865 if self
.CRASHREPORTER
and not debuggerInfo
:
867 # ABRT will get picked up by Breakpad's signal handler
868 os
.kill(proc
.pid
, signal
.SIGABRT
)
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:
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")
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
885 if proc
.stdout
is None:
886 self
.log
.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
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
)
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
)
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")],
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")
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().
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
)
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
)
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
)
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.
957 args
.extend(debuggerInfo
["args"])
959 cmd
= os
.path
.abspath(debuggerInfo
["path"])
962 args
.append("-foreground")
965 profileDirectory
= commands
.getoutput("cygpath -w \"" + profileDir
+ "/\"")
967 profileDirectory
= profileDir
+ "/"
969 args
.extend(("-no-remote", "-profile", profileDirectory
))
970 if testURL
is not None:
972 args
.extend(("-url", testURL
))
974 args
.append((testURL
))
975 args
.extend(extraArgs
)
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
986 self
.log
.info('INFO | automation.py | Reading PID log: %s', processLog
)
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
)
994 processList
.append(int(m
.group(1)))
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
):
1001 self
.log
.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID
)
1002 self
.killPid(processPID
)
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
1021 xrePath
= self
.DIST_BIN
1022 if certPath
== None:
1023 certPath
= self
.CERTS_SRC_DIR
1025 timeout
= self
.DEFAULT_TIMEOUT
1027 # copy env so we don't munge the caller's environment
1029 env
["NO_EM_RESTART"] = "1"
1030 tmpfd
, processLog
= tempfile
.mkstemp(suffix
='pidlog')
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.
1057 signal
.signal(signal
.SIGINT
, lambda sigid
, frame
: None)
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
:
1080 if os
.path
.exists(processLog
):
1081 os
.unlink(processLog
)
1083 if self
.IS_TEST_BUILD
and runSSLTunnel
:
1084 ssltunnelProcess
.kill()
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
)
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
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
)
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")
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")
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")
1166 # Copy extension tree into its own directory.
1167 # "destination directory must not already exist".
1168 shutil
.copytree(extensionSource
, os
.path
.join(extensionsRootDir
, extensionID
))
1171 self
.log
.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource
)