Bumping manifests a=b2g-bump
[gecko.git] / build / mobile / remoteautomation.py
blob1bee7668e7f0931cc6667b2253f67c3dfdc03429
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 glob
6 import time
7 import re
8 import os
9 import tempfile
10 import shutil
11 import subprocess
12 import sys
14 from automation import Automation
15 from devicemanager import DMError
16 import mozcrash
18 # signatures for logcat messages that we don't care about much
19 fennecLogcatFilters = [ "The character encoding of the HTML document was not declared",
20 "Use of Mutation Events is deprecated. Use MutationObserver instead.",
21 "Unexpected value from nativeGetEnabledTags: 0" ]
23 class RemoteAutomation(Automation):
24 _devicemanager = None
26 def __init__(self, deviceManager, appName = '', remoteLog = None,
27 processArgs=None):
28 self._devicemanager = deviceManager
29 self._appName = appName
30 self._remoteProfile = None
31 self._remoteLog = remoteLog
32 self._processArgs = processArgs or {};
34 # Default our product to fennec
35 self._product = "fennec"
36 self.lastTestSeen = "remoteautomation.py"
37 Automation.__init__(self)
39 def setDeviceManager(self, deviceManager):
40 self._devicemanager = deviceManager
42 def setAppName(self, appName):
43 self._appName = appName
45 def setRemoteProfile(self, remoteProfile):
46 self._remoteProfile = remoteProfile
48 def setProduct(self, product):
49 self._product = product
51 def setRemoteLog(self, logfile):
52 self._remoteLog = logfile
54 # Set up what we need for the remote environment
55 def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None, lsanPath=None):
56 # Because we are running remote, we don't want to mimic the local env
57 # so no copying of os.environ
58 if env is None:
59 env = {}
61 if dmdPath:
62 env['DMD'] = '1'
63 env['MOZ_REPLACE_MALLOC_LIB'] = os.path.join(dmdPath, 'libdmd.so')
65 # Except for the mochitest results table hiding option, which isn't
66 # passed to runtestsremote.py as an actual option, but through the
67 # MOZ_HIDE_RESULTS_TABLE environment variable.
68 if 'MOZ_HIDE_RESULTS_TABLE' in os.environ:
69 env['MOZ_HIDE_RESULTS_TABLE'] = os.environ['MOZ_HIDE_RESULTS_TABLE']
71 if crashreporter and not debugger:
72 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
73 env['MOZ_CRASHREPORTER'] = '1'
74 else:
75 env['MOZ_CRASHREPORTER_DISABLE'] = '1'
77 # Crash on non-local network connections.
78 env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
80 return env
82 def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
83 """ Wait for tests to finish.
84 If maxTime seconds elapse or no output is detected for timeout
85 seconds, kill the process and fail the test.
86 """
87 # maxTime is used to override the default timeout, we should honor that
88 status = proc.wait(timeout = maxTime, noOutputTimeout = timeout)
89 self.lastTestSeen = proc.getLastTestSeen
91 topActivity = self._devicemanager.getTopActivity()
92 if topActivity == proc.procName:
93 proc.kill(True)
94 if status == 1:
95 if maxTime:
96 print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
97 "allowed maximum time of %s seconds" % (self.lastTestSeen, maxTime)
98 else:
99 print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
100 "allowed maximum time" % (self.lastTestSeen)
101 if status == 2:
102 print "TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output" \
103 % (self.lastTestSeen, int(timeout))
105 return status
107 def deleteANRs(self):
108 # empty ANR traces.txt file; usually need root permissions
109 # we make it empty and writable so we can test the ANR reporter later
110 traces = "/data/anr/traces.txt"
111 try:
112 self._devicemanager.shellCheckOutput(['echo', '', '>', traces], root=True)
113 self._devicemanager.shellCheckOutput(['chmod', '666', traces], root=True)
114 except DMError:
115 print "Error deleting %s" % traces
116 pass
118 def checkForANRs(self):
119 traces = "/data/anr/traces.txt"
120 if self._devicemanager.fileExists(traces):
121 try:
122 t = self._devicemanager.pullFile(traces)
123 print "Contents of %s:" % traces
124 print t
125 # Once reported, delete traces
126 self.deleteANRs()
127 except DMError:
128 print "Error pulling %s" % traces
129 pass
130 else:
131 print "%s not found" % traces
133 def deleteTombstones(self):
134 # delete any existing tombstone files from device
135 remoteDir = "/data/tombstones"
136 try:
137 self._devicemanager.shellCheckOutput(['rm', '-r', remoteDir], root=True)
138 except DMError:
139 # This may just indicate that the tombstone directory is missing
140 pass
142 def checkForTombstones(self):
143 # pull any tombstones from device and move to MOZ_UPLOAD_DIR
144 remoteDir = "/data/tombstones"
145 blobberUploadDir = os.environ.get('MOZ_UPLOAD_DIR', None)
146 if blobberUploadDir:
147 if not os.path.exists(blobberUploadDir):
148 os.mkdir(blobberUploadDir)
149 if self._devicemanager.dirExists(remoteDir):
150 # copy tombstone files from device to local blobber upload directory
151 try:
152 self._devicemanager.shellCheckOutput(['chmod', '777', remoteDir], root=True)
153 self._devicemanager.shellCheckOutput(['chmod', '666', os.path.join(remoteDir, '*')], root=True)
154 self._devicemanager.getDirectory(remoteDir, blobberUploadDir, False)
155 except DMError:
156 # This may just indicate that no tombstone files are present
157 pass
158 self.deleteTombstones()
159 # add a .txt file extension to each tombstone file name, so
160 # that blobber will upload it
161 for f in glob.glob(os.path.join(blobberUploadDir, "tombstone_??")):
162 # add a unique integer to the file name, in case there are
163 # multiple tombstones generated with the same name, for
164 # instance, after multiple robocop tests
165 for i in xrange(1, sys.maxint):
166 newname = "%s.%d.txt" % (f, i)
167 if not os.path.exists(newname):
168 os.rename(f, newname)
169 break
170 else:
171 print "%s does not exist; tombstone check skipped" % remoteDir
172 else:
173 print "MOZ_UPLOAD_DIR not defined; tombstone check skipped"
175 def checkForCrashes(self, directory, symbolsPath):
176 self.checkForANRs()
177 self.checkForTombstones()
179 logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
180 javaException = mozcrash.check_for_java_exception(logcat)
181 if javaException:
182 return True
184 # If crash reporting is disabled (MOZ_CRASHREPORTER!=1), we can't say
185 # anything.
186 if not self.CRASHREPORTER:
187 return False
189 try:
190 dumpDir = tempfile.mkdtemp()
191 remoteCrashDir = self._remoteProfile + '/minidumps/'
192 if not self._devicemanager.dirExists(remoteCrashDir):
193 # If crash reporting is enabled (MOZ_CRASHREPORTER=1), the
194 # minidumps directory is automatically created when Fennec
195 # (first) starts, so its lack of presence is a hint that
196 # something went wrong.
197 print "Automation Error: No crash directory (%s) found on remote device" % remoteCrashDir
198 # Whilst no crash was found, the run should still display as a failure
199 return True
200 self._devicemanager.getDirectory(remoteCrashDir, dumpDir)
201 crashed = Automation.checkForCrashes(self, dumpDir, symbolsPath)
203 finally:
204 try:
205 shutil.rmtree(dumpDir)
206 except:
207 print "WARNING: unable to remove directory: %s" % dumpDir
208 return crashed
210 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
211 # If remote profile is specified, use that instead
212 if (self._remoteProfile):
213 profileDir = self._remoteProfile
215 # Hack for robocop, if app & testURL == None and extraArgs contains the rest of the stuff, lets
216 # assume extraArgs is all we need
217 if app == "am" and extraArgs[0] == "instrument":
218 return app, extraArgs
220 cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
221 # Remove -foreground if it exists, if it doesn't this just returns
222 try:
223 args.remove('-foreground')
224 except:
225 pass
226 #TODO: figure out which platform require NO_EM_RESTART
227 # return app, ['--environ:NO_EM_RESTART=1'] + args
228 return app, args
230 def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = None):
231 if stdout == None or stdout == -1 or stdout == subprocess.PIPE:
232 stdout = self._remoteLog
234 return self.RProcess(self._devicemanager, cmd, stdout, stderr, env, cwd, self._appName,
235 **self._processArgs)
237 # be careful here as this inner class doesn't have access to outer class members
238 class RProcess(object):
239 # device manager process
240 dm = None
241 def __init__(self, dm, cmd, stdout=None, stderr=None, env=None, cwd=None, app=None,
242 messageLogger=None):
243 self.dm = dm
244 self.stdoutlen = 0
245 self.lastTestSeen = "remoteautomation.py"
246 self.proc = dm.launchProcess(cmd, stdout, cwd, env, True)
247 self.messageLogger = messageLogger
249 if (self.proc is None):
250 if cmd[0] == 'am':
251 self.proc = stdout
252 else:
253 raise Exception("unable to launch process")
254 self.procName = cmd[0].split('/')[-1]
255 if cmd[0] == 'am' and cmd[1] == "instrument":
256 self.procName = app
257 print "Robocop process name: "+self.procName
259 # Setting timeout at 1 hour since on a remote device this takes much longer
260 self.timeout = 3600
261 # The benefit of the following sleep is unclear; it was formerly 15 seconds
262 time.sleep(1)
264 # Used to buffer log messages until we meet a line break
265 self.logBuffer = ""
267 @property
268 def pid(self):
269 pid = self.dm.processExist(self.procName)
270 # HACK: we should probably be more sophisticated about monitoring
271 # running processes for the remote case, but for now we'll assume
272 # that this method can be called when nothing exists and it is not
273 # an error
274 if pid is None:
275 return 0
276 return pid
278 def read_stdout(self):
279 """ Fetch the full remote log file using devicemanager and return just
280 the new log entries since the last call (as a list of messages or lines).
282 if not self.dm.fileExists(self.proc):
283 return []
284 try:
285 newLogContent = self.dm.pullFile(self.proc, self.stdoutlen)
286 except DMError:
287 # we currently don't retry properly in the pullFile
288 # function in dmSUT, so an error here is not necessarily
289 # the end of the world
290 return []
291 if not newLogContent:
292 return []
294 self.stdoutlen += len(newLogContent)
296 if self.messageLogger is None:
297 testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", newLogContent)
298 if testStartFilenames:
299 self.lastTestSeen = testStartFilenames[-1]
300 print newLogContent
301 return [newLogContent]
303 self.logBuffer += newLogContent
304 lines = self.logBuffer.split('\n')
305 if not lines:
306 return
308 # We only keep the last (unfinished) line in the buffer
309 self.logBuffer = lines[-1]
310 del lines[-1]
311 messages = []
312 for line in lines:
313 # This passes the line to the logger (to be logged or buffered)
314 # and returns a list of structured messages (dict)
315 parsed_messages = self.messageLogger.write(line)
316 for message in parsed_messages:
317 if message['action'] == 'test_start':
318 self.lastTestSeen = message['test']
319 messages += parsed_messages
320 return messages
322 @property
323 def getLastTestSeen(self):
324 return self.lastTestSeen
326 # Wait for the remote process to end (or for its activity to go to background).
327 # While waiting, periodically retrieve the process output and print it.
328 # If the process is still running after *timeout* seconds, return 1;
329 # If the process is still running but no output is received in *noOutputTimeout*
330 # seconds, return 2;
331 # Else, once the process exits/goes to background, return 0.
332 def wait(self, timeout = None, noOutputTimeout = None):
333 timer = 0
334 noOutputTimer = 0
335 interval = 20
337 if timeout == None:
338 timeout = self.timeout
340 status = 0
341 while (self.dm.getTopActivity() == self.procName):
342 # retrieve log updates every 60 seconds
343 if timer % 60 == 0:
344 messages = self.read_stdout()
345 if messages:
346 noOutputTimer = 0
348 time.sleep(interval)
349 timer += interval
350 noOutputTimer += interval
351 if (timer > timeout):
352 status = 1
353 break
354 if (noOutputTimeout and noOutputTimer > noOutputTimeout):
355 status = 2
356 break
358 # Flush anything added to stdout during the sleep
359 self.read_stdout()
361 return status
363 def kill(self, stagedShutdown = False):
364 if stagedShutdown:
365 # Trigger an ANR report with "kill -3" (SIGQUIT)
366 self.dm.killProcess(self.procName, 3)
367 time.sleep(3)
368 # Trigger a breakpad dump with "kill -6" (SIGABRT)
369 self.dm.killProcess(self.procName, 6)
370 # Wait for process to end
371 retries = 0
372 while retries < 3:
373 pid = self.dm.processExist(self.procName)
374 if pid and pid > 0:
375 print "%s still alive after SIGABRT: waiting..." % self.procName
376 time.sleep(5)
377 else:
378 return
379 retries += 1
380 self.dm.killProcess(self.procName, 9)
381 pid = self.dm.processExist(self.procName)
382 if pid and pid > 0:
383 self.dm.killProcess(self.procName)
384 else:
385 self.dm.killProcess(self.procName)