build/mobile/remoteautomation.py

branch
TOR_BUG_9701
changeset 3
141e0f1194b1
equal deleted inserted replaced
-1:000000000000 0:dfe602c6cbb2
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/.
4
5 import time
6 import re
7 import os
8 import tempfile
9 import shutil
10 import subprocess
11
12 from automation import Automation
13 from devicemanager import DMError
14 import mozcrash
15
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" ]
20
21 class RemoteAutomation(Automation):
22 _devicemanager = None
23
24 def __init__(self, deviceManager, appName = '', remoteLog = None):
25 self._devicemanager = deviceManager
26 self._appName = appName
27 self._remoteProfile = None
28 self._remoteLog = remoteLog
29
30 # Default our product to fennec
31 self._product = "fennec"
32 self.lastTestSeen = "remoteautomation.py"
33 Automation.__init__(self)
34
35 def setDeviceManager(self, deviceManager):
36 self._devicemanager = deviceManager
37
38 def setAppName(self, appName):
39 self._appName = appName
40
41 def setRemoteProfile(self, remoteProfile):
42 self._remoteProfile = remoteProfile
43
44 def setProduct(self, product):
45 self._product = product
46
47 def setRemoteLog(self, logfile):
48 self._remoteLog = logfile
49
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 = {}
56
57 if dmdPath:
58 env['DMD'] = '1'
59 env['MOZ_REPLACE_MALLOC_LIB'] = os.path.join(dmdPath, 'libdmd.so')
60
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']
66
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'
72
73 # Crash on non-local network connections.
74 env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
75
76 return env
77
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
86
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))
100
101 return status
102
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
113
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
128
129 def checkForCrashes(self, directory, symbolsPath):
130 self.checkForANRs()
131
132 logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
133 javaException = mozcrash.check_for_java_exception(logcat)
134 if javaException:
135 return True
136
137 # If crash reporting is disabled (MOZ_CRASHREPORTER!=1), we can't say
138 # anything.
139 if not self.CRASHREPORTER:
140 return False
141
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)
155
156 finally:
157 try:
158 shutil.rmtree(dumpDir)
159 except:
160 print "WARNING: unable to remove directory: %s" % dumpDir
161 return crashed
162
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
167
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
172
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
182
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
186
187 return self.RProcess(self._devicemanager, cmd, stdout, stderr, env, cwd, self._appName)
188
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
207
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)
212
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
223
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 ''
247
248 @property
249 def getLastTestSeen(self):
250 return self.lastTestSeen
251
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
262
263 if timeout == None:
264 timeout = self.timeout
265
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
274
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
284
285 # Flush anything added to stdout during the sleep
286 print self.stdout
287
288 return status
289
290 def kill(self):
291 self.dm.killProcess(self.procName)

mercurial