1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/build/mobile/remoteautomation.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,291 @@ 1.4 +# This Source Code Form is subject to the terms of the Mozilla Public 1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.7 + 1.8 +import time 1.9 +import re 1.10 +import os 1.11 +import tempfile 1.12 +import shutil 1.13 +import subprocess 1.14 + 1.15 +from automation import Automation 1.16 +from devicemanager import DMError 1.17 +import mozcrash 1.18 + 1.19 +# signatures for logcat messages that we don't care about much 1.20 +fennecLogcatFilters = [ "The character encoding of the HTML document was not declared", 1.21 + "Use of Mutation Events is deprecated. Use MutationObserver instead.", 1.22 + "Unexpected value from nativeGetEnabledTags: 0" ] 1.23 + 1.24 +class RemoteAutomation(Automation): 1.25 + _devicemanager = None 1.26 + 1.27 + def __init__(self, deviceManager, appName = '', remoteLog = None): 1.28 + self._devicemanager = deviceManager 1.29 + self._appName = appName 1.30 + self._remoteProfile = None 1.31 + self._remoteLog = remoteLog 1.32 + 1.33 + # Default our product to fennec 1.34 + self._product = "fennec" 1.35 + self.lastTestSeen = "remoteautomation.py" 1.36 + Automation.__init__(self) 1.37 + 1.38 + def setDeviceManager(self, deviceManager): 1.39 + self._devicemanager = deviceManager 1.40 + 1.41 + def setAppName(self, appName): 1.42 + self._appName = appName 1.43 + 1.44 + def setRemoteProfile(self, remoteProfile): 1.45 + self._remoteProfile = remoteProfile 1.46 + 1.47 + def setProduct(self, product): 1.48 + self._product = product 1.49 + 1.50 + def setRemoteLog(self, logfile): 1.51 + self._remoteLog = logfile 1.52 + 1.53 + # Set up what we need for the remote environment 1.54 + def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None): 1.55 + # Because we are running remote, we don't want to mimic the local env 1.56 + # so no copying of os.environ 1.57 + if env is None: 1.58 + env = {} 1.59 + 1.60 + if dmdPath: 1.61 + env['DMD'] = '1' 1.62 + env['MOZ_REPLACE_MALLOC_LIB'] = os.path.join(dmdPath, 'libdmd.so') 1.63 + 1.64 + # Except for the mochitest results table hiding option, which isn't 1.65 + # passed to runtestsremote.py as an actual option, but through the 1.66 + # MOZ_HIDE_RESULTS_TABLE environment variable. 1.67 + if 'MOZ_HIDE_RESULTS_TABLE' in os.environ: 1.68 + env['MOZ_HIDE_RESULTS_TABLE'] = os.environ['MOZ_HIDE_RESULTS_TABLE'] 1.69 + 1.70 + if crashreporter and not debugger: 1.71 + env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' 1.72 + env['MOZ_CRASHREPORTER'] = '1' 1.73 + else: 1.74 + env['MOZ_CRASHREPORTER_DISABLE'] = '1' 1.75 + 1.76 + # Crash on non-local network connections. 1.77 + env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1' 1.78 + 1.79 + return env 1.80 + 1.81 + def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath): 1.82 + """ Wait for tests to finish. 1.83 + If maxTime seconds elapse or no output is detected for timeout 1.84 + seconds, kill the process and fail the test. 1.85 + """ 1.86 + # maxTime is used to override the default timeout, we should honor that 1.87 + status = proc.wait(timeout = maxTime, noOutputTimeout = timeout) 1.88 + self.lastTestSeen = proc.getLastTestSeen 1.89 + 1.90 + topActivity = self._devicemanager.getTopActivity() 1.91 + if topActivity == proc.procName: 1.92 + proc.kill() 1.93 + if status == 1: 1.94 + if maxTime: 1.95 + print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \ 1.96 + "allowed maximum time of %s seconds" % (self.lastTestSeen, maxTime) 1.97 + else: 1.98 + print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \ 1.99 + "allowed maximum time" % (self.lastTestSeen) 1.100 + if status == 2: 1.101 + print "TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output" \ 1.102 + % (self.lastTestSeen, int(timeout)) 1.103 + 1.104 + return status 1.105 + 1.106 + def deleteANRs(self): 1.107 + # empty ANR traces.txt file; usually need root permissions 1.108 + # we make it empty and writable so we can test the ANR reporter later 1.109 + traces = "/data/anr/traces.txt" 1.110 + try: 1.111 + self._devicemanager.shellCheckOutput(['echo', '', '>', traces], root=True) 1.112 + self._devicemanager.shellCheckOutput(['chmod', '666', traces], root=True) 1.113 + except DMError: 1.114 + print "Error deleting %s" % traces 1.115 + pass 1.116 + 1.117 + def checkForANRs(self): 1.118 + traces = "/data/anr/traces.txt" 1.119 + if self._devicemanager.fileExists(traces): 1.120 + try: 1.121 + t = self._devicemanager.pullFile(traces) 1.122 + print "Contents of %s:" % traces 1.123 + print t 1.124 + # Once reported, delete traces 1.125 + self.deleteANRs() 1.126 + except DMError: 1.127 + print "Error pulling %s" % traces 1.128 + pass 1.129 + else: 1.130 + print "%s not found" % traces 1.131 + 1.132 + def checkForCrashes(self, directory, symbolsPath): 1.133 + self.checkForANRs() 1.134 + 1.135 + logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters) 1.136 + javaException = mozcrash.check_for_java_exception(logcat) 1.137 + if javaException: 1.138 + return True 1.139 + 1.140 + # If crash reporting is disabled (MOZ_CRASHREPORTER!=1), we can't say 1.141 + # anything. 1.142 + if not self.CRASHREPORTER: 1.143 + return False 1.144 + 1.145 + try: 1.146 + dumpDir = tempfile.mkdtemp() 1.147 + remoteCrashDir = self._remoteProfile + '/minidumps/' 1.148 + if not self._devicemanager.dirExists(remoteCrashDir): 1.149 + # If crash reporting is enabled (MOZ_CRASHREPORTER=1), the 1.150 + # minidumps directory is automatically created when Fennec 1.151 + # (first) starts, so its lack of presence is a hint that 1.152 + # something went wrong. 1.153 + print "Automation Error: No crash directory (%s) found on remote device" % remoteCrashDir 1.154 + # Whilst no crash was found, the run should still display as a failure 1.155 + return True 1.156 + self._devicemanager.getDirectory(remoteCrashDir, dumpDir) 1.157 + crashed = Automation.checkForCrashes(self, dumpDir, symbolsPath) 1.158 + 1.159 + finally: 1.160 + try: 1.161 + shutil.rmtree(dumpDir) 1.162 + except: 1.163 + print "WARNING: unable to remove directory: %s" % dumpDir 1.164 + return crashed 1.165 + 1.166 + def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): 1.167 + # If remote profile is specified, use that instead 1.168 + if (self._remoteProfile): 1.169 + profileDir = self._remoteProfile 1.170 + 1.171 + # Hack for robocop, if app & testURL == None and extraArgs contains the rest of the stuff, lets 1.172 + # assume extraArgs is all we need 1.173 + if app == "am" and extraArgs[0] == "instrument": 1.174 + return app, extraArgs 1.175 + 1.176 + cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs) 1.177 + # Remove -foreground if it exists, if it doesn't this just returns 1.178 + try: 1.179 + args.remove('-foreground') 1.180 + except: 1.181 + pass 1.182 +#TODO: figure out which platform require NO_EM_RESTART 1.183 +# return app, ['--environ:NO_EM_RESTART=1'] + args 1.184 + return app, args 1.185 + 1.186 + def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = None): 1.187 + if stdout == None or stdout == -1 or stdout == subprocess.PIPE: 1.188 + stdout = self._remoteLog 1.189 + 1.190 + return self.RProcess(self._devicemanager, cmd, stdout, stderr, env, cwd, self._appName) 1.191 + 1.192 + # be careful here as this inner class doesn't have access to outer class members 1.193 + class RProcess(object): 1.194 + # device manager process 1.195 + dm = None 1.196 + def __init__(self, dm, cmd, stdout = None, stderr = None, env = None, cwd = None, app = None): 1.197 + self.dm = dm 1.198 + self.stdoutlen = 0 1.199 + self.lastTestSeen = "remoteautomation.py" 1.200 + self.proc = dm.launchProcess(cmd, stdout, cwd, env, True) 1.201 + if (self.proc is None): 1.202 + if cmd[0] == 'am': 1.203 + self.proc = stdout 1.204 + else: 1.205 + raise Exception("unable to launch process") 1.206 + self.procName = cmd[0].split('/')[-1] 1.207 + if cmd[0] == 'am' and cmd[1] == "instrument": 1.208 + self.procName = app 1.209 + print "Robocop process name: "+self.procName 1.210 + 1.211 + # Setting timeout at 1 hour since on a remote device this takes much longer 1.212 + self.timeout = 3600 1.213 + # The benefit of the following sleep is unclear; it was formerly 15 seconds 1.214 + time.sleep(1) 1.215 + 1.216 + @property 1.217 + def pid(self): 1.218 + pid = self.dm.processExist(self.procName) 1.219 + # HACK: we should probably be more sophisticated about monitoring 1.220 + # running processes for the remote case, but for now we'll assume 1.221 + # that this method can be called when nothing exists and it is not 1.222 + # an error 1.223 + if pid is None: 1.224 + return 0 1.225 + return pid 1.226 + 1.227 + @property 1.228 + def stdout(self): 1.229 + """ Fetch the full remote log file using devicemanager and return just 1.230 + the new log entries since the last call (as a multi-line string). 1.231 + """ 1.232 + if self.dm.fileExists(self.proc): 1.233 + try: 1.234 + newLogContent = self.dm.pullFile(self.proc, self.stdoutlen) 1.235 + except DMError: 1.236 + # we currently don't retry properly in the pullFile 1.237 + # function in dmSUT, so an error here is not necessarily 1.238 + # the end of the world 1.239 + return '' 1.240 + self.stdoutlen += len(newLogContent) 1.241 + # Match the test filepath from the last TEST-START line found in the new 1.242 + # log content. These lines are in the form: 1.243 + # 1234 INFO TEST-START | /filepath/we/wish/to/capture.html\n 1.244 + testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", newLogContent) 1.245 + if testStartFilenames: 1.246 + self.lastTestSeen = testStartFilenames[-1] 1.247 + return newLogContent.strip('\n').strip() 1.248 + else: 1.249 + return '' 1.250 + 1.251 + @property 1.252 + def getLastTestSeen(self): 1.253 + return self.lastTestSeen 1.254 + 1.255 + # Wait for the remote process to end (or for its activity to go to background). 1.256 + # While waiting, periodically retrieve the process output and print it. 1.257 + # If the process is still running after *timeout* seconds, return 1; 1.258 + # If the process is still running but no output is received in *noOutputTimeout* 1.259 + # seconds, return 2; 1.260 + # Else, once the process exits/goes to background, return 0. 1.261 + def wait(self, timeout = None, noOutputTimeout = None): 1.262 + timer = 0 1.263 + noOutputTimer = 0 1.264 + interval = 20 1.265 + 1.266 + if timeout == None: 1.267 + timeout = self.timeout 1.268 + 1.269 + status = 0 1.270 + while (self.dm.getTopActivity() == self.procName): 1.271 + # retrieve log updates every 60 seconds 1.272 + if timer % 60 == 0: 1.273 + t = self.stdout 1.274 + if t != '': 1.275 + print t 1.276 + noOutputTimer = 0 1.277 + 1.278 + time.sleep(interval) 1.279 + timer += interval 1.280 + noOutputTimer += interval 1.281 + if (timer > timeout): 1.282 + status = 1 1.283 + break 1.284 + if (noOutputTimeout and noOutputTimer > noOutputTimeout): 1.285 + status = 2 1.286 + break 1.287 + 1.288 + # Flush anything added to stdout during the sleep 1.289 + print self.stdout 1.290 + 1.291 + return status 1.292 + 1.293 + def kill(self): 1.294 + self.dm.killProcess(self.procName)