зеркало из https://github.com/mozilla/gecko-dev.git
274 строки
9.9 KiB
Python
274 строки
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")
|
|
|