build/mobile/b2gautomation.py

changeset 0
6474c204b198
     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 +

mercurial