|
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) |