Wed, 31 Dec 2014 07:16:47 +0100
Revert simplistic fix pending revisit of Mozilla integration attempt.
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 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
15 from automation import Automation
16 from mozprocess import ProcessHandlerMixin
19 class StdOutProc(ProcessHandlerMixin):
20 """Process handler for b2g which puts all output in a Queue.
21 """
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)
28 def handle_output(self, line):
29 self.queue.put_nowait(line)
32 class B2GRemoteAutomation(Automation):
33 _devicemanager = None
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
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)
54 def setEmulator(self, is_emulator):
55 self._is_emulator = is_emulator
57 def setDeviceManager(self, deviceManager):
58 self._devicemanager = deviceManager
60 def setAppName(self, appName):
61 self._appName = appName
63 def setRemoteProfile(self, remoteProfile):
64 self._remoteProfile = remoteProfile
66 def setProduct(self, product):
67 self._product = product
69 def setRemoteLog(self, logfile):
70 self._remoteLog = logfile
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)
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 = {}
84 if crashreporter:
85 env['MOZ_CRASHREPORTER'] = '1'
86 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
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
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
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
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
127 cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
129 return app, args
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
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'
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)
171 return (serial, status)
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()
183 def rebootDevice(self):
184 # find device's current status and serial number
185 serial, status = self.getDeviceStatus()
187 # reboot!
188 self._devicemanager._runCmd(['shell', '/system/bin/reboot'])
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)
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
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.
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")
227 # stop b2g
228 self._devicemanager._runCmd(['shell', 'stop', 'b2g'])
229 time.sleep(5)
231 # relaunch b2g inside b2g instance
232 instance = self.B2GInstance(self._devicemanager, env=env)
234 time.sleep(5)
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])
243 if self._is_emulator:
244 self.marionette.emulator.wait_for_port()
245 else:
246 time.sleep(5)
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)
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 """)
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)
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
281 return instance
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 """
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()
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()
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
319 @property
320 def pid(self):
321 # a dummy value to make the automation happy
322 return 0
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
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)
342 def wait(self, timeout=None):
343 # this should never happen
344 raise Exception("'wait' called on B2GInstance")
346 def kill(self):
347 # this should never happen
348 raise Exception("'kill' called on B2GInstance")