diff options
Diffstat (limited to 'build/mobile/b2gautomation.py')
-rw-r--r-- | build/mobile/b2gautomation.py | 451 |
1 files changed, 0 insertions, 451 deletions
diff --git a/build/mobile/b2gautomation.py b/build/mobile/b2gautomation.py deleted file mode 100644 index a218090682..0000000000 --- a/build/mobile/b2gautomation.py +++ /dev/null @@ -1,451 +0,0 @@ -# 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 datetime -import mozcrash -import threading -import os -import posixpath -import Queue -import re -import shutil -import signal -import tempfile -import time -import traceback -import zipfile - -from automation import Automation -from mozlog import get_default_logger -from mozprocess import ProcessHandlerMixin - - -class StdOutProc(ProcessHandlerMixin): - """Process handler for b2g which puts all output in a Queue. - """ - - def __init__(self, cmd, queue, **kwargs): - self.queue = queue - kwargs.setdefault('processOutputLine', []).append(self.handle_output) - ProcessHandlerMixin.__init__(self, cmd, **kwargs) - - def handle_output(self, line): - self.queue.put_nowait(line) - - -class B2GRemoteAutomation(Automation): - _devicemanager = None - - def __init__(self, deviceManager, appName='', remoteLog=None, - marionette=None): - self._devicemanager = deviceManager - self._appName = appName - self._remoteProfile = None - self._remoteLog = remoteLog - self.marionette = marionette - self._is_emulator = False - self.test_script = None - self.test_script_args = None - - # Default our product to b2g - self._product = "b2g" - self.lastTestSeen = "b2gautomation.py" - # Default log finish to mochitest standard - self.logFinish = 'INFO SimpleTest FINISHED' - Automation.__init__(self) - - def setEmulator(self, is_emulator): - self._is_emulator = is_emulator - - def setDeviceManager(self, deviceManager): - self._devicemanager = deviceManager - - def setAppName(self, appName): - self._appName = appName - - def setRemoteProfile(self, remoteProfile): - self._remoteProfile = remoteProfile - - def setProduct(self, product): - self._product = product - - def setRemoteLog(self, logfile): - self._remoteLog = logfile - - def getExtensionIDFromRDF(self, rdfSource): - """ - Retrieves the extension id from an install.rdf file (or string). - """ - from xml.dom.minidom import parse, parseString, Node - - if isinstance(rdfSource, file): - document = parse(rdfSource) - else: - document = parseString(rdfSource) - - # Find the <em:id> element. There can be multiple <em:id> tags - # within <em:targetApplication> tags, so we have to check this way. - for rdfChild in document.documentElement.childNodes: - if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description": - for descChild in rdfChild.childNodes: - if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id": - return descChild.childNodes[0].data - return None - - def installExtension(self, extensionSource, profileDir, extensionID=None): - # Bug 827504 - installing special-powers extension separately causes problems in B2G - if extensionID != "special-powers@mozilla.org": - if not os.path.isdir(profileDir): - self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir) - return - - installRDFFilename = "install.rdf" - - extensionsRootDir = os.path.join(profileDir, "extensions", "staged") - if not os.path.isdir(extensionsRootDir): - os.makedirs(extensionsRootDir) - - if os.path.isfile(extensionSource): - reader = zipfile.ZipFile(extensionSource, "r") - - for filename in reader.namelist(): - # Sanity check the zip file. - if os.path.isabs(filename): - self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi") - return - - # We may need to dig the extensionID out of the zip file... - if extensionID is None and filename == installRDFFilename: - extensionID = self.getExtensionIDFromRDF(reader.read(filename)) - - # We must know the extensionID now. - if extensionID is None: - self.log.info("INFO | automation.py | Cannot install extension, missing extensionID") - return - - # Make the extension directory. - extensionDir = os.path.join(extensionsRootDir, extensionID) - os.mkdir(extensionDir) - - # Extract all files. - reader.extractall(extensionDir) - - elif os.path.isdir(extensionSource): - if extensionID is None: - filename = os.path.join(extensionSource, installRDFFilename) - if os.path.isfile(filename): - with open(filename, "r") as installRDF: - extensionID = self.getExtensionIDFromRDF(installRDF) - - if extensionID is None: - self.log.info("INFO | automation.py | Cannot install extension, missing extensionID") - return - - # Copy extension tree into its own directory. - # "destination directory must not already exist". - shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID)) - - else: - self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource) - - # Set up what we need for the remote environment - def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False): - # Because we are running remote, we don't want to mimic the local env - # so no copying of os.environ - if env is None: - env = {} - - # We always hide the results table in B2G; it's much slower if we don't. - env['MOZ_HIDE_RESULTS_TABLE'] = '1' - return env - - def waitForNet(self): - active = False - time_out = 0 - while not active and time_out < 40: - data = self._devicemanager._runCmd(['shell', '/system/bin/netcfg']).stdout.readlines() - data.pop(0) - for line in data: - if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)): - active = True - break - time_out += 1 - time.sleep(1) - return active - - def checkForCrashes(self, directory, symbolsPath): - crashed = False - remote_dump_dir = self._remoteProfile + '/minidumps' - print "checking for crashes in '%s'" % remote_dump_dir - if self._devicemanager.dirExists(remote_dump_dir): - local_dump_dir = tempfile.mkdtemp() - self._devicemanager.getDirectory(remote_dump_dir, local_dump_dir) - try: - logger = get_default_logger() - if logger is not None: - crashed = mozcrash.log_crashes(logger, local_dump_dir, symbolsPath, test=self.lastTestSeen) - else: - crashed = mozcrash.check_for_crashes(local_dump_dir, symbolsPath, test_name=self.lastTestSeen) - except: - traceback.print_exc() - finally: - shutil.rmtree(local_dump_dir) - self._devicemanager.removeDir(remote_dump_dir) - return crashed - - def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): - # if remote profile is specified, use that instead - if (self._remoteProfile): - profileDir = self._remoteProfile - - cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs) - - return app, args - - def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, - debuggerInfo, symbolsPath, outputHandler=None): - """ Wait for tests to finish (as evidenced by a signature string - in logcat), or for a given amount of time to elapse with no - output. - """ - timeout = timeout or 120 - while True: - lines = proc.getStdoutLines(timeout) - if lines: - currentlog = '\n'.join(lines) - - if outputHandler: - for line in lines: - outputHandler(line) - else: - print(currentlog) - - # Match the test filepath from the last TEST-START line found in the new - # log content. These lines are in the form: - # ... INFO TEST-START | /filepath/we/wish/to/capture.html\n - testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", currentlog) - if testStartFilenames: - self.lastTestSeen = testStartFilenames[-1] - if (outputHandler and outputHandler.suite_finished) or ( - hasattr(self, 'logFinish') and self.logFinish in currentlog): - return 0 - else: - self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed " - "out after %d seconds with no output", - self.lastTestSeen, int(timeout)) - self._devicemanager.killProcess('/system/b2g/b2g', sig=signal.SIGABRT) - - timeout = 10 # seconds - starttime = datetime.datetime.utcnow() - while datetime.datetime.utcnow() - starttime < datetime.timedelta(seconds=timeout): - if not self._devicemanager.processExist('/system/b2g/b2g'): - break - time.sleep(1) - else: - print "timed out after %d seconds waiting for b2g process to exit" % timeout - return 1 - - self.checkForCrashes(None, symbolsPath) - return 1 - - def getDeviceStatus(self, serial=None): - # Get the current status of the device. If we know the device - # serial number, we look for that, otherwise we use the (presumably - # only) device shown in 'adb devices'. - serial = serial or self._devicemanager._deviceSerial - status = 'unknown' - - for line in self._devicemanager._runCmd(['devices']).stdout.readlines(): - result = re.match('(.*?)\t(.*)', line) - if result: - thisSerial = result.group(1) - if not serial or thisSerial == serial: - serial = thisSerial - status = result.group(2) - - return (serial, status) - - def restartB2G(self): - # TODO hangs in subprocess.Popen without this delay - time.sleep(5) - self._devicemanager._checkCmd(['shell', 'stop', 'b2g']) - # Wait for a bit to make sure B2G has completely shut down. - time.sleep(10) - self._devicemanager._checkCmd(['shell', 'start', 'b2g']) - if self._is_emulator: - self.marionette.emulator.wait_for_port(self.marionette.port) - - def rebootDevice(self): - # find device's current status and serial number - serial, status = self.getDeviceStatus() - - # reboot! - self._devicemanager._runCmd(['shell', '/system/bin/reboot']) - - # The above command can return while adb still thinks the device is - # connected, so wait a little bit for it to disconnect from adb. - time.sleep(10) - - # wait for device to come back to previous status - print 'waiting for device to come back online after reboot' - start = time.time() - rserial, rstatus = self.getDeviceStatus(serial) - while rstatus != 'device': - if time.time() - start > 120: - # device hasn't come back online in 2 minutes, something's wrong - raise Exception("Device %s (status: %s) not back online after reboot" % (serial, rstatus)) - time.sleep(5) - rserial, rstatus = self.getDeviceStatus(serial) - print 'device:', serial, 'status:', rstatus - - def Process(self, cmd, stdout=None, stderr=None, env=None, cwd=None): - # On a desktop or fennec run, the Process method invokes a gecko - # process in which to the tests. For B2G, we simply - # reboot the device (which was configured with a test profile - # already), wait for B2G to start up, and then navigate to the - # test url using Marionette. There doesn't seem to be any way - # to pass env variables into the B2G process, but this doesn't - # seem to matter. - - # reboot device so it starts up with the mochitest profile - # XXX: We could potentially use 'stop b2g' + 'start b2g' to achieve - # a similar effect; will see which is more stable while attempting - # to bring up the continuous integration. - if not self._is_emulator: - self.rebootDevice() - time.sleep(5) - #wait for wlan to come up - if not self.waitForNet(): - raise Exception("network did not come up, please configure the network" + - " prior to running before running the automation framework") - - # stop b2g - self._devicemanager._runCmd(['shell', 'stop', 'b2g']) - time.sleep(5) - - # For some reason user.js in the profile doesn't get picked up. - # Manually copy it over to prefs.js. See bug 1009730 for more details. - self._devicemanager.moveTree(posixpath.join(self._remoteProfile, 'user.js'), - posixpath.join(self._remoteProfile, 'prefs.js')) - - # relaunch b2g inside b2g instance - instance = self.B2GInstance(self._devicemanager, env=env) - - time.sleep(5) - - # Set up port forwarding again for Marionette, since any that - # existed previously got wiped out by the reboot. - if not self._is_emulator: - self._devicemanager._checkCmd(['forward', - 'tcp:%s' % self.marionette.port, - 'tcp:%s' % self.marionette.port]) - - if self._is_emulator: - self.marionette.emulator.wait_for_port(self.marionette.port) - else: - time.sleep(5) - - # start a marionette session - session = self.marionette.start_session() - if 'b2g' not in session: - raise Exception("bad session value %s returned by start_session" % session) - - with self.marionette.using_context(self.marionette.CONTEXT_CHROME): - self.marionette.execute_script(""" - let SECURITY_PREF = "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer"; - Components.utils.import("resource://gre/modules/Services.jsm"); - 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.jsm", - testUtils); - testUtils.specialPowersObserver = new testUtils.SpecialPowersObserver(); - testUtils.specialPowersObserver.init(); - } - """) - - # run the script that starts the tests - if self.test_script: - if os.path.isfile(self.test_script): - script = open(self.test_script, 'r') - self.marionette.execute_script(script.read(), script_args=self.test_script_args) - script.close() - elif isinstance(self.test_script, basestring): - self.marionette.execute_script(self.test_script, script_args=self.test_script_args) - else: - # assumes the tests are started on startup automatically - pass - - return instance - - # be careful here as this inner class doesn't have access to outer class members - class B2GInstance(object): - """Represents a B2G instance running on a device, and exposes - some process-like methods/properties that are expected by the - automation. - """ - - def __init__(self, dm, env=None): - self.dm = dm - self.env = env or {} - self.stdout_proc = None - self.queue = Queue.Queue() - - # Launch b2g in a separate thread, and dump all output lines - # into a queue. The lines in this queue are - # retrieved and returned by accessing the stdout property of - # this class. - cmd = [self.dm._adbPath] - if self.dm._deviceSerial: - cmd.extend(['-s', self.dm._deviceSerial]) - cmd.append('shell') - for k, v in self.env.iteritems(): - cmd.append("%s=%s" % (k, v)) - cmd.append('/system/bin/b2g.sh') - proc = threading.Thread(target=self._save_stdout_proc, args=(cmd, self.queue)) - proc.daemon = True - proc.start() - - def _save_stdout_proc(self, cmd, queue): - self.stdout_proc = StdOutProc(cmd, queue) - self.stdout_proc.run() - if hasattr(self.stdout_proc, 'processOutput'): - self.stdout_proc.processOutput() - self.stdout_proc.wait() - self.stdout_proc = None - - @property - def pid(self): - # a dummy value to make the automation happy - return 0 - - def getStdoutLines(self, timeout): - # Return any lines in the queue used by the - # b2g process handler. - lines = [] - # get all of the lines that are currently available - while True: - try: - lines.append(self.queue.get_nowait()) - except Queue.Empty: - break - - # wait 'timeout' for any additional lines - if not lines: - try: - lines.append(self.queue.get(True, timeout)) - except Queue.Empty: - pass - return lines - - def wait(self, timeout=None): - # this should never happen - raise Exception("'wait' called on B2GInstance") - - def kill(self): - # this should never happen - raise Exception("'kill' called on B2GInstance") - |