build/mobile/remoteautomation.py

Wed, 31 Dec 2014 07:16:47 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:16:47 +0100
branch
TOR_BUG_9701
changeset 3
141e0f1194b1
permissions
-rw-r--r--

Revert simplistic fix pending revisit of Mozilla integration attempt.

     1 # This Source Code Form is subject to the terms of the Mozilla Public
     2 # License, v. 2.0. If a copy of the MPL was not distributed with this
     3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     5 import time
     6 import re
     7 import os
     8 import tempfile
     9 import shutil
    10 import subprocess
    12 from automation import Automation
    13 from devicemanager import DMError
    14 import mozcrash
    16 # signatures for logcat messages that we don't care about much
    17 fennecLogcatFilters = [ "The character encoding of the HTML document was not declared",
    18                         "Use of Mutation Events is deprecated. Use MutationObserver instead.",
    19                         "Unexpected value from nativeGetEnabledTags: 0" ]
    21 class RemoteAutomation(Automation):
    22     _devicemanager = None
    24     def __init__(self, deviceManager, appName = '', remoteLog = None):
    25         self._devicemanager = deviceManager
    26         self._appName = appName
    27         self._remoteProfile = None
    28         self._remoteLog = remoteLog
    30         # Default our product to fennec
    31         self._product = "fennec"
    32         self.lastTestSeen = "remoteautomation.py"
    33         Automation.__init__(self)
    35     def setDeviceManager(self, deviceManager):
    36         self._devicemanager = deviceManager
    38     def setAppName(self, appName):
    39         self._appName = appName
    41     def setRemoteProfile(self, remoteProfile):
    42         self._remoteProfile = remoteProfile
    44     def setProduct(self, product):
    45         self._product = product
    47     def setRemoteLog(self, logfile):
    48         self._remoteLog = logfile
    50     # Set up what we need for the remote environment
    51     def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None):
    52         # Because we are running remote, we don't want to mimic the local env
    53         # so no copying of os.environ
    54         if env is None:
    55             env = {}
    57         if dmdPath:
    58             env['DMD'] = '1'
    59             env['MOZ_REPLACE_MALLOC_LIB'] = os.path.join(dmdPath, 'libdmd.so')
    61         # Except for the mochitest results table hiding option, which isn't
    62         # passed to runtestsremote.py as an actual option, but through the
    63         # MOZ_HIDE_RESULTS_TABLE environment variable.
    64         if 'MOZ_HIDE_RESULTS_TABLE' in os.environ:
    65             env['MOZ_HIDE_RESULTS_TABLE'] = os.environ['MOZ_HIDE_RESULTS_TABLE']
    67         if crashreporter and not debugger:
    68             env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
    69             env['MOZ_CRASHREPORTER'] = '1'
    70         else:
    71             env['MOZ_CRASHREPORTER_DISABLE'] = '1'
    73         # Crash on non-local network connections.
    74         env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
    76         return env
    78     def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
    79         """ Wait for tests to finish.
    80             If maxTime seconds elapse or no output is detected for timeout
    81             seconds, kill the process and fail the test.
    82         """
    83         # maxTime is used to override the default timeout, we should honor that
    84         status = proc.wait(timeout = maxTime, noOutputTimeout = timeout)
    85         self.lastTestSeen = proc.getLastTestSeen
    87         topActivity = self._devicemanager.getTopActivity()
    88         if topActivity == proc.procName:
    89             proc.kill()
    90         if status == 1:
    91             if maxTime:
    92                 print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
    93                       "allowed maximum time of %s seconds" % (self.lastTestSeen, maxTime)
    94             else:
    95                 print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
    96                       "allowed maximum time" % (self.lastTestSeen)
    97         if status == 2:
    98             print "TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output" \
    99                 % (self.lastTestSeen, int(timeout))
   101         return status
   103     def deleteANRs(self):
   104         # empty ANR traces.txt file; usually need root permissions
   105         # we make it empty and writable so we can test the ANR reporter later
   106         traces = "/data/anr/traces.txt"
   107         try:
   108             self._devicemanager.shellCheckOutput(['echo', '', '>', traces], root=True)
   109             self._devicemanager.shellCheckOutput(['chmod', '666', traces], root=True)
   110         except DMError:
   111             print "Error deleting %s" % traces
   112             pass
   114     def checkForANRs(self):
   115         traces = "/data/anr/traces.txt"
   116         if self._devicemanager.fileExists(traces):
   117             try:
   118                 t = self._devicemanager.pullFile(traces)
   119                 print "Contents of %s:" % traces
   120                 print t
   121                 # Once reported, delete traces
   122                 self.deleteANRs()
   123             except DMError:
   124                 print "Error pulling %s" % traces
   125                 pass
   126         else:
   127             print "%s not found" % traces
   129     def checkForCrashes(self, directory, symbolsPath):
   130         self.checkForANRs()
   132         logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
   133         javaException = mozcrash.check_for_java_exception(logcat)
   134         if javaException:
   135             return True
   137         # If crash reporting is disabled (MOZ_CRASHREPORTER!=1), we can't say
   138         # anything.
   139         if not self.CRASHREPORTER:
   140             return False
   142         try:
   143             dumpDir = tempfile.mkdtemp()
   144             remoteCrashDir = self._remoteProfile + '/minidumps/'
   145             if not self._devicemanager.dirExists(remoteCrashDir):
   146                 # If crash reporting is enabled (MOZ_CRASHREPORTER=1), the
   147                 # minidumps directory is automatically created when Fennec
   148                 # (first) starts, so its lack of presence is a hint that
   149                 # something went wrong.
   150                 print "Automation Error: No crash directory (%s) found on remote device" % remoteCrashDir
   151                 # Whilst no crash was found, the run should still display as a failure
   152                 return True
   153             self._devicemanager.getDirectory(remoteCrashDir, dumpDir)
   154             crashed = Automation.checkForCrashes(self, dumpDir, symbolsPath)
   156         finally:
   157             try:
   158                 shutil.rmtree(dumpDir)
   159             except:
   160                 print "WARNING: unable to remove directory: %s" % dumpDir
   161         return crashed
   163     def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
   164         # If remote profile is specified, use that instead
   165         if (self._remoteProfile):
   166             profileDir = self._remoteProfile
   168         # Hack for robocop, if app & testURL == None and extraArgs contains the rest of the stuff, lets
   169         # assume extraArgs is all we need
   170         if app == "am" and extraArgs[0] == "instrument":
   171             return app, extraArgs
   173         cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
   174         # Remove -foreground if it exists, if it doesn't this just returns
   175         try:
   176             args.remove('-foreground')
   177         except:
   178             pass
   179 #TODO: figure out which platform require NO_EM_RESTART
   180 #        return app, ['--environ:NO_EM_RESTART=1'] + args
   181         return app, args
   183     def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = None):
   184         if stdout == None or stdout == -1 or stdout == subprocess.PIPE:
   185             stdout = self._remoteLog
   187         return self.RProcess(self._devicemanager, cmd, stdout, stderr, env, cwd, self._appName)
   189     # be careful here as this inner class doesn't have access to outer class members
   190     class RProcess(object):
   191         # device manager process
   192         dm = None
   193         def __init__(self, dm, cmd, stdout = None, stderr = None, env = None, cwd = None, app = None):
   194             self.dm = dm
   195             self.stdoutlen = 0
   196             self.lastTestSeen = "remoteautomation.py"
   197             self.proc = dm.launchProcess(cmd, stdout, cwd, env, True)
   198             if (self.proc is None):
   199                 if cmd[0] == 'am':
   200                     self.proc = stdout
   201                 else:
   202                     raise Exception("unable to launch process")
   203             self.procName = cmd[0].split('/')[-1]
   204             if cmd[0] == 'am' and cmd[1] == "instrument":
   205                 self.procName = app
   206                 print "Robocop process name: "+self.procName
   208             # Setting timeout at 1 hour since on a remote device this takes much longer
   209             self.timeout = 3600
   210             # The benefit of the following sleep is unclear; it was formerly 15 seconds
   211             time.sleep(1)
   213         @property
   214         def pid(self):
   215             pid = self.dm.processExist(self.procName)
   216             # HACK: we should probably be more sophisticated about monitoring
   217             # running processes for the remote case, but for now we'll assume
   218             # that this method can be called when nothing exists and it is not
   219             # an error
   220             if pid is None:
   221                 return 0
   222             return pid
   224         @property
   225         def stdout(self):
   226             """ Fetch the full remote log file using devicemanager and return just
   227                 the new log entries since the last call (as a multi-line string).
   228             """
   229             if self.dm.fileExists(self.proc):
   230                 try:
   231                     newLogContent = self.dm.pullFile(self.proc, self.stdoutlen)
   232                 except DMError:
   233                     # we currently don't retry properly in the pullFile
   234                     # function in dmSUT, so an error here is not necessarily
   235                     # the end of the world
   236                     return ''
   237                 self.stdoutlen += len(newLogContent)
   238                 # Match the test filepath from the last TEST-START line found in the new
   239                 # log content. These lines are in the form:
   240                 # 1234 INFO TEST-START | /filepath/we/wish/to/capture.html\n
   241                 testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", newLogContent)
   242                 if testStartFilenames:
   243                     self.lastTestSeen = testStartFilenames[-1]
   244                 return newLogContent.strip('\n').strip()
   245             else:
   246                 return ''
   248         @property
   249         def getLastTestSeen(self):
   250             return self.lastTestSeen
   252         # Wait for the remote process to end (or for its activity to go to background).
   253         # While waiting, periodically retrieve the process output and print it.
   254         # If the process is still running after *timeout* seconds, return 1;
   255         # If the process is still running but no output is received in *noOutputTimeout*
   256         # seconds, return 2;
   257         # Else, once the process exits/goes to background, return 0.
   258         def wait(self, timeout = None, noOutputTimeout = None):
   259             timer = 0
   260             noOutputTimer = 0
   261             interval = 20 
   263             if timeout == None:
   264                 timeout = self.timeout
   266             status = 0
   267             while (self.dm.getTopActivity() == self.procName):
   268                 # retrieve log updates every 60 seconds
   269                 if timer % 60 == 0: 
   270                     t = self.stdout
   271                     if t != '':
   272                         print t
   273                         noOutputTimer = 0
   275                 time.sleep(interval)
   276                 timer += interval
   277                 noOutputTimer += interval
   278                 if (timer > timeout):
   279                     status = 1
   280                     break
   281                 if (noOutputTimeout and noOutputTimer > noOutputTimeout):
   282                     status = 2
   283                     break
   285             # Flush anything added to stdout during the sleep
   286             print self.stdout
   288             return status
   290         def kill(self):
   291             self.dm.killProcess(self.procName)

mercurial