build/mobile/b2gautomation.py
author Andrew Halberstadt <ahalberstadt@mozilla.com>
Wed, 11 Jul 2012 18:14:35 -0700
changeset 99062 46804c31366b9fcf06fc707baa21be6eec5f5152
parent 96981 515c5d751c5e2d729fa62d48a018cf8935353c2b
child 101258 0f66a7ecf8de7a71dd9847e2664e93258a8a38db
permissions -rw-r--r--
Bug 772307 - Add dummy browser.manifestURL pref to user.js to prevent B2G from dying, r=jgriffin

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

import automationutils
import threading
import os
import Queue
import re
import socket
import shutil
import sys
import tempfile
import time

from automation import Automation
from devicemanager import DeviceManager, NetworkTools
from mozprocess import ProcessHandlerMixin


class LogcatProc(ProcessHandlerMixin):
    """Process handler for logcat which puts all output in a Queue.
    """

    def __init__(self, cmd, queue, **kwargs):
        self.queue = queue
        kwargs.setdefault('processOutputLine', []).append(self.handle_output)
        ProcessHandlerMixin.__init__(self, cmd, **kwargs)

    def handle_output(self, line):
        self.queue.put_nowait(line)


class B2GRemoteAutomation(Automation):
    _devicemanager = None

    def __init__(self, deviceManager, appName='', remoteLog=None,
                 marionette=None):
        self._devicemanager = deviceManager
        self._appName = appName
        self._remoteProfile = None
        self._remoteLog = remoteLog
        self.marionette = marionette
        self._is_emulator = False

        # Default our product to b2g
        self._product = "b2g"
        Automation.__init__(self)

    def setEmulator(self, is_emulator):
        self._is_emulator = is_emulator

    def setDeviceManager(self, deviceManager):
        self._devicemanager = deviceManager

    def setAppName(self, appName):
        self._appName = appName

    def setRemoteProfile(self, remoteProfile):
        self._remoteProfile = remoteProfile

    def setProduct(self, product):
        self._product = product

    def setRemoteLog(self, logfile):
        self._remoteLog = logfile

    # Set up what we need for the remote environment
    def environment(self, env=None, xrePath=None, crashreporter=True):
        # Because we are running remote, we don't want to mimic the local env
        # so no copying of os.environ
        if env is None:
            env = {}

        # We always hide the results table in B2G; it's much slower if we don't.
        env['MOZ_HIDE_RESULTS_TABLE'] = '1'
        return env

    def checkForCrashes(self, directory, symbolsPath):
        # XXX: This will have to be updated after crash reporting on b2g
        # is in place.
        dumpDir = tempfile.mkdtemp()
        self._devicemanager.getDirectory(self._remoteProfile + '/minidumps/', dumpDir)
        automationutils.checkForCrashes(dumpDir, symbolsPath, self.lastTestSeen)
        try:
          shutil.rmtree(dumpDir)
        except:
          print "WARNING: unable to remove directory: %s" % (dumpDir)

    def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
        # add b2g specific prefs
        extraPrefs.extend(["browser.manifestURL='dummy (bug 772307)'"])
        return Automation.initializeProfile(self, profileDir, extraPrefs, useServerLocations)

    def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
        # if remote profile is specified, use that instead
        if (self._remoteProfile):
            profileDir = self._remoteProfile

        cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)

        return app, args

    def getLanIp(self):
        nettools = NetworkTools()
        return nettools.getLanIp()

    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime,
                      debuggerInfo, symbolsPath, logger):
        """ Wait for mochitest to finish (as evidenced by a signature string
            in logcat), or for a given amount of time to elapse with no
            output.
        """
        timeout = timeout or 120

        didTimeout = False

        done = time.time() + timeout
        while True:
            currentlog = proc.stdout
            if currentlog:
                done = time.time() + timeout
                print currentlog
                if 'INFO SimpleTest FINISHED' in currentlog:
                    return 0
            else:
                if time.time() > done:
                    self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed "
                                  "out after %d seconds with no output",
                                  self.lastTestSeen, int(timeout))
                    return 1

    def getDeviceStatus(self, serial=None):
        # Get the current status of the device.  If we know the device
        # serial number, we look for that, otherwise we use the (presumably
        # only) device shown in 'adb devices'.
        serial = serial or self._devicemanager.deviceSerial
        status = 'unknown'

        for line in self._devicemanager.runCmd(['devices']).stdout.readlines():
            result =  re.match('(.*?)\t(.*)', line)
            if result:
                thisSerial = result.group(1)
                if not serial or thisSerial == serial:
                    serial = thisSerial
                    status = result.group(2)

        return (serial, status)

    def restartB2G(self):
        self._devicemanager.checkCmd(['shell', 'stop', 'b2g'])
        # Wait for a bit to make sure B2G has completely shut down.
        time.sleep(10)
        self._devicemanager.checkCmd(['shell', 'start', 'b2g'])
        if self._is_emulator:
            self.marionette.emulator.wait_for_port()

    def rebootDevice(self):
        # find device's current status and serial number
        serial, status = self.getDeviceStatus()

        # reboot!
        self._devicemanager.checkCmd(['reboot'])

        # wait for device to come back to previous status
        print 'waiting for device to come back online after reboot'
        start = time.time()
        rserial, rstatus = self.getDeviceStatus(serial)
        while rstatus != 'device':
            if time.time() - start > 120:
                # device hasn't come back online in 2 minutes, something's wrong
                raise Exception("Device %s (status: %s) not back online after reboot" % (serial, rstatus))
            time.sleep(5)
            rserial, rstatus = self.getDeviceStatus(serial)
        print 'device:', serial, 'status:', rstatus

    def Process(self, cmd, stdout=None, stderr=None, env=None, cwd=None):
        # On a desktop or fennec run, the Process method invokes a gecko
        # process in which to run mochitests.  For B2G, we simply
        # reboot the device (which was configured with a test profile
        # already), wait for B2G to start up, and then navigate to the
        # test url using Marionette.  There doesn't seem to be any way
        # to pass env variables into the B2G process, but this doesn't 
        # seem to matter.

        instance = self.B2GInstance(self._devicemanager)

        # reboot device so it starts up with the mochitest profile
        # XXX:  We could potentially use 'stop b2g' + 'start b2g' to achieve
        # a similar effect; will see which is more stable while attempting
        # to bring up the continuous integration.
        if self._is_emulator:
            self.restartB2G()
        else:
            self.rebootDevice()

        # Infrequently, gecko comes up before networking does, so wait a little
        # bit to give the network time to become available.
        # XXX:  need a more robust mechanism for this
        time.sleep(40)

        # Set up port forwarding again for Marionette, since any that
        # existed previously got wiped out by the reboot.
        if not self._is_emulator:
            self._devicemanager.checkCmd(['forward',
                                          'tcp:%s' % self.marionette.port,
                                          'tcp:%s' % self.marionette.port])

        # start a marionette session
        session = self.marionette.start_session()
        if 'b2g' not in session:
            raise Exception("bad session value %s returned by start_session" % session)

        # start the tests by navigating to the mochitest url
        self.marionette.execute_script("window.location.href='%s';" % self.testURL)

        return instance

    # be careful here as this inner class doesn't have access to outer class members
    class B2GInstance(object):
        """Represents a B2G instance running on a device, and exposes
           some process-like methods/properties that are expected by the
           automation.
        """

        def __init__(self, dm):
            self.dm = dm
            self.logcat_proc = None
            self.queue = Queue.Queue()

            # Launch logcat in a separate thread, and dump all output lines
            # into a queue.  The lines in this queue are
            # retrieved and returned by accessing the stdout property of
            # this class.
            cmd = [self.dm.adbPath]
            if self.dm.deviceSerial:
                cmd.extend(['-s', self.dm.deviceSerial])
            cmd.append('logcat')
            proc = threading.Thread(target=self._save_logcat_proc, args=(cmd, self.queue))
            proc.daemon = True
            proc.start()

        def _save_logcat_proc(self, cmd, queue):
            self.logcat_proc = LogcatProc(cmd, queue)
            self.logcat_proc.run()
            self.logcat_proc.waitForFinish()
            self.logcat_proc = None

        @property
        def pid(self):
            # a dummy value to make the automation happy
            return 0

        @property
        def stdout(self):
            # Return any lines in the queue used by the
            # logcat process handler.
            lines = []
            while True:
                try:
                    lines.append(self.queue.get_nowait())
                except Queue.Empty:
                    break
            return '\n'.join(lines)

        def wait(self, timeout = None):
            # this should never happen
            raise Exception("'wait' called on B2GInstance")

        def kill(self):
            # this should never happen
            raise Exception("'kill' called on B2GInstance")