123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- # 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")
|