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.

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

mercurial