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/.
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.
26 def __init__(self
, cmd
, queue
, **kwargs
):
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
):
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
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
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'
98 while not active
and time_out
< 40:
99 data
= self
._devicemanager
._runCmd
(['shell', '/system/bin/netcfg']).stdout
.readlines()
102 if (re
.search(r
'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line
)):
109 def checkForCrashes(self
, directory
, symbolsPath
):
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
)
117 crashed
= mozcrash
.check_for_crashes(local_dump_dir
, symbolsPath
, test_name
=self
.lastTestSeen
)
119 traceback
.print_exc()
121 shutil
.rmtree(local_dump_dir
)
122 self
._devicemanager
.removeDir(remote_dump_dir
)
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
)
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
140 timeout
= timeout
or 120
142 currentlog
= proc
.getStdoutLines(timeout
)
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
:
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'):
166 print "timed out after %d seconds waiting for b2g process to exit" % timeout
169 self
.checkForCrashes(None, symbolsPath
)
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
179 for line
in self
._devicemanager
._runCmd
(['devices']).stdout
.readlines():
180 result
= re
.match('(.*?)\t(.*)', line
)
182 thisSerial
= result
.group(1)
183 if not serial
or thisSerial
== serial
:
185 status
= result
.group(2)
187 return (serial
, status
)
189 def restartB2G(self
):
190 # TODO hangs in subprocess.Popen without this delay
192 self
._devicemanager
._checkCmd
(['shell', 'stop', 'b2g'])
193 # Wait for a bit to make sure B2G has completely shut down.
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()
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.
210 # wait for device to come back to previous status
211 print 'waiting for device to come back online after reboot'
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
))
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
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
:
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")
244 self
._devicemanager
._runCmd
(['shell', 'stop', 'b2g'])
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
)
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
)
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
)
277 self
.marionette
.set_context(self
.marionette
.CONTEXT_CONTENT
)
279 # run the script that starts the tests
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
)
285 elif isinstance(self
.test_script
, basestring
):
286 self
.marionette
.execute_script(self
.test_script
, script_args
=self
.test_script_args
)
288 # assumes the tests are started on startup automatically
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
300 def __init__(self
, dm
, env
=None):
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
310 cmd
= [self
.dm
._adbPath
]
311 if self
.dm
._deviceSerial
:
312 cmd
.extend(['-s', self
.dm
._deviceSerial
])
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
))
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
331 # a dummy value to make the automation happy
334 def getStdoutLines(self
, timeout
):
335 # Return any lines in the queue used by the
336 # b2g process handler.
338 # get all of the lines that are currently available
341 lines
.append(self
.queue
.get_nowait())
345 # wait 'timeout' for any additional lines
347 lines
.append(self
.queue
.get(True, timeout
))
350 return '\n'.join(lines
)
352 def wait(self
, timeout
=None):
353 # this should never happen
354 raise Exception("'wait' called on B2GInstance")
357 # this should never happen
358 raise Exception("'kill' called on B2GInstance")