|
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 file, |
|
3 # You can obtain one at http://mozilla.org/MPL/2.0/. |
|
4 |
|
5 import mozcrash |
|
6 import threading |
|
7 import os |
|
8 import Queue |
|
9 import re |
|
10 import shutil |
|
11 import tempfile |
|
12 import time |
|
13 import traceback |
|
14 |
|
15 from automation import Automation |
|
16 from mozprocess import ProcessHandlerMixin |
|
17 |
|
18 |
|
19 class StdOutProc(ProcessHandlerMixin): |
|
20 """Process handler for b2g which puts all output in a Queue. |
|
21 """ |
|
22 |
|
23 def __init__(self, cmd, queue, **kwargs): |
|
24 self.queue = queue |
|
25 kwargs.setdefault('processOutputLine', []).append(self.handle_output) |
|
26 ProcessHandlerMixin.__init__(self, cmd, **kwargs) |
|
27 |
|
28 def handle_output(self, line): |
|
29 self.queue.put_nowait(line) |
|
30 |
|
31 |
|
32 class B2GRemoteAutomation(Automation): |
|
33 _devicemanager = None |
|
34 |
|
35 def __init__(self, deviceManager, appName='', remoteLog=None, |
|
36 marionette=None, context_chrome=True): |
|
37 self._devicemanager = deviceManager |
|
38 self._appName = appName |
|
39 self._remoteProfile = None |
|
40 self._remoteLog = remoteLog |
|
41 self.marionette = marionette |
|
42 self.context_chrome = context_chrome |
|
43 self._is_emulator = False |
|
44 self.test_script = None |
|
45 self.test_script_args = None |
|
46 |
|
47 # Default our product to b2g |
|
48 self._product = "b2g" |
|
49 self.lastTestSeen = "b2gautomation.py" |
|
50 # Default log finish to mochitest standard |
|
51 self.logFinish = 'INFO SimpleTest FINISHED' |
|
52 Automation.__init__(self) |
|
53 |
|
54 def setEmulator(self, is_emulator): |
|
55 self._is_emulator = is_emulator |
|
56 |
|
57 def setDeviceManager(self, deviceManager): |
|
58 self._devicemanager = deviceManager |
|
59 |
|
60 def setAppName(self, appName): |
|
61 self._appName = appName |
|
62 |
|
63 def setRemoteProfile(self, remoteProfile): |
|
64 self._remoteProfile = remoteProfile |
|
65 |
|
66 def setProduct(self, product): |
|
67 self._product = product |
|
68 |
|
69 def setRemoteLog(self, logfile): |
|
70 self._remoteLog = logfile |
|
71 |
|
72 def installExtension(self, extensionSource, profileDir, extensionID=None): |
|
73 # Bug 827504 - installing special-powers extension separately causes problems in B2G |
|
74 if extensionID != "special-powers@mozilla.org": |
|
75 Automation.installExtension(self, extensionSource, profileDir, extensionID) |
|
76 |
|
77 # Set up what we need for the remote environment |
|
78 def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False): |
|
79 # Because we are running remote, we don't want to mimic the local env |
|
80 # so no copying of os.environ |
|
81 if env is None: |
|
82 env = {} |
|
83 |
|
84 if crashreporter: |
|
85 env['MOZ_CRASHREPORTER'] = '1' |
|
86 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' |
|
87 |
|
88 # We always hide the results table in B2G; it's much slower if we don't. |
|
89 env['MOZ_HIDE_RESULTS_TABLE'] = '1' |
|
90 return env |
|
91 |
|
92 def waitForNet(self): |
|
93 active = False |
|
94 time_out = 0 |
|
95 while not active and time_out < 40: |
|
96 data = self._devicemanager._runCmd(['shell', '/system/bin/netcfg']).stdout.readlines() |
|
97 data.pop(0) |
|
98 for line in data: |
|
99 if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)): |
|
100 active = True |
|
101 break |
|
102 time_out += 1 |
|
103 time.sleep(1) |
|
104 return active |
|
105 |
|
106 def checkForCrashes(self, directory, symbolsPath): |
|
107 crashed = False |
|
108 remote_dump_dir = self._remoteProfile + '/minidumps' |
|
109 print "checking for crashes in '%s'" % remote_dump_dir |
|
110 if self._devicemanager.dirExists(remote_dump_dir): |
|
111 local_dump_dir = tempfile.mkdtemp() |
|
112 self._devicemanager.getDirectory(remote_dump_dir, local_dump_dir) |
|
113 try: |
|
114 crashed = mozcrash.check_for_crashes(local_dump_dir, symbolsPath, test_name=self.lastTestSeen) |
|
115 except: |
|
116 traceback.print_exc() |
|
117 finally: |
|
118 shutil.rmtree(local_dump_dir) |
|
119 self._devicemanager.removeDir(remote_dump_dir) |
|
120 return crashed |
|
121 |
|
122 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): |
|
123 # if remote profile is specified, use that instead |
|
124 if (self._remoteProfile): |
|
125 profileDir = self._remoteProfile |
|
126 |
|
127 cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs) |
|
128 |
|
129 return app, args |
|
130 |
|
131 def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, |
|
132 debuggerInfo, symbolsPath): |
|
133 """ Wait for tests to finish (as evidenced by a signature string |
|
134 in logcat), or for a given amount of time to elapse with no |
|
135 output. |
|
136 """ |
|
137 timeout = timeout or 120 |
|
138 while True: |
|
139 currentlog = proc.getStdoutLines(timeout) |
|
140 if currentlog: |
|
141 print currentlog |
|
142 # Match the test filepath from the last TEST-START line found in the new |
|
143 # log content. These lines are in the form: |
|
144 # ... INFO TEST-START | /filepath/we/wish/to/capture.html\n |
|
145 testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", currentlog) |
|
146 if testStartFilenames: |
|
147 self.lastTestSeen = testStartFilenames[-1] |
|
148 if hasattr(self, 'logFinish') and self.logFinish in currentlog: |
|
149 return 0 |
|
150 else: |
|
151 self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed " |
|
152 "out after %d seconds with no output", |
|
153 self.lastTestSeen, int(timeout)) |
|
154 return 1 |
|
155 |
|
156 def getDeviceStatus(self, serial=None): |
|
157 # Get the current status of the device. If we know the device |
|
158 # serial number, we look for that, otherwise we use the (presumably |
|
159 # only) device shown in 'adb devices'. |
|
160 serial = serial or self._devicemanager._deviceSerial |
|
161 status = 'unknown' |
|
162 |
|
163 for line in self._devicemanager._runCmd(['devices']).stdout.readlines(): |
|
164 result = re.match('(.*?)\t(.*)', line) |
|
165 if result: |
|
166 thisSerial = result.group(1) |
|
167 if not serial or thisSerial == serial: |
|
168 serial = thisSerial |
|
169 status = result.group(2) |
|
170 |
|
171 return (serial, status) |
|
172 |
|
173 def restartB2G(self): |
|
174 # TODO hangs in subprocess.Popen without this delay |
|
175 time.sleep(5) |
|
176 self._devicemanager._checkCmd(['shell', 'stop', 'b2g']) |
|
177 # Wait for a bit to make sure B2G has completely shut down. |
|
178 time.sleep(10) |
|
179 self._devicemanager._checkCmd(['shell', 'start', 'b2g']) |
|
180 if self._is_emulator: |
|
181 self.marionette.emulator.wait_for_port() |
|
182 |
|
183 def rebootDevice(self): |
|
184 # find device's current status and serial number |
|
185 serial, status = self.getDeviceStatus() |
|
186 |
|
187 # reboot! |
|
188 self._devicemanager._runCmd(['shell', '/system/bin/reboot']) |
|
189 |
|
190 # The above command can return while adb still thinks the device is |
|
191 # connected, so wait a little bit for it to disconnect from adb. |
|
192 time.sleep(10) |
|
193 |
|
194 # wait for device to come back to previous status |
|
195 print 'waiting for device to come back online after reboot' |
|
196 start = time.time() |
|
197 rserial, rstatus = self.getDeviceStatus(serial) |
|
198 while rstatus != 'device': |
|
199 if time.time() - start > 120: |
|
200 # device hasn't come back online in 2 minutes, something's wrong |
|
201 raise Exception("Device %s (status: %s) not back online after reboot" % (serial, rstatus)) |
|
202 time.sleep(5) |
|
203 rserial, rstatus = self.getDeviceStatus(serial) |
|
204 print 'device:', serial, 'status:', rstatus |
|
205 |
|
206 def Process(self, cmd, stdout=None, stderr=None, env=None, cwd=None): |
|
207 # On a desktop or fennec run, the Process method invokes a gecko |
|
208 # process in which to the tests. For B2G, we simply |
|
209 # reboot the device (which was configured with a test profile |
|
210 # already), wait for B2G to start up, and then navigate to the |
|
211 # test url using Marionette. There doesn't seem to be any way |
|
212 # to pass env variables into the B2G process, but this doesn't |
|
213 # seem to matter. |
|
214 |
|
215 # reboot device so it starts up with the mochitest profile |
|
216 # XXX: We could potentially use 'stop b2g' + 'start b2g' to achieve |
|
217 # a similar effect; will see which is more stable while attempting |
|
218 # to bring up the continuous integration. |
|
219 if not self._is_emulator: |
|
220 self.rebootDevice() |
|
221 time.sleep(5) |
|
222 #wait for wlan to come up |
|
223 if not self.waitForNet(): |
|
224 raise Exception("network did not come up, please configure the network" + |
|
225 " prior to running before running the automation framework") |
|
226 |
|
227 # stop b2g |
|
228 self._devicemanager._runCmd(['shell', 'stop', 'b2g']) |
|
229 time.sleep(5) |
|
230 |
|
231 # relaunch b2g inside b2g instance |
|
232 instance = self.B2GInstance(self._devicemanager, env=env) |
|
233 |
|
234 time.sleep(5) |
|
235 |
|
236 # Set up port forwarding again for Marionette, since any that |
|
237 # existed previously got wiped out by the reboot. |
|
238 if not self._is_emulator: |
|
239 self._devicemanager._checkCmd(['forward', |
|
240 'tcp:%s' % self.marionette.port, |
|
241 'tcp:%s' % self.marionette.port]) |
|
242 |
|
243 if self._is_emulator: |
|
244 self.marionette.emulator.wait_for_port() |
|
245 else: |
|
246 time.sleep(5) |
|
247 |
|
248 # start a marionette session |
|
249 session = self.marionette.start_session() |
|
250 if 'b2g' not in session: |
|
251 raise Exception("bad session value %s returned by start_session" % session) |
|
252 |
|
253 if self._is_emulator: |
|
254 # Disable offline status management (bug 777145), otherwise the network |
|
255 # will be 'offline' when the mochitests start. Presumably, the network |
|
256 # won't be offline on a real device, so we only do this for emulators. |
|
257 self.marionette.set_context(self.marionette.CONTEXT_CHROME) |
|
258 self.marionette.execute_script(""" |
|
259 Components.utils.import("resource://gre/modules/Services.jsm"); |
|
260 Services.io.manageOfflineStatus = false; |
|
261 Services.io.offline = false; |
|
262 """) |
|
263 |
|
264 if self.context_chrome: |
|
265 self.marionette.set_context(self.marionette.CONTEXT_CHROME) |
|
266 else: |
|
267 self.marionette.set_context(self.marionette.CONTEXT_CONTENT) |
|
268 |
|
269 # run the script that starts the tests |
|
270 if self.test_script: |
|
271 if os.path.isfile(self.test_script): |
|
272 script = open(self.test_script, 'r') |
|
273 self.marionette.execute_script(script.read(), script_args=self.test_script_args) |
|
274 script.close() |
|
275 elif isinstance(self.test_script, basestring): |
|
276 self.marionette.execute_script(self.test_script, script_args=self.test_script_args) |
|
277 else: |
|
278 # assumes the tests are started on startup automatically |
|
279 pass |
|
280 |
|
281 return instance |
|
282 |
|
283 # be careful here as this inner class doesn't have access to outer class members |
|
284 class B2GInstance(object): |
|
285 """Represents a B2G instance running on a device, and exposes |
|
286 some process-like methods/properties that are expected by the |
|
287 automation. |
|
288 """ |
|
289 |
|
290 def __init__(self, dm, env=None): |
|
291 self.dm = dm |
|
292 self.env = env or {} |
|
293 self.stdout_proc = None |
|
294 self.queue = Queue.Queue() |
|
295 |
|
296 # Launch b2g in a separate thread, and dump all output lines |
|
297 # into a queue. The lines in this queue are |
|
298 # retrieved and returned by accessing the stdout property of |
|
299 # this class. |
|
300 cmd = [self.dm._adbPath] |
|
301 if self.dm._deviceSerial: |
|
302 cmd.extend(['-s', self.dm._deviceSerial]) |
|
303 cmd.append('shell') |
|
304 for k, v in self.env.iteritems(): |
|
305 cmd.append("%s=%s" % (k, v)) |
|
306 cmd.append('/system/bin/b2g.sh') |
|
307 proc = threading.Thread(target=self._save_stdout_proc, args=(cmd, self.queue)) |
|
308 proc.daemon = True |
|
309 proc.start() |
|
310 |
|
311 def _save_stdout_proc(self, cmd, queue): |
|
312 self.stdout_proc = StdOutProc(cmd, queue) |
|
313 self.stdout_proc.run() |
|
314 if hasattr(self.stdout_proc, 'processOutput'): |
|
315 self.stdout_proc.processOutput() |
|
316 self.stdout_proc.wait() |
|
317 self.stdout_proc = None |
|
318 |
|
319 @property |
|
320 def pid(self): |
|
321 # a dummy value to make the automation happy |
|
322 return 0 |
|
323 |
|
324 def getStdoutLines(self, timeout): |
|
325 # Return any lines in the queue used by the |
|
326 # b2g process handler. |
|
327 lines = [] |
|
328 # get all of the lines that are currently available |
|
329 while True: |
|
330 try: |
|
331 lines.append(self.queue.get_nowait()) |
|
332 except Queue.Empty: |
|
333 break |
|
334 |
|
335 # wait 'timeout' for any additional lines |
|
336 try: |
|
337 lines.append(self.queue.get(True, timeout)) |
|
338 except Queue.Empty: |
|
339 pass |
|
340 return '\n'.join(lines) |
|
341 |
|
342 def wait(self, timeout=None): |
|
343 # this should never happen |
|
344 raise Exception("'wait' called on B2GInstance") |
|
345 |
|
346 def kill(self): |
|
347 # this should never happen |
|
348 raise Exception("'kill' called on B2GInstance") |
|
349 |