gecko/build/mobile/b2gautomation.py

274 lines
9.9 KiB
Python

# 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")