# 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 json import os import posixpath import shutil import sys import tempfile import threading import traceback here = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, here) from runtests import Mochitest from runtests import MochitestUtilsMixin from mochitest_options import MochitestArgumentParser from marionette import Marionette from mozprofile import Profile, Preferences import mozinfo import mozleak class B2GMochitest(MochitestUtilsMixin): marionette = None def __init__(self, marionette_args, logger_options, out_of_process=True, profile_data_dir=None, locations=os.path.join(here, 'server-locations.txt')): super(B2GMochitest, self).__init__(logger_options) self.marionette_args = marionette_args self.out_of_process = out_of_process self.locations_file = locations self.preferences = [] self.webapps = None self.test_script = os.path.join(here, 'b2g_start_script.js') self.test_script_args = [self.out_of_process] self.product = 'b2g' self.remote_chrome_test_dir = 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')] # mozinfo is populated by the parent class if mozinfo.info['debug']: self.SERVER_STARTUP_TIMEOUT = 180 else: self.SERVER_STARTUP_TIMEOUT = 90 def setup_common_options(self, options): test_url = self.buildTestPath(options) # For B2G emulators buildURLOptions has been called # without calling buildTestPath first and that # causes manifestFile not to be set if not "manifestFile=tests.json" in self.urlOpts: self.urlOpts.append("manifestFile=%s" % options.manifestFile) if len(self.urlOpts) > 0: test_url += "?" + "&".join(self.urlOpts) self.test_script_args.append(test_url) def buildTestPath(self, options, testsToFilter=None): if options.manifestFile != 'tests.json': super(B2GMochitest, self).buildTestPath(options, testsToFilter, disabled=False) return self.buildTestURL(options) def build_profile(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.out_of_process 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_file, '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 def run_tests(self, options): """ Prepare, configure, run tests and cleanup """ self.setTestRoot(options) manifest = self.build_profile(options) self.logPreamble(self.getActiveTests(options)) # configuring the message logger's buffering self.message_logger.buffering = options.quiet if options.debugger or not options.autorun: timeout = None else: if not options.timeout: if mozinfo.info['debug']: options.timeout = 420 else: options.timeout = 300 timeout = options.timeout + 30.0 self.log.info("runtestsb2g.py | Running tests: start.") status = 0 try: def on_output(line): messages = self.message_logger.write(line) for message in messages: if message['action'] == 'test_start': self.runner.last_test = message['test'] # The logging will be handled by on_output, so we set the stream to # None process_args = {'processOutputLine': on_output, 'stream': None} self.marionette_args['process_args'] = process_args self.marionette_args['profile'] = self.profile self.marionette = Marionette(**self.marionette_args) self.runner = self.marionette.runner self.app_ctx = self.runner.app_ctx self.remote_log = posixpath.join(self.app_ctx.remote_test_root, 'log', 'mochitest.log') if not self.app_ctx.dm.dirExists( posixpath.dirname( self.remote_log)): self.app_ctx.dm.mkDirs(self.remote_log) if options.chrome: # Update chrome manifest file in profile with correct path. self.writeChromeManifest(options) self.leak_report_file = posixpath.join( self.app_ctx.remote_test_root, 'log', 'runtests_leaks.log') # We don't want to copy the host env onto the device, so pass in an # empty env. self.browserEnv = self.buildBrowserEnv(options, env={}) # B2G emulator debug tests still make external connections, so don't # pass MOZ_DISABLE_NONLOCAL_CONNECTIONS to them for now (bug # 1039019). if mozinfo.info[ 'debug'] and 'MOZ_DISABLE_NONLOCAL_CONNECTIONS' in self.browserEnv: del self.browserEnv['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] self.runner.env.update(self.browserEnv) # Despite our efforts to clean up servers started by this script, in practice # we still see infrequent cases where a process is orphaned and interferes # with future tests, typically because the old server is keeping the port in use. # Try to avoid those failures by checking for and killing orphan servers before # trying to start new ones. self.killNamedOrphans('ssltunnel') self.killNamedOrphans('xpcshell') self.startServers(options, None) # In desktop mochitests buildTestPath is called before buildURLOptions. This # means options.manifestFile has already been converted to the proper json # style manifest. Not so with B2G, that conversion along with updating the URL # option will happen later. So backup and restore options.manifestFile to # prevent us from trying to pass in an instance of TestManifest via url param. manifestFile = options.manifestFile options.manifestFile = None self.buildURLOptions(options, {'MOZ_HIDE_RESULTS_TABLE': '1'}) options.manifestFile = manifestFile self.test_script_args.append(not options.emulator) self.test_script_args.append(options.wifi) self.test_script_args.append(options.chrome) self.runner.start(outputTimeout=timeout) self.marionette.wait_for_port() self.marionette.start_session() self.marionette.set_context(self.marionette.CONTEXT_CHROME) # Disable offline status management (bug 777145), otherwise the network # will be 'offline' when the mochitests start. Presumably, the network # won't be offline on a real device, so we only do this for # emulators. self.marionette.execute_script(""" Components.utils.import("resource://gre/modules/Services.jsm"); Services.io.manageOfflineStatus = false; Services.io.offline = false; """) self.marionette.execute_script(""" let SECURITY_PREF = "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer"; Services.prefs.setBoolPref(SECURITY_PREF, true); if (!testUtils.hasOwnProperty("specialPowersObserver")) { let loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] .getService(Components.interfaces.mozIJSSubScriptLoader); loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js", testUtils); testUtils.specialPowersObserver = new testUtils.SpecialPowersObserver(); testUtils.specialPowersObserver.init(); testUtils.specialPowersObserver._loadFrameScript(); } """) if options.chrome: self.app_ctx.dm.removeDir(self.remote_chrome_test_dir) self.app_ctx.dm.mkDir(self.remote_chrome_test_dir) local = super(B2GMochitest, self).getChromeTestDir(options) local = os.path.join(local, "chrome") remote = self.remote_chrome_test_dir self.log.info( "pushing %s to %s on device..." % (local, remote)) self.app_ctx.dm.pushDir(local, remote) if os.path.isfile(self.test_script): with open(self.test_script, 'r') as script: self.marionette.execute_script( script.read(), script_args=self.test_script_args) else: self.marionette.execute_script( self.test_script, script_args=self.test_script_args) status = self.runner.wait() if status is None: # the runner has timed out status = 124 local_leak_file = tempfile.NamedTemporaryFile() self.app_ctx.dm.getFile( self.leak_report_file, local_leak_file.name) self.app_ctx.dm.removeFile(self.leak_report_file) mozleak.process_leak_log( local_leak_file.name, leak_thresholds=options.leakThresholds, ignore_missing_leaks=options.ignoreMissingLeaks, log=self.log, ) except KeyboardInterrupt: self.log.info("runtests.py | Received keyboard interrupt.\n") status = -1 except: traceback.print_exc() self.log.error( "Automation Error: Received unexpected exception while running application\n") if hasattr(self, 'runner'): self.runner.check_for_crashes() status = 1 self.stopServers() self.log.info("runtestsb2g.py | Running tests: end.") if manifest is not None: self.cleanup(manifest, options) return status def getGMPPluginPath(self, options): if options.gmp_path: return options.gmp_path return '/system/b2g/gmp-clearkey/0.1' def getChromeTestDir(self, options): # The chrome test directory returned here is the remote location # of chrome test files. A reference to this directory is requested # when building the profile locally, before self.app_ctx is defined. # To get around this, return a dummy directory until self.app_ctx # is defined; the correct directory will be returned later, over- # writing the dummy. if hasattr(self, 'app_ctx'): self.remote_chrome_test_dir = posixpath.join( self.app_ctx.remote_test_root, 'chrome') return self.remote_chrome_test_dir return 'dummy-chrome-test-dir' class B2GDeviceMochitest(B2GMochitest, Mochitest): remote_log = None def __init__( self, marionette_args, logger_options, profile_data_dir, local_binary_dir, remote_test_root=None, remote_log_file=None): B2GMochitest.__init__( self, marionette_args, logger_options, out_of_process=True, profile_data_dir=profile_data_dir) self.local_log = None self.local_binary_dir = local_binary_dir def cleanup(self, manifest, options): if self.local_log: self.app_ctx.dm.getFile(self.remote_log, self.local_log) self.app_ctx.dm.removeFile(self.remote_log) 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 # stop and clean up the runner if getattr(self, 'runner', False): if self.local_log: self.app_ctx.dm.getFile(self.remote_log, self.local_log) self.app_ctx.dm.removeFile(self.remote_log) self.runner.cleanup() self.runner = None def startServers(self, options, debuggerInfo): """ Create the servers on the host and start them up """ savedXre = options.xrePath savedUtility = options.utilityPath savedProfie = options.profilePath options.xrePath = self.local_binary_dir options.utilityPath = self.local_binary_dir options.profilePath = tempfile.mkdtemp() MochitestUtilsMixin.startServers(self, options, debuggerInfo) options.xrePath = savedXre options.utilityPath = savedUtility options.profilePath = savedProfie def buildURLOptions(self, options, env): self.local_log = options.logFile options.logFile = self.remote_log options.profilePath = self.profile.profile super(B2GDeviceMochitest, self).buildURLOptions(options, env) self.setup_common_options(options) options.profilePath = self.app_ctx.remote_profile options.logFile = self.local_log class B2GDesktopMochitest(B2GMochitest, Mochitest): def __init__(self, marionette_args, logger_options, profile_data_dir): B2GMochitest.__init__( self, marionette_args, logger_options, out_of_process=False, profile_data_dir=profile_data_dir) Mochitest.__init__(self, logger_options) self.certdbNew = True def runMarionetteScript(self, marionette, test_script, test_script_args): assert(marionette.wait_for_port()) marionette.start_session() marionette.set_context(marionette.CONTEXT_CHROME) if os.path.isfile(test_script): f = open(test_script, 'r') test_script = f.read() f.close() self.marionette.execute_script(test_script, script_args=test_script_args) 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. self.marionette = Marionette(**self.marionette_args) thread = threading.Thread(target=self.runMarionetteScript, args=(self.marionette, self.test_script, self.test_script_args)) thread.start() def buildURLOptions(self, options, env): super(B2GDesktopMochitest, self).buildURLOptions(options, env) self.setup_common_options(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)) def buildProfile(self, options): return self.build_profile(options) def run_remote_mochitests(options): # create our Marionette instance marionette_args = { 'adb_path': options.adbPath, 'emulator': options.emulator, 'no_window': options.noWindow, 'logdir': options.logdir, 'busybox': options.busybox, 'symbols_path': options.symbolsPath, 'sdcard': options.sdcard, 'homedir': options.b2gPath, } if options.marionette: host, port = options.marionette.split(':') marionette_args['host'] = host marionette_args['port'] = int(port) if (options is None): print "ERROR: Invalid options specified, use --help for a list of valid options" sys.exit(1) mochitest = B2GDeviceMochitest( marionette_args, options, options.profile_data_dir, options.xrePath, remote_log_file=options.remoteLogFile) if (options is None): sys.exit(1) retVal = 1 try: mochitest.cleanup(None, options) retVal = mochitest.run_tests(options) except: print "Automation Error: Exception caught while running tests" traceback.print_exc() mochitest.stopServers() try: mochitest.cleanup(None, options) except: pass retVal = 1 mochitest.message_logger.finish() return retVal def run_desktop_mochitests(options): # create our Marionette instance marionette_args = {} if options.marionette: host, port = options.marionette.split(':') marionette_args['host'] = host marionette_args['port'] = int(port) # add a -bin suffix if b2g-bin exists, but just b2g was specified if options.app[-4:] != '-bin': if os.path.isfile("%s-bin" % options.app): options.app = "%s-bin" % options.app mochitest = B2GDesktopMochitest( marionette_args, options, options.profile_data_dir) if options is None: sys.exit(1) if options.desktop and not options.profile: raise Exception("must specify --profile when specifying --desktop") options.browserArgs += ['-marionette'] options.runByDir = False retVal = mochitest.runTests(options, onLaunch=mochitest.startTests) mochitest.message_logger.finish() return retVal def main(): parser = MochitestArgumentParser(app='b2g') options = parser.parse_args() if options.desktop: return run_desktop_mochitests(options) else: return run_remote_mochitests(options) if __name__ == "__main__": sys.exit(main())