1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/build/mobile/b2gautomation.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,349 @@ 1.4 +# This Source Code Form is subject to the terms of the Mozilla Public 1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 +# You can obtain one at http://mozilla.org/MPL/2.0/. 1.7 + 1.8 +import mozcrash 1.9 +import threading 1.10 +import os 1.11 +import Queue 1.12 +import re 1.13 +import shutil 1.14 +import tempfile 1.15 +import time 1.16 +import traceback 1.17 + 1.18 +from automation import Automation 1.19 +from mozprocess import ProcessHandlerMixin 1.20 + 1.21 + 1.22 +class StdOutProc(ProcessHandlerMixin): 1.23 + """Process handler for b2g which puts all output in a Queue. 1.24 + """ 1.25 + 1.26 + def __init__(self, cmd, queue, **kwargs): 1.27 + self.queue = queue 1.28 + kwargs.setdefault('processOutputLine', []).append(self.handle_output) 1.29 + ProcessHandlerMixin.__init__(self, cmd, **kwargs) 1.30 + 1.31 + def handle_output(self, line): 1.32 + self.queue.put_nowait(line) 1.33 + 1.34 + 1.35 +class B2GRemoteAutomation(Automation): 1.36 + _devicemanager = None 1.37 + 1.38 + def __init__(self, deviceManager, appName='', remoteLog=None, 1.39 + marionette=None, context_chrome=True): 1.40 + self._devicemanager = deviceManager 1.41 + self._appName = appName 1.42 + self._remoteProfile = None 1.43 + self._remoteLog = remoteLog 1.44 + self.marionette = marionette 1.45 + self.context_chrome = context_chrome 1.46 + self._is_emulator = False 1.47 + self.test_script = None 1.48 + self.test_script_args = None 1.49 + 1.50 + # Default our product to b2g 1.51 + self._product = "b2g" 1.52 + self.lastTestSeen = "b2gautomation.py" 1.53 + # Default log finish to mochitest standard 1.54 + self.logFinish = 'INFO SimpleTest FINISHED' 1.55 + Automation.__init__(self) 1.56 + 1.57 + def setEmulator(self, is_emulator): 1.58 + self._is_emulator = is_emulator 1.59 + 1.60 + def setDeviceManager(self, deviceManager): 1.61 + self._devicemanager = deviceManager 1.62 + 1.63 + def setAppName(self, appName): 1.64 + self._appName = appName 1.65 + 1.66 + def setRemoteProfile(self, remoteProfile): 1.67 + self._remoteProfile = remoteProfile 1.68 + 1.69 + def setProduct(self, product): 1.70 + self._product = product 1.71 + 1.72 + def setRemoteLog(self, logfile): 1.73 + self._remoteLog = logfile 1.74 + 1.75 + def installExtension(self, extensionSource, profileDir, extensionID=None): 1.76 + # Bug 827504 - installing special-powers extension separately causes problems in B2G 1.77 + if extensionID != "special-powers@mozilla.org": 1.78 + Automation.installExtension(self, extensionSource, profileDir, extensionID) 1.79 + 1.80 + # Set up what we need for the remote environment 1.81 + def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False): 1.82 + # Because we are running remote, we don't want to mimic the local env 1.83 + # so no copying of os.environ 1.84 + if env is None: 1.85 + env = {} 1.86 + 1.87 + if crashreporter: 1.88 + env['MOZ_CRASHREPORTER'] = '1' 1.89 + env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' 1.90 + 1.91 + # We always hide the results table in B2G; it's much slower if we don't. 1.92 + env['MOZ_HIDE_RESULTS_TABLE'] = '1' 1.93 + return env 1.94 + 1.95 + def waitForNet(self): 1.96 + active = False 1.97 + time_out = 0 1.98 + while not active and time_out < 40: 1.99 + data = self._devicemanager._runCmd(['shell', '/system/bin/netcfg']).stdout.readlines() 1.100 + data.pop(0) 1.101 + for line in data: 1.102 + if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)): 1.103 + active = True 1.104 + break 1.105 + time_out += 1 1.106 + time.sleep(1) 1.107 + return active 1.108 + 1.109 + def checkForCrashes(self, directory, symbolsPath): 1.110 + crashed = False 1.111 + remote_dump_dir = self._remoteProfile + '/minidumps' 1.112 + print "checking for crashes in '%s'" % remote_dump_dir 1.113 + if self._devicemanager.dirExists(remote_dump_dir): 1.114 + local_dump_dir = tempfile.mkdtemp() 1.115 + self._devicemanager.getDirectory(remote_dump_dir, local_dump_dir) 1.116 + try: 1.117 + crashed = mozcrash.check_for_crashes(local_dump_dir, symbolsPath, test_name=self.lastTestSeen) 1.118 + except: 1.119 + traceback.print_exc() 1.120 + finally: 1.121 + shutil.rmtree(local_dump_dir) 1.122 + self._devicemanager.removeDir(remote_dump_dir) 1.123 + return crashed 1.124 + 1.125 + def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): 1.126 + # if remote profile is specified, use that instead 1.127 + if (self._remoteProfile): 1.128 + profileDir = self._remoteProfile 1.129 + 1.130 + cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs) 1.131 + 1.132 + return app, args 1.133 + 1.134 + def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, 1.135 + debuggerInfo, symbolsPath): 1.136 + """ Wait for tests to finish (as evidenced by a signature string 1.137 + in logcat), or for a given amount of time to elapse with no 1.138 + output. 1.139 + """ 1.140 + timeout = timeout or 120 1.141 + while True: 1.142 + currentlog = proc.getStdoutLines(timeout) 1.143 + if currentlog: 1.144 + print currentlog 1.145 + # Match the test filepath from the last TEST-START line found in the new 1.146 + # log content. These lines are in the form: 1.147 + # ... INFO TEST-START | /filepath/we/wish/to/capture.html\n 1.148 + testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", currentlog) 1.149 + if testStartFilenames: 1.150 + self.lastTestSeen = testStartFilenames[-1] 1.151 + if hasattr(self, 'logFinish') and self.logFinish in currentlog: 1.152 + return 0 1.153 + else: 1.154 + self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed " 1.155 + "out after %d seconds with no output", 1.156 + self.lastTestSeen, int(timeout)) 1.157 + return 1 1.158 + 1.159 + def getDeviceStatus(self, serial=None): 1.160 + # Get the current status of the device. If we know the device 1.161 + # serial number, we look for that, otherwise we use the (presumably 1.162 + # only) device shown in 'adb devices'. 1.163 + serial = serial or self._devicemanager._deviceSerial 1.164 + status = 'unknown' 1.165 + 1.166 + for line in self._devicemanager._runCmd(['devices']).stdout.readlines(): 1.167 + result = re.match('(.*?)\t(.*)', line) 1.168 + if result: 1.169 + thisSerial = result.group(1) 1.170 + if not serial or thisSerial == serial: 1.171 + serial = thisSerial 1.172 + status = result.group(2) 1.173 + 1.174 + return (serial, status) 1.175 + 1.176 + def restartB2G(self): 1.177 + # TODO hangs in subprocess.Popen without this delay 1.178 + time.sleep(5) 1.179 + self._devicemanager._checkCmd(['shell', 'stop', 'b2g']) 1.180 + # Wait for a bit to make sure B2G has completely shut down. 1.181 + time.sleep(10) 1.182 + self._devicemanager._checkCmd(['shell', 'start', 'b2g']) 1.183 + if self._is_emulator: 1.184 + self.marionette.emulator.wait_for_port() 1.185 + 1.186 + def rebootDevice(self): 1.187 + # find device's current status and serial number 1.188 + serial, status = self.getDeviceStatus() 1.189 + 1.190 + # reboot! 1.191 + self._devicemanager._runCmd(['shell', '/system/bin/reboot']) 1.192 + 1.193 + # The above command can return while adb still thinks the device is 1.194 + # connected, so wait a little bit for it to disconnect from adb. 1.195 + time.sleep(10) 1.196 + 1.197 + # wait for device to come back to previous status 1.198 + print 'waiting for device to come back online after reboot' 1.199 + start = time.time() 1.200 + rserial, rstatus = self.getDeviceStatus(serial) 1.201 + while rstatus != 'device': 1.202 + if time.time() - start > 120: 1.203 + # device hasn't come back online in 2 minutes, something's wrong 1.204 + raise Exception("Device %s (status: %s) not back online after reboot" % (serial, rstatus)) 1.205 + time.sleep(5) 1.206 + rserial, rstatus = self.getDeviceStatus(serial) 1.207 + print 'device:', serial, 'status:', rstatus 1.208 + 1.209 + def Process(self, cmd, stdout=None, stderr=None, env=None, cwd=None): 1.210 + # On a desktop or fennec run, the Process method invokes a gecko 1.211 + # process in which to the tests. For B2G, we simply 1.212 + # reboot the device (which was configured with a test profile 1.213 + # already), wait for B2G to start up, and then navigate to the 1.214 + # test url using Marionette. There doesn't seem to be any way 1.215 + # to pass env variables into the B2G process, but this doesn't 1.216 + # seem to matter. 1.217 + 1.218 + # reboot device so it starts up with the mochitest profile 1.219 + # XXX: We could potentially use 'stop b2g' + 'start b2g' to achieve 1.220 + # a similar effect; will see which is more stable while attempting 1.221 + # to bring up the continuous integration. 1.222 + if not self._is_emulator: 1.223 + self.rebootDevice() 1.224 + time.sleep(5) 1.225 + #wait for wlan to come up 1.226 + if not self.waitForNet(): 1.227 + raise Exception("network did not come up, please configure the network" + 1.228 + " prior to running before running the automation framework") 1.229 + 1.230 + # stop b2g 1.231 + self._devicemanager._runCmd(['shell', 'stop', 'b2g']) 1.232 + time.sleep(5) 1.233 + 1.234 + # relaunch b2g inside b2g instance 1.235 + instance = self.B2GInstance(self._devicemanager, env=env) 1.236 + 1.237 + time.sleep(5) 1.238 + 1.239 + # Set up port forwarding again for Marionette, since any that 1.240 + # existed previously got wiped out by the reboot. 1.241 + if not self._is_emulator: 1.242 + self._devicemanager._checkCmd(['forward', 1.243 + 'tcp:%s' % self.marionette.port, 1.244 + 'tcp:%s' % self.marionette.port]) 1.245 + 1.246 + if self._is_emulator: 1.247 + self.marionette.emulator.wait_for_port() 1.248 + else: 1.249 + time.sleep(5) 1.250 + 1.251 + # start a marionette session 1.252 + session = self.marionette.start_session() 1.253 + if 'b2g' not in session: 1.254 + raise Exception("bad session value %s returned by start_session" % session) 1.255 + 1.256 + if self._is_emulator: 1.257 + # Disable offline status management (bug 777145), otherwise the network 1.258 + # will be 'offline' when the mochitests start. Presumably, the network 1.259 + # won't be offline on a real device, so we only do this for emulators. 1.260 + self.marionette.set_context(self.marionette.CONTEXT_CHROME) 1.261 + self.marionette.execute_script(""" 1.262 + Components.utils.import("resource://gre/modules/Services.jsm"); 1.263 + Services.io.manageOfflineStatus = false; 1.264 + Services.io.offline = false; 1.265 + """) 1.266 + 1.267 + if self.context_chrome: 1.268 + self.marionette.set_context(self.marionette.CONTEXT_CHROME) 1.269 + else: 1.270 + self.marionette.set_context(self.marionette.CONTEXT_CONTENT) 1.271 + 1.272 + # run the script that starts the tests 1.273 + if self.test_script: 1.274 + if os.path.isfile(self.test_script): 1.275 + script = open(self.test_script, 'r') 1.276 + self.marionette.execute_script(script.read(), script_args=self.test_script_args) 1.277 + script.close() 1.278 + elif isinstance(self.test_script, basestring): 1.279 + self.marionette.execute_script(self.test_script, script_args=self.test_script_args) 1.280 + else: 1.281 + # assumes the tests are started on startup automatically 1.282 + pass 1.283 + 1.284 + return instance 1.285 + 1.286 + # be careful here as this inner class doesn't have access to outer class members 1.287 + class B2GInstance(object): 1.288 + """Represents a B2G instance running on a device, and exposes 1.289 + some process-like methods/properties that are expected by the 1.290 + automation. 1.291 + """ 1.292 + 1.293 + def __init__(self, dm, env=None): 1.294 + self.dm = dm 1.295 + self.env = env or {} 1.296 + self.stdout_proc = None 1.297 + self.queue = Queue.Queue() 1.298 + 1.299 + # Launch b2g in a separate thread, and dump all output lines 1.300 + # into a queue. The lines in this queue are 1.301 + # retrieved and returned by accessing the stdout property of 1.302 + # this class. 1.303 + cmd = [self.dm._adbPath] 1.304 + if self.dm._deviceSerial: 1.305 + cmd.extend(['-s', self.dm._deviceSerial]) 1.306 + cmd.append('shell') 1.307 + for k, v in self.env.iteritems(): 1.308 + cmd.append("%s=%s" % (k, v)) 1.309 + cmd.append('/system/bin/b2g.sh') 1.310 + proc = threading.Thread(target=self._save_stdout_proc, args=(cmd, self.queue)) 1.311 + proc.daemon = True 1.312 + proc.start() 1.313 + 1.314 + def _save_stdout_proc(self, cmd, queue): 1.315 + self.stdout_proc = StdOutProc(cmd, queue) 1.316 + self.stdout_proc.run() 1.317 + if hasattr(self.stdout_proc, 'processOutput'): 1.318 + self.stdout_proc.processOutput() 1.319 + self.stdout_proc.wait() 1.320 + self.stdout_proc = None 1.321 + 1.322 + @property 1.323 + def pid(self): 1.324 + # a dummy value to make the automation happy 1.325 + return 0 1.326 + 1.327 + def getStdoutLines(self, timeout): 1.328 + # Return any lines in the queue used by the 1.329 + # b2g process handler. 1.330 + lines = [] 1.331 + # get all of the lines that are currently available 1.332 + while True: 1.333 + try: 1.334 + lines.append(self.queue.get_nowait()) 1.335 + except Queue.Empty: 1.336 + break 1.337 + 1.338 + # wait 'timeout' for any additional lines 1.339 + try: 1.340 + lines.append(self.queue.get(True, timeout)) 1.341 + except Queue.Empty: 1.342 + pass 1.343 + return '\n'.join(lines) 1.344 + 1.345 + def wait(self, timeout=None): 1.346 + # this should never happen 1.347 + raise Exception("'wait' called on B2GInstance") 1.348 + 1.349 + def kill(self): 1.350 + # this should never happen 1.351 + raise Exception("'kill' called on B2GInstance") 1.352 +