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