author Geoff Brown <>
Tue, 26 Feb 2013 07:19:58 -0700
changeset 123017 bf6ef4772884397681a98b82a99004afa7982e14
parent 121774 e754df01a1903b76cce9f6795940d6a00b68d969
child 124655 1c19d2a03d900cef52914ff4c9d4306bd173c508
permissions -rw-r--r--
Bug 844797 - Avoid UnboundLocalError on dumpDir in checkForCrashes; r=edmorley

# 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

import time
import re
import os
import automationutils
import tempfile
import shutil
import subprocess

from automation import Automation
from devicemanager import NetworkTools, DMError

# 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." ]

class RemoteAutomation(Automation):
    _devicemanager = None

    def __init__(self, deviceManager, appName = '', remoteLog = None):
        self._devicemanager = deviceManager
        self._appName = appName
        self._remoteProfile = None
        self._remoteLog = remoteLog

        # Default our product to fennec
        self._product = "fennec"
        self.lastTestSeen = ""

    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):
        # 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 = {}

        # Except for the mochitest results table hiding option, which isn't
        # passed to as an actual option, but through the
        # MOZ_CRASHREPORTER_DISABLE environment variable.
        if 'MOZ_HIDE_RESULTS_TABLE' in os.environ:
            env['MOZ_HIDE_RESULTS_TABLE'] = os.environ['MOZ_HIDE_RESULTS_TABLE']

        if crashreporter:
            env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
            env['MOZ_CRASHREPORTER'] = '1'
            env['MOZ_CRASHREPORTER_DISABLE'] = '1'

        return env

    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
        """ Wait for tests to finish (as evidenced by the process exiting),
            or for maxTime elapse, in which case kill the process regardless.
        # maxTime is used to override the default timeout, we should honor that
        status = proc.wait(timeout = maxTime)
        self.lastTestSeen = proc.getLastTestSeen

        if (status == 1 and self._devicemanager.processExist(proc.procName)):
            # Then we timed out, make sure Fennec is dead
            if maxTime:
                print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
                      "allowed maximum time of %s seconds" % (self.lastTestSeen, maxTime)
                print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
                      "allowed maximum time" % (self.lastTestSeen)

        return status

    def checkForJavaException(self, logcat):
        found_exception = False
        for i, line in enumerate(logcat):
            if "REPORTING UNCAUGHT EXCEPTION" in line:
                # Strip away the date, time, logcat tag and pid from the next two lines and
                # concatenate the remainder to form a concise summary of the exception. 
                # For example:
                # 01-30 20:15:41.937 E/GeckoAppShell( 1703): >>> REPORTING UNCAUGHT EXCEPTION FROM THREAD 9 ("GeckoBackgroundThread")
                # 01-30 20:15:41.937 E/GeckoAppShell( 1703): java.lang.NullPointerException
                # 01-30 20:15:41.937 E/GeckoAppShell( 1703): 	at org.mozilla.gecko.GeckoApp$
                # 01-30 20:15:41.937 E/GeckoAppShell( 1703): 	at android.os.Handler.handleCallback(
                # 01-30 20:15:41.937 E/GeckoAppShell( 1703): 	at android.os.Handler.dispatchMessage(
                # 01-30 20:15:41.937 E/GeckoAppShell( 1703): 	at android.os.Looper.loop(
                # 01-30 20:15:41.937 E/GeckoAppShell( 1703): 	at
                #   -> java.lang.NullPointerException at org.mozilla.gecko.GeckoApp$
                found_exception = True
                logre = re.compile(r".*\):\s(.*)")
                m =[i+1])
                if m and
                    top_frame =
                m =[i+2])
                if m and
                    top_frame = top_frame +
                print "PROCESS-CRASH | java-exception | %s" % top_frame
        return found_exception

    def checkForCrashes(self, directory, symbolsPath):
        logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
        javaException = self.checkForJavaException(logcat)
        if javaException:
            return True
            dumpDir = tempfile.mkdtemp()
            remoteCrashDir = self._remoteProfile + '/minidumps/'
            if not self._devicemanager.dirExists(remoteCrashDir):
                # As of this writing, 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)
            crashed = automationutils.checkForCrashes(dumpDir, symbolsPath,
                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] == "instrument":
            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
#TODO: figure out which platform require NO_EM_RESTART
#        return app, ['--environ:NO_EM_RESTART=1'] + args
        return app, args

    def getLanIp(self):
        nettools = NetworkTools()
        return nettools.getLanIp()

    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)

    # 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):
   = dm
            self.stdoutlen = 0
            self.lastTestSeen = ""
            self.proc = dm.launchProcess(cmd, stdout, cwd, env, True)
            if (self.proc is None):
              if cmd[0] == 'am':
                self.proc = stdout
                raise Exception("unable to launch process")
            exepath = cmd[0]
            name = exepath.split('/')[-1]
            self.procName = name
            # Hack for Robocop: Derive the actual process name from the command line.
            # We expect something like:
            #  ['am', 'instrument', '-w', '-e', 'class', 'org.mozilla.fennec.tests.testBookmark', 'org.mozilla.roboexample.test/android.test.InstrumentationTestRunner']
            # and want to derive 'org.mozilla.fennec'.
            if cmd[0] == 'am' and cmd[1] == "instrument":
                i = cmd.index("class")
              except ValueError:
                # no "class" argument -- maybe this isn't robocop?
                i = -1
              if (i > 0):
                classname = cmd[i+1]
                parts = classname.split('.')
                  i = parts.index("tests")
                except ValueError:
                  # no "tests" component -- maybe this isn't robocop?
                  i = -1
                if (i > 0):
                  self.procName = '.'.join(parts[0:i])
                  print "Robocop derived process name: "+self.procName

            # Setting timeout at 1 hour since on a remote device this takes much longer
            self.timeout = 3600
            # The benefit of the following sleep is unclear; it was formerly 15 seconds

        def pid(self):
            pid =
            # 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 stdout(self):
            """ Fetch the full remote log file using devicemanager and return just
                the new log entries since the last call (as a multi-line string).
                    t =
                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 ''
                newLogContent = t[self.stdoutlen:]
                self.stdoutlen = len(t)
                # Match the test filepath from the last TEST-START line found in the new
                # log content. These lines are in the form:
                # 1234 INFO TEST-START | /filepath/we/wish/to/capture.html\n
                testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", newLogContent)
                if testStartFilenames:
                    self.lastTestSeen = testStartFilenames[-1]
                return newLogContent.strip('\n').strip()
                return ''

        def getLastTestSeen(self):
            return self.lastTestSeen

        def wait(self, timeout = None):
            timer = 0
            interval = 5

            if timeout == None:
                timeout = self.timeout

            while (
                t = self.stdout
                if t != '': print t
                timer += interval
                if (timer > timeout):

            # Flush anything added to stdout during the sleep
            print self.stdout

            if (timer >= timeout):
                return 1
            return 0

        def kill(self):