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/.
14 from automation
import Automation
15 from devicemanager
import DMError
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
):
26 def __init__(self
, deviceManager
, appName
= '', remoteLog
= 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
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'
75 env
['MOZ_CRASHREPORTER_DISABLE'] = '1'
77 # Crash on non-local network connections.
78 env
['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
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.
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
:
96 print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
97 "allowed maximum time of %s seconds" % (self
.lastTestSeen
, maxTime
)
99 print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
100 "allowed maximum time" % (self
.lastTestSeen
)
102 print "TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output" \
103 % (self
.lastTestSeen
, int(timeout
))
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"
112 self
._devicemanager
.shellCheckOutput(['echo', '', '>', traces
], root
=True)
113 self
._devicemanager
.shellCheckOutput(['chmod', '666', traces
], root
=True)
115 print "Error deleting %s" % traces
118 def checkForANRs(self
):
119 traces
= "/data/anr/traces.txt"
120 if self
._devicemanager
.fileExists(traces
):
122 t
= self
._devicemanager
.pullFile(traces
)
123 print "Contents of %s:" % traces
125 # Once reported, delete traces
128 print "Error pulling %s" % traces
131 print "%s not found" % traces
133 def deleteTombstones(self
):
134 # delete any existing tombstone files from device
135 remoteDir
= "/data/tombstones"
137 self
._devicemanager
.shellCheckOutput(['rm', '-r', remoteDir
], root
=True)
139 # This may just indicate that the tombstone directory is missing
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)
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
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)
156 # This may just indicate that no tombstone files are present
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
)
171 print "%s does not exist; tombstone check skipped" % remoteDir
173 print "MOZ_UPLOAD_DIR not defined; tombstone check skipped"
175 def checkForCrashes(self
, directory
, symbolsPath
):
177 self
.checkForTombstones()
179 logcat
= self
._devicemanager
.getLogcat(filterOutRegexps
=fennecLogcatFilters
)
180 javaException
= mozcrash
.check_for_java_exception(logcat
)
184 # If crash reporting is disabled (MOZ_CRASHREPORTER!=1), we can't say
186 if not self
.CRASHREPORTER
:
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
200 self
._devicemanager
.getDirectory(remoteCrashDir
, dumpDir
)
201 crashed
= Automation
.checkForCrashes(self
, dumpDir
, symbolsPath
)
205 shutil
.rmtree(dumpDir
)
207 print "WARNING: unable to remove directory: %s" % dumpDir
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
223 args
.remove('-foreground')
226 #TODO: figure out which platform require NO_EM_RESTART
227 # return app, ['--environ:NO_EM_RESTART=1'] + 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
,
237 # be careful here as this inner class doesn't have access to outer class members
238 class RProcess(object):
239 # device manager process
241 def __init__(self
, dm
, cmd
, stdout
=None, stderr
=None, env
=None, cwd
=None, app
=None,
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):
253 raise Exception("unable to launch process")
254 self
.procName
= cmd
[0].split('/')[-1]
255 if cmd
[0] == 'am' and cmd
[1] == "instrument":
257 print "Robocop process name: "+self
.procName
259 # Setting timeout at 1 hour since on a remote device this takes much longer
261 # The benefit of the following sleep is unclear; it was formerly 15 seconds
264 # Used to buffer log messages until we meet a line break
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
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
):
285 newLogContent
= self
.dm
.pullFile(self
.proc
, self
.stdoutlen
)
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
291 if not newLogContent
:
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]
301 return [newLogContent
]
303 self
.logBuffer
+= newLogContent
304 lines
= self
.logBuffer
.split('\n')
308 # We only keep the last (unfinished) line in the buffer
309 self
.logBuffer
= lines
[-1]
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
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*
331 # Else, once the process exits/goes to background, return 0.
332 def wait(self
, timeout
= None, noOutputTimeout
= None):
338 timeout
= self
.timeout
341 while (self
.dm
.getTopActivity() == self
.procName
):
342 # retrieve log updates every 60 seconds
344 messages
= self
.read_stdout()
350 noOutputTimer
+= interval
351 if (timer
> timeout
):
354 if (noOutputTimeout
and noOutputTimer
> noOutputTimeout
):
358 # Flush anything added to stdout during the sleep
363 def kill(self
, stagedShutdown
= False):
365 # Trigger an ANR report with "kill -3" (SIGQUIT)
366 self
.dm
.killProcess(self
.procName
, 3)
368 # Trigger a breakpad dump with "kill -6" (SIGABRT)
369 self
.dm
.killProcess(self
.procName
, 6)
370 # Wait for process to end
373 pid
= self
.dm
.processExist(self
.procName
)
375 print "%s still alive after SIGABRT: waiting..." % self
.procName
380 self
.dm
.killProcess(self
.procName
, 9)
381 pid
= self
.dm
.processExist(self
.procName
)
383 self
.dm
.killProcess(self
.procName
)
385 self
.dm
.killProcess(self
.procName
)