# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import ConfigParser import os import shutil import sys import tempfile import threading import traceback try: import json except ImportError: import simplejson as json here = os.path.abspath(os.path.dirname(sys.argv[0])) sys.path.insert(0, here) from automation import Automation from b2gautomation import B2GRemoteAutomation, B2GDesktopAutomation from runtests import Mochitest from runtests import MochitestOptions from runtests import MochitestServer from marionette import Marionette from mozdevice import DeviceManagerADB, DMError from mozprofile import Profile, Preferences class B2GMochitest(Mochitest): def __init__(self, automation, OOP=True, profile_data_dir=None, locations=os.path.join(here, 'server-locations.txt')): Mochitest.__init__(self, automation) self.OOP = OOP self.locations = locations self.preferences = [] self.webapps = None if profile_data_dir: self.preferences = [os.path.join(profile_data_dir, f) for f in os.listdir(profile_data_dir) if f.startswith('pref')] self.webapps = [os.path.join(profile_data_dir, f) for f in os.listdir(profile_data_dir) if f.startswith('webapp')] def setupCommonOptions(self, options): # set the testURL testURL = self.buildTestPath(options) if len(self.urlOpts) > 0: testURL += "?" + "&".join(self.urlOpts) self.automation.testURL = testURL if self.OOP: OOP_script = """ let specialpowers = {}; let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader); loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js", specialpowers); let specialPowersObserver = new specialpowers.SpecialPowersObserver(); specialPowersObserver.init(); let mm = container.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; mm.addMessageListener("SPPrefService", specialPowersObserver); mm.addMessageListener("SPProcessCrashService", specialPowersObserver); mm.addMessageListener("SPPingService", specialPowersObserver); mm.addMessageListener("SpecialPowers.Quit", specialPowersObserver); mm.addMessageListener("SpecialPowers.Focus", specialPowersObserver); mm.addMessageListener("SPPermissionManager", specialPowersObserver); mm.loadFrameScript(CHILD_LOGGER_SCRIPT, true); mm.loadFrameScript(CHILD_SCRIPT_API, true); mm.loadFrameScript(CHILD_SCRIPT, true); specialPowersObserver._isFrameScriptLoaded = true; """ else: OOP_script = "" # Execute this script on start up: loads special powers and sets # the test-container apps's iframe to the mochitest URL. self.automation.test_script = """ const CHILD_SCRIPT = "chrome://specialpowers/content/specialpowers.js"; const CHILD_SCRIPT_API = "chrome://specialpowers/content/specialpowersAPI.js"; const CHILD_LOGGER_SCRIPT = "chrome://specialpowers/content/MozillaLogger.js"; let homescreen = document.getElementById('homescreen'); let container = homescreen.contentWindow.document.getElementById('test-container'); function openWindow(aEvent) { var popupIframe = aEvent.detail.frameElement; popupIframe.setAttribute('style', 'position: absolute; left: 0; top: 300px; background: white; '); popupIframe.addEventListener('mozbrowserclose', function(e) { container.parentNode.removeChild(popupIframe); container.focus(); }); // yes, the popup can call window.open too! popupIframe.addEventListener('mozbrowseropenwindow', openWindow); popupIframe.addEventListener('mozbrowserloadstart', function(e) { popupIframe.focus(); }); container.parentNode.appendChild(popupIframe); } container.addEventListener('mozbrowseropenwindow', openWindow); %s container.src = '%s'; """ % (OOP_script, testURL) def buildProfile(self, options): # preferences prefs = {} for path in self.preferences: prefs.update(Preferences.read_prefs(path)) for v in options.extraPrefs: thispref = v.split("=", 1) if len(thispref) < 2: print "Error: syntax error in --setpref=" + v sys.exit(1) prefs[thispref[0]] = thispref[1] # interpolate the preferences interpolation = { "server": "%s:%s" % (options.webServer, options.httpPort), "OOP": "true" if self.OOP else "false" } prefs = json.loads(json.dumps(prefs) % interpolation) for pref in prefs: prefs[pref] = Preferences.cast(prefs[pref]) kwargs = { 'addons': self.getExtensionsToInstall(options), 'apps': self.webapps, 'locations': self.locations, 'preferences': prefs, 'proxy': {"remote": options.webServer} } if options.profile: self.profile = Profile.clone(options.profile, **kwargs) else: self.profile = Profile(**kwargs) options.profilePath = self.profile.profile # TODO bug 839108 - mozprofile should probably handle this manifest = self.addChromeToProfile(options) self.copyExtraFilesToProfile(options) return manifest class B2GOptions(MochitestOptions): def __init__(self, automation, scriptdir, **kwargs): defaults = {} MochitestOptions.__init__(self, automation, scriptdir) self.add_option("--b2gpath", action="store", type="string", dest="b2gPath", help="path to B2G repo or qemu dir") defaults["b2gPath"] = None self.add_option("--desktop", action="store_true", dest="desktop", help="Run the tests on a B2G desktop build") defaults["desktop"] = False self.add_option("--marionette", action="store", type="string", dest="marionette", help="host:port to use when connecting to Marionette") defaults["marionette"] = None self.add_option("--emulator", action="store", type="string", dest="emulator", help="Architecture of emulator to use: x86 or arm") defaults["emulator"] = None self.add_option("--sdcard", action="store", type="string", dest="sdcard", help="Define size of sdcard: 1MB, 50MB...etc") defaults["sdcard"] = "10MB" self.add_option("--no-window", action="store_true", dest="noWindow", help="Pass --no-window to the emulator") defaults["noWindow"] = False self.add_option("--adbpath", action="store", type="string", dest="adbPath", help="path to adb") defaults["adbPath"] = "adb" self.add_option("--deviceIP", action="store", type="string", dest="deviceIP", help="ip address of remote device to test") defaults["deviceIP"] = None self.add_option("--devicePort", action="store", type="string", dest="devicePort", help="port of remote device to test") defaults["devicePort"] = 20701 self.add_option("--remote-logfile", action="store", type="string", dest="remoteLogFile", help="Name of log file on the device relative to the device root. PLEASE ONLY USE A FILENAME.") defaults["remoteLogFile"] = None self.add_option("--remote-webserver", action="store", type="string", dest="remoteWebServer", help="ip address where the remote web server is hosted at") defaults["remoteWebServer"] = None self.add_option("--http-port", action="store", type="string", dest="httpPort", help="ip address where the remote web server is hosted at") defaults["httpPort"] = automation.DEFAULT_HTTP_PORT self.add_option("--ssl-port", action="store", type="string", dest="sslPort", help="ip address where the remote web server is hosted at") defaults["sslPort"] = automation.DEFAULT_SSL_PORT self.add_option("--pidfile", action="store", type="string", dest="pidFile", help="name of the pidfile to generate") defaults["pidFile"] = "" self.add_option("--gecko-path", action="store", type="string", dest="geckoPath", help="the path to a gecko distribution that should " "be installed on the emulator prior to test") defaults["geckoPath"] = None self.add_option("--profile", action="store", type="string", dest="profile", help="for desktop testing, the path to the " "gaia profile to use") defaults["profile"] = None self.add_option("--logcat-dir", action="store", type="string", dest="logcat_dir", help="directory to store logcat dump files") defaults["logcat_dir"] = None self.add_option('--busybox', action='store', type='string', dest='busybox', help="Path to busybox binary to install on device") defaults['busybox'] = None self.add_option('--profile-data-dir', action='store', type='string', dest='profile_data_dir', help="Path to a directory containing preference and other " "data to be installed into the profile") defaults['profile_data_dir'] = os.path.join(here, 'profile_data') defaults["remoteTestRoot"] = "/data/local/tests" defaults["logFile"] = "mochitest.log" defaults["autorun"] = True defaults["closeWhenDone"] = True defaults["testPath"] = "" defaults["extensionsToExclude"] = ["specialpowers"] self.set_defaults(**defaults) def verifyRemoteOptions(self, options, automation): if not options.remoteTestRoot: options.remoteTestRoot = automation._devicemanager.getDeviceRoot() productRoot = options.remoteTestRoot + "/" + automation._product if options.utilityPath == self._automation.DIST_BIN: options.utilityPath = productRoot + "/bin" if options.remoteWebServer == None: if os.name != "nt": options.remoteWebServer = automation.getLanIp() else: self.error("You must specify a --remote-webserver=") options.webServer = options.remoteWebServer if options.geckoPath and not options.emulator: self.error("You must specify --emulator if you specify --gecko-path") if options.logcat_dir and not options.emulator: self.error("You must specify --emulator if you specify --logcat-dir") #if not options.emulator and not options.deviceIP: # print "ERROR: you must provide a device IP" # return None if options.remoteLogFile == None: options.remoteLogFile = options.remoteTestRoot + '/logs/mochitest.log' if options.remoteLogFile.count('/') < 1: options.remoteLogFile = options.remoteTestRoot + '/' + options.remoteLogFile # Only reset the xrePath if it wasn't provided if options.xrePath == None: options.xrePath = options.utilityPath if not os.path.isdir(options.xrePath): self.error("--xre-path '%s' is not a directory" % options.xrePath) xpcshell = os.path.join(options.xrePath, 'xpcshell') if not os.access(xpcshell, os.F_OK): self.error('xpcshell not found at %s' % xpcshell) if automation.elf_arm(xpcshell): self.error('--xre-path points to an ARM version of xpcshell; it ' 'should instead point to a version that can run on ' 'your desktop') if options.pidFile != "": f = open(options.pidFile, 'w') f.write("%s" % os.getpid()) f.close() return options def verifyOptions(self, options, mochitest): # since we are reusing verifyOptions, it will exit if App is not found temp = options.app options.app = sys.argv[0] tempPort = options.httpPort tempSSL = options.sslPort tempIP = options.webServer options = MochitestOptions.verifyOptions(self, options, mochitest) options.webServer = tempIP options.app = temp options.sslPort = tempSSL options.httpPort = tempPort return options class ProfileConfigParser(ConfigParser.RawConfigParser): """Subclass of RawConfigParser that outputs .ini files in the exact format expected for profiles.ini, which is slightly different than the default format. """ def optionxform(self, optionstr): return optionstr def write(self, fp): if self._defaults: fp.write("[%s]\n" % ConfigParser.DEFAULTSECT) for (key, value) in self._defaults.items(): fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t'))) fp.write("\n") for section in self._sections: fp.write("[%s]\n" % section) for (key, value) in self._sections[section].items(): if key == "__name__": continue if (value is not None) or (self._optcre == self.OPTCRE): key = "=".join((key, str(value).replace('\n', '\n\t'))) fp.write("%s\n" % (key)) fp.write("\n") class B2GDeviceMochitest(B2GMochitest): _automation = None _dm = None def __init__(self, automation, devmgr, options): self._automation = automation B2GMochitest.__init__(self, automation, OOP=True, profile_data_dir=options.profile_data_dir) self._dm = devmgr self.runSSLTunnel = False self.remoteProfile = options.remoteTestRoot + '/profile' self._automation.setRemoteProfile(self.remoteProfile) self.remoteLog = options.remoteLogFile self.localLog = None self.userJS = '/data/local/user.js' self.remoteMozillaPath = '/data/b2g/mozilla' self.bundlesDir = '/system/b2g/distribution/bundles' self.remoteProfilesIniPath = os.path.join(self.remoteMozillaPath, 'profiles.ini') self.originalProfilesIni = None def copyRemoteFile(self, src, dest): self._dm._checkCmdAs(['shell', 'dd', 'if=%s' % src, 'of=%s' % dest]) def origUserJSExists(self): return self._dm.fileExists('/data/local/user.js.orig') def cleanup(self, manifest, options): if self.localLog: self._dm.getFile(self.remoteLog, self.localLog) self._dm.removeFile(self.remoteLog) # Delete any bundled extensions extensionDir = os.path.join(options.profilePath, 'extensions', 'staged') if os.access(extensionDir, os.F_OK): for filename in os.listdir(extensionDir): try: self._dm._checkCmdAs(['shell', 'rm', '-rf', os.path.join(self.bundlesDir, filename)]) except DMError: pass if not options.emulator: # Remove the test profile self._dm._checkCmdAs(['shell', 'rm', '-r', self.remoteProfile]) if self.origUserJSExists(): # Restore the original user.js self._dm.removeFile(self.userJS) self.copyRemoteFile('%s.orig' % self.userJS, self.userJS) self._dm.removeFile("%s.orig" % self.userJS) if self._dm.fileExists('%s.orig' % self.remoteProfilesIniPath): # Restore the original profiles.ini self._dm.removeFile(self.remoteProfilesIniPath) self.copyRemoteFile('%s.orig' % self.remoteProfilesIniPath, self.remoteProfilesIniPath) self._dm.removeFile("%s.orig" % self.remoteProfilesIniPath) # We've restored the original profile, so reboot the device so that # it gets picked up. self._automation.rebootDevice() if options.pidFile != "": try: os.remove(options.pidFile) os.remove(options.pidFile + ".xpcshell.pid") except: print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % options.pidFile def findPath(self, paths, filename=None): for path in paths: p = path if filename: p = os.path.join(p, filename) if os.path.exists(self.getFullPath(p)): return path return None def startWebServer(self, options): """ Create the webserver on the host and start it up """ remoteXrePath = options.xrePath remoteProfilePath = options.profilePath remoteUtilityPath = options.utilityPath localAutomation = Automation() localAutomation.IS_WIN32 = False localAutomation.IS_LINUX = False localAutomation.IS_MAC = False localAutomation.UNIXISH = False hostos = sys.platform if hostos in ['mac', 'darwin']: localAutomation.IS_MAC = True elif hostos in ['linux', 'linux2']: localAutomation.IS_LINUX = True localAutomation.UNIXISH = True elif hostos in ['win32', 'win64']: localAutomation.BIN_SUFFIX = ".exe" localAutomation.IS_WIN32 = True paths = [options.xrePath, localAutomation.DIST_BIN, self._automation._product, os.path.join('..', self._automation._product)] options.xrePath = self.findPath(paths) if options.xrePath == None: print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name) sys.exit(1) paths.append("bin") paths.append(os.path.join("..", "bin")) xpcshell = "xpcshell" if (os.name == "nt"): xpcshell += ".exe" if (options.utilityPath): paths.insert(0, options.utilityPath) options.utilityPath = self.findPath(paths, xpcshell) if options.utilityPath == None: print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name) sys.exit(1) options.profilePath = tempfile.mkdtemp() self.server = MochitestServer(localAutomation, options) self.server.start() if (options.pidFile != ""): f = open(options.pidFile + ".xpcshell.pid", 'w') f.write("%s" % self.server._process.pid) f.close() self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT) options.xrePath = remoteXrePath options.utilityPath = remoteUtilityPath options.profilePath = remoteProfilePath def stopWebServer(self, options): if hasattr(self, 'server'): self.server.stop() def updateProfilesIni(self, profilePath): # update profiles.ini on the device to point to the test profile self.originalProfilesIni = tempfile.mktemp() self._dm.getFile(self.remoteProfilesIniPath, self.originalProfilesIni) config = ProfileConfigParser() config.read(self.originalProfilesIni) for section in config.sections(): if 'Profile' in section: config.set(section, 'IsRelative', 0) config.set(section, 'Path', profilePath) newProfilesIni = tempfile.mktemp() with open(newProfilesIni, 'wb') as configfile: config.write(configfile) self._dm.pushFile(newProfilesIni, self.remoteProfilesIniPath) self._dm.pushFile(self.originalProfilesIni, '%s.orig' % self.remoteProfilesIniPath) try: os.remove(newProfilesIni) os.remove(self.originalProfilesIni) except: pass def buildURLOptions(self, options, env): self.localLog = options.logFile options.logFile = self.remoteLog options.profilePath = self.profile.profile retVal = Mochitest.buildURLOptions(self, options, env) self.setupCommonOptions(options) # Copy the profile to the device. self._dm._checkCmdAs(['shell', 'rm', '-r', self.remoteProfile]) try: self._dm.pushDir(options.profilePath, self.remoteProfile) except DMError: print "Automation Error: Unable to copy profile to device." raise # Copy the extensions to the B2G bundles dir. extensionDir = os.path.join(options.profilePath, 'extensions', 'staged') # need to write to read-only dir self._dm._checkCmdAs(['remount']) for filename in os.listdir(extensionDir): self._dm._checkCmdAs(['shell', 'rm', '-rf', os.path.join(self.bundlesDir, filename)]) try: self._dm.pushDir(extensionDir, self.bundlesDir) except DMError: print "Automation Error: Unable to copy extensions to device." raise # In B2G, user.js is always read from /data/local, not the profile # directory. Backup the original user.js first so we can restore it. if not self._dm.fileExists('%s.orig' % self.userJS): self.copyRemoteFile(self.userJS, '%s.orig' % self.userJS) self._dm.pushFile(os.path.join(options.profilePath, "user.js"), self.userJS) self.updateProfilesIni(self.remoteProfile) options.profilePath = self.remoteProfile options.logFile = self.localLog return retVal class B2GDesktopMochitest(B2GMochitest): def __init__(self, automation, options): B2GMochitest.__init__(self, automation, OOP=False, profile_data_dir=options.profile_data_dir) def runMarionetteScript(self, marionette, test_script): assert(marionette.wait_for_port()) marionette.start_session() marionette.set_context(marionette.CONTEXT_CHROME) marionette.execute_script(test_script) def startTests(self): # This is run in a separate thread because otherwise, the app's # stdout buffer gets filled (which gets drained only after this # function returns, by waitForFinish), which causes the app to hang. thread = threading.Thread(target=self.runMarionetteScript, args=(self.automation.marionette, self.automation.test_script)) thread.start() def buildURLOptions(self, options, env): retVal = Mochitest.buildURLOptions(self, options, env) self.setupCommonOptions(options) # Copy the extensions to the B2G bundles dir. extensionDir = os.path.join(options.profilePath, 'extensions', 'staged') bundlesDir = os.path.join(os.path.dirname(options.app), 'distribution', 'bundles') for filename in os.listdir(extensionDir): shutil.rmtree(os.path.join(bundlesDir, filename), True) shutil.copytree(os.path.join(extensionDir, filename), os.path.join(bundlesDir, filename)) return retVal def run_remote_mochitests(automation, parser, options): # create our Marionette instance kwargs = {} if options.emulator: kwargs['emulator'] = options.emulator automation.setEmulator(True) if options.noWindow: kwargs['noWindow'] = True if options.geckoPath: kwargs['gecko_path'] = options.geckoPath if options.logcat_dir: kwargs['logcat_dir'] = options.logcat_dir if options.busybox: kwargs['busybox'] = options.busybox if options.symbolsPath: kwargs['symbols_path'] = options.symbolsPath # needless to say sdcard is only valid if using an emulator if options.sdcard: kwargs['sdcard'] = options.sdcard if options.b2gPath: kwargs['homedir'] = options.b2gPath if options.marionette: host, port = options.marionette.split(':') kwargs['host'] = host kwargs['port'] = int(port) marionette = Marionette.getMarionetteOrExit(**kwargs) automation.marionette = marionette # create the DeviceManager kwargs = {'adbPath': options.adbPath, 'deviceRoot': options.remoteTestRoot} if options.deviceIP: kwargs.update({'host': options.deviceIP, 'port': options.devicePort}) dm = DeviceManagerADB(**kwargs) automation.setDeviceManager(dm) options = parser.verifyRemoteOptions(options, automation) if (options == None): print "ERROR: Invalid options specified, use --help for a list of valid options" sys.exit(1) automation.setProduct("b2g") mochitest = B2GDeviceMochitest(automation, dm, options) options = parser.verifyOptions(options, mochitest) if (options == None): sys.exit(1) logParent = os.path.dirname(options.remoteLogFile) dm.mkDir(logParent) automation.setRemoteLog(options.remoteLogFile) automation.setServerInfo(options.webServer, options.httpPort, options.sslPort) retVal = 1 try: mochitest.cleanup(None, options) retVal = mochitest.runTests(options) except: print "Automation Error: Exception caught while running tests" traceback.print_exc() mochitest.stopWebServer(options) mochitest.stopWebSocketServer(options) try: mochitest.cleanup(None, options) except: pass retVal = 1 sys.exit(retVal) def run_desktop_mochitests(parser, options): automation = B2GDesktopAutomation() # create our Marionette instance kwargs = {} if options.marionette: host, port = options.marionette.split(':') kwargs['host'] = host kwargs['port'] = int(port) marionette = Marionette.getMarionetteOrExit(**kwargs) automation.marionette = marionette mochitest = B2GDesktopMochitest(automation, options) # b2g desktop builds don't always have a b2g-bin file if options.app[-4:] == '-bin': options.app = options.app[:-4] options = MochitestOptions.verifyOptions(parser, options, mochitest) if options == None: sys.exit(1) if options.desktop and not options.profile: raise Exception("must specify --profile when specifying --desktop") automation.setServerInfo(options.webServer, options.httpPort, options.sslPort, options.webSocketPort) sys.exit(mochitest.runTests(options, onLaunch=mochitest.startTests)) def main(): scriptdir = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) automation = B2GRemoteAutomation(None, "fennec") parser = B2GOptions(automation, scriptdir) options, args = parser.parse_args() if options.desktop: run_desktop_mochitests(parser, options) else: run_remote_mochitests(automation, parser, options) if __name__ == "__main__": main()