build/mobile/b2gautomation.py

Wed, 31 Dec 2014 07:16:47 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:16:47 +0100
branch
TOR_BUG_9701
changeset 3
141e0f1194b1
permissions
-rw-r--r--

Revert simplistic fix pending revisit of Mozilla integration attempt.

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

mercurial