summaryrefslogtreecommitdiffstats
path: root/build/mobile
diff options
context:
space:
mode:
Diffstat (limited to 'build/mobile')
-rw-r--r--build/mobile/b2gautomation.py455
-rw-r--r--build/mobile/remoteautomation.py432
2 files changed, 887 insertions, 0 deletions
diff --git a/build/mobile/b2gautomation.py b/build/mobile/b2gautomation.py
new file mode 100644
index 000000000..d49a5f1ac
--- /dev/null
+++ b/build/mobile/b2gautomation.py
@@ -0,0 +1,455 @@
+# 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 = {}
+
+ if crashreporter:
+ env['MOZ_CRASHREPORTER'] = '1'
+ env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
+
+ # 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.now()
+ while datetime.datetime.now() - 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")
+
diff --git a/build/mobile/remoteautomation.py b/build/mobile/remoteautomation.py
new file mode 100644
index 000000000..7b2fad6cb
--- /dev/null
+++ b/build/mobile/remoteautomation.py
@@ -0,0 +1,432 @@
+# 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 glob
+import time
+import re
+import os
+import posixpath
+import tempfile
+import shutil
+import subprocess
+import sys
+
+from automation import Automation
+from mozdevice import DMError, DeviceManager
+from mozlog import get_default_logger
+import mozcrash
+
+# signatures for logcat messages that we don't care about much
+fennecLogcatFilters = [ "The character encoding of the HTML document was not declared",
+ "Use of Mutation Events is deprecated. Use MutationObserver instead.",
+ "Unexpected value from nativeGetEnabledTags: 0" ]
+
+class RemoteAutomation(Automation):
+ _devicemanager = None
+
+ # Part of a hack for Robocop: "am COMMAND" is handled specially if COMMAND
+ # is in this set. See usages below.
+ _specialAmCommands = ('instrument', 'start')
+
+ def __init__(self, deviceManager, appName = '', remoteLog = None,
+ processArgs=None):
+ self._devicemanager = deviceManager
+ self._appName = appName
+ self._remoteProfile = None
+ self._remoteLog = remoteLog
+ self._processArgs = processArgs or {};
+
+ # Default our product to fennec
+ self._product = "fennec"
+ self.lastTestSeen = "remoteautomation.py"
+ Automation.__init__(self)
+
+ 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
+
+ # Set up what we need for the remote environment
+ def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None, lsanPath=None):
+ # 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 = {}
+
+ if dmdPath:
+ env['MOZ_REPLACE_MALLOC_LIB'] = os.path.join(dmdPath, 'libdmd.so')
+
+ # Except for the mochitest results table hiding option, which isn't
+ # passed to runtestsremote.py as an actual option, but through the
+ # MOZ_HIDE_RESULTS_TABLE environment variable.
+ if 'MOZ_HIDE_RESULTS_TABLE' in os.environ:
+ env['MOZ_HIDE_RESULTS_TABLE'] = os.environ['MOZ_HIDE_RESULTS_TABLE']
+
+ if crashreporter and not debugger:
+ env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
+ env['MOZ_CRASHREPORTER'] = '1'
+ else:
+ env['MOZ_CRASHREPORTER_DISABLE'] = '1'
+
+ # Crash on non-local network connections by default.
+ # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
+ # enable non-local connections for the purposes of local testing.
+ # Don't override the user's choice here. See bug 1049688.
+ env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1')
+
+ # Send an env var noting that we are in automation. Passing any
+ # value except the empty string will declare the value to exist.
+ #
+ # This may be used to disabled network connections during testing, e.g.
+ # Switchboard & telemetry uploads.
+ env.setdefault('MOZ_IN_AUTOMATION', '1')
+
+ # Set WebRTC logging in case it is not set yet.
+ # On Android, environment variables cannot contain ',' so the
+ # standard WebRTC setting for NSPR_LOG_MODULES is not available.
+ # env.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:5,datachannel:5,jsep:5,MediaPipelineFactory:5')
+ env.setdefault('R_LOG_LEVEL', '6')
+ env.setdefault('R_LOG_DESTINATION', 'stderr')
+ env.setdefault('R_LOG_VERBOSE', '1')
+
+ return env
+
+ def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, outputHandler=None):
+ """ Wait for tests to finish.
+ If maxTime seconds elapse or no output is detected for timeout
+ seconds, kill the process and fail the test.
+ """
+ # maxTime is used to override the default timeout, we should honor that
+ status = proc.wait(timeout = maxTime, noOutputTimeout = timeout)
+ self.lastTestSeen = proc.getLastTestSeen
+
+ topActivity = self._devicemanager.getTopActivity()
+ if topActivity == proc.procName:
+ proc.kill(True)
+ if status == 1:
+ if maxTime:
+ print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
+ "allowed maximum time of %s seconds" % (self.lastTestSeen, maxTime)
+ else:
+ print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
+ "allowed maximum time" % (self.lastTestSeen)
+ if status == 2:
+ print "TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output" \
+ % (self.lastTestSeen, int(timeout))
+
+ return status
+
+ def deleteANRs(self):
+ # empty ANR traces.txt file; usually need root permissions
+ # we make it empty and writable so we can test the ANR reporter later
+ traces = "/data/anr/traces.txt"
+ try:
+ self._devicemanager.shellCheckOutput(['echo', '', '>', traces], root=True,
+ timeout=DeviceManager.short_timeout)
+ self._devicemanager.shellCheckOutput(['chmod', '666', traces], root=True,
+ timeout=DeviceManager.short_timeout)
+ except DMError:
+ print "Error deleting %s" % traces
+ pass
+
+ def checkForANRs(self):
+ traces = "/data/anr/traces.txt"
+ if self._devicemanager.fileExists(traces):
+ try:
+ t = self._devicemanager.pullFile(traces)
+ if t:
+ stripped = t.strip()
+ if len(stripped) > 0:
+ print "Contents of %s:" % traces
+ print t
+ # Once reported, delete traces
+ self.deleteANRs()
+ except DMError:
+ print "Error pulling %s" % traces
+ except IOError:
+ print "Error pulling %s" % traces
+ else:
+ print "%s not found" % traces
+
+ def deleteTombstones(self):
+ # delete any existing tombstone files from device
+ remoteDir = "/data/tombstones"
+ try:
+ self._devicemanager.shellCheckOutput(['rm', '-r', remoteDir], root=True,
+ timeout=DeviceManager.short_timeout)
+ except DMError:
+ # This may just indicate that the tombstone directory is missing
+ pass
+
+ def checkForTombstones(self):
+ # pull any tombstones from device and move to MOZ_UPLOAD_DIR
+ remoteDir = "/data/tombstones"
+ blobberUploadDir = os.environ.get('MOZ_UPLOAD_DIR', None)
+ if blobberUploadDir:
+ if not os.path.exists(blobberUploadDir):
+ os.mkdir(blobberUploadDir)
+ if self._devicemanager.dirExists(remoteDir):
+ # copy tombstone files from device to local blobber upload directory
+ try:
+ self._devicemanager.shellCheckOutput(['chmod', '777', remoteDir], root=True,
+ timeout=DeviceManager.short_timeout)
+ self._devicemanager.shellCheckOutput(['chmod', '666', os.path.join(remoteDir, '*')], root=True,
+ timeout=DeviceManager.short_timeout)
+ self._devicemanager.getDirectory(remoteDir, blobberUploadDir, False)
+ except DMError:
+ # This may just indicate that no tombstone files are present
+ pass
+ self.deleteTombstones()
+ # add a .txt file extension to each tombstone file name, so
+ # that blobber will upload it
+ for f in glob.glob(os.path.join(blobberUploadDir, "tombstone_??")):
+ # add a unique integer to the file name, in case there are
+ # multiple tombstones generated with the same name, for
+ # instance, after multiple robocop tests
+ for i in xrange(1, sys.maxint):
+ newname = "%s.%d.txt" % (f, i)
+ if not os.path.exists(newname):
+ os.rename(f, newname)
+ break
+ else:
+ print "%s does not exist; tombstone check skipped" % remoteDir
+ else:
+ print "MOZ_UPLOAD_DIR not defined; tombstone check skipped"
+
+ def checkForCrashes(self, directory, symbolsPath):
+ self.checkForANRs()
+ self.checkForTombstones()
+
+ logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
+
+ javaException = mozcrash.check_for_java_exception(logcat, test_name=self.lastTestSeen)
+ if javaException:
+ return True
+
+ # If crash reporting is disabled (MOZ_CRASHREPORTER!=1), we can't say
+ # anything.
+ if not self.CRASHREPORTER:
+ return False
+
+ try:
+ dumpDir = tempfile.mkdtemp()
+ remoteCrashDir = posixpath.join(self._remoteProfile, 'minidumps')
+ if not self._devicemanager.dirExists(remoteCrashDir):
+ # If crash reporting is enabled (MOZ_CRASHREPORTER=1), the
+ # minidumps directory is automatically created when Fennec
+ # (first) starts, so its lack of presence is a hint that
+ # something went wrong.
+ print "Automation Error: No crash directory (%s) found on remote device" % remoteCrashDir
+ # Whilst no crash was found, the run should still display as a failure
+ return True
+ self._devicemanager.getDirectory(remoteCrashDir, dumpDir)
+
+ logger = get_default_logger()
+ if logger is not None:
+ crashed = mozcrash.log_crashes(logger, dumpDir, symbolsPath, test=self.lastTestSeen)
+ else:
+ crashed = Automation.checkForCrashes(self, dumpDir, symbolsPath)
+
+ finally:
+ try:
+ shutil.rmtree(dumpDir)
+ except:
+ print "WARNING: unable to remove directory: %s" % dumpDir
+ return crashed
+
+ def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
+ # If remote profile is specified, use that instead
+ if (self._remoteProfile):
+ profileDir = self._remoteProfile
+
+ # Hack for robocop, if app & testURL == None and extraArgs contains the rest of the stuff, lets
+ # assume extraArgs is all we need
+ if app == "am" and extraArgs[0] in RemoteAutomation._specialAmCommands:
+ return app, extraArgs
+
+ cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
+ # Remove -foreground if it exists, if it doesn't this just returns
+ try:
+ args.remove('-foreground')
+ except:
+ pass
+#TODO: figure out which platform require NO_EM_RESTART
+# return app, ['--environ:NO_EM_RESTART=1'] + args
+ return app, args
+
+ def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = None):
+ if stdout == None or stdout == -1 or stdout == subprocess.PIPE:
+ stdout = self._remoteLog
+
+ return self.RProcess(self._devicemanager, cmd, stdout, stderr, env, cwd, self._appName,
+ **self._processArgs)
+
+ # be careful here as this inner class doesn't have access to outer class members
+ class RProcess(object):
+ # device manager process
+ dm = None
+ def __init__(self, dm, cmd, stdout=None, stderr=None, env=None, cwd=None, app=None,
+ messageLogger=None):
+ self.dm = dm
+ self.stdoutlen = 0
+ self.lastTestSeen = "remoteautomation.py"
+ self.proc = dm.launchProcess(cmd, stdout, cwd, env, True)
+ self.messageLogger = messageLogger
+
+ if (self.proc is None):
+ if cmd[0] == 'am':
+ self.proc = stdout
+ else:
+ raise Exception("unable to launch process")
+ self.procName = cmd[0].split('/')[-1]
+ if cmd[0] == 'am' and cmd[1] in RemoteAutomation._specialAmCommands:
+ self.procName = app
+
+ # Setting timeout at 1 hour since on a remote device this takes much longer.
+ # Temporarily increased to 90 minutes because no more chunks can be created.
+ self.timeout = 5400
+ # The benefit of the following sleep is unclear; it was formerly 15 seconds
+ time.sleep(1)
+
+ # Used to buffer log messages until we meet a line break
+ self.logBuffer = ""
+
+ @property
+ def pid(self):
+ pid = self.dm.processExist(self.procName)
+ # HACK: we should probably be more sophisticated about monitoring
+ # running processes for the remote case, but for now we'll assume
+ # that this method can be called when nothing exists and it is not
+ # an error
+ if pid is None:
+ return 0
+ return pid
+
+ def read_stdout(self):
+ """
+ Fetch the full remote log file using devicemanager, process them and
+ return whether there were any new log entries since the last call.
+ """
+ if not self.dm.fileExists(self.proc):
+ return False
+ try:
+ newLogContent = self.dm.pullFile(self.proc, self.stdoutlen)
+ except DMError:
+ # we currently don't retry properly in the pullFile
+ # function in dmSUT, so an error here is not necessarily
+ # the end of the world
+ return False
+ if not newLogContent:
+ return False
+
+ self.stdoutlen += len(newLogContent)
+
+ if self.messageLogger is None:
+ testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", newLogContent)
+ if testStartFilenames:
+ self.lastTestSeen = testStartFilenames[-1]
+ print newLogContent
+ return True
+
+ self.logBuffer += newLogContent
+ lines = self.logBuffer.split('\n')
+ lines = [l for l in lines if l]
+
+ if lines:
+ if self.logBuffer.endswith('\n'):
+ # all lines are complete; no need to buffer
+ self.logBuffer = ""
+ else:
+ # keep the last (unfinished) line in the buffer
+ self.logBuffer = lines[-1]
+ del lines[-1]
+
+ if not lines:
+ return False
+
+ for line in lines:
+ # This passes the line to the logger (to be logged or buffered)
+ parsed_messages = self.messageLogger.write(line)
+ for message in parsed_messages:
+ if isinstance(message, dict) and message.get('action') == 'test_start':
+ self.lastTestSeen = message['test']
+ return True
+
+ @property
+ def getLastTestSeen(self):
+ return self.lastTestSeen
+
+ # Wait for the remote process to end (or for its activity to go to background).
+ # While waiting, periodically retrieve the process output and print it.
+ # If the process is still running after *timeout* seconds, return 1;
+ # If the process is still running but no output is received in *noOutputTimeout*
+ # seconds, return 2;
+ # Else, once the process exits/goes to background, return 0.
+ def wait(self, timeout = None, noOutputTimeout = None):
+ timer = 0
+ noOutputTimer = 0
+ interval = 10
+ if timeout == None:
+ timeout = self.timeout
+ status = 0
+ top = self.procName
+ slowLog = False
+ while (top == self.procName):
+ # Get log updates on each interval, but if it is taking
+ # too long, only do it every 60 seconds
+ if (not slowLog) or (timer % 60 == 0):
+ startRead = datetime.datetime.now()
+ hasOutput = self.read_stdout()
+ if (datetime.datetime.now() - startRead) > datetime.timedelta(seconds=5):
+ slowLog = True
+ if hasOutput:
+ noOutputTimer = 0
+ time.sleep(interval)
+ timer += interval
+ noOutputTimer += interval
+ if (timer > timeout):
+ status = 1
+ break
+ if (noOutputTimeout and noOutputTimer > noOutputTimeout):
+ status = 2
+ break
+ top = self.dm.getTopActivity()
+ # Flush anything added to stdout during the sleep
+ self.read_stdout()
+ return status
+
+ def kill(self, stagedShutdown = False):
+ if stagedShutdown:
+ # Trigger an ANR report with "kill -3" (SIGQUIT)
+ self.dm.killProcess(self.procName, 3)
+ time.sleep(3)
+ # Trigger a breakpad dump with "kill -6" (SIGABRT)
+ self.dm.killProcess(self.procName, 6)
+ # Wait for process to end
+ retries = 0
+ while retries < 3:
+ pid = self.dm.processExist(self.procName)
+ if pid and pid > 0:
+ print "%s still alive after SIGABRT: waiting..." % self.procName
+ time.sleep(5)
+ else:
+ return
+ retries += 1
+ self.dm.killProcess(self.procName, 9)
+ pid = self.dm.processExist(self.procName)
+ if pid and pid > 0:
+ self.dm.killProcess(self.procName)
+ else:
+ self.dm.killProcess(self.procName)