mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-29 15:52:07 +00:00
c1802eb571
--HG-- extra : commitid : 3mgWZQdnDa4 extra : rebase_source : f8b91fff719dbeda7923a7e8c2c3e01a856d9b14
452 lines
18 KiB
Python
452 lines
18 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 datetime
|
|
import mozcrash
|
|
import threading
|
|
import os
|
|
import posixpath
|
|
import Queue
|
|
import re
|
|
import shutil
|
|
import signal
|
|
import tempfile
|
|
import time
|
|
import traceback
|
|
import zipfile
|
|
|
|
from automation import Automation
|
|
from mozlog.structured import get_default_logger
|
|
from mozprocess import ProcessHandlerMixin
|
|
|
|
|
|
class StdOutProc(ProcessHandlerMixin):
|
|
"""Process handler for b2g 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, context_chrome=True):
|
|
self._devicemanager = deviceManager
|
|
self._appName = appName
|
|
self._remoteProfile = None
|
|
self._remoteLog = remoteLog
|
|
self.marionette = marionette
|
|
self.context_chrome = context_chrome
|
|
self._is_emulator = False
|
|
self.test_script = None
|
|
self.test_script_args = None
|
|
|
|
# Default our product to b2g
|
|
self._product = "b2g"
|
|
self.lastTestSeen = "b2gautomation.py"
|
|
# Default log finish to mochitest standard
|
|
self.logFinish = 'INFO SimpleTest FINISHED'
|
|
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
|
|
|
|
def getExtensionIDFromRDF(self, rdfSource):
|
|
"""
|
|
Retrieves the extension id from an install.rdf file (or string).
|
|
"""
|
|
from xml.dom.minidom import parse, parseString, Node
|
|
|
|
if isinstance(rdfSource, file):
|
|
document = parse(rdfSource)
|
|
else:
|
|
document = parseString(rdfSource)
|
|
|
|
# Find the <em:id> element. There can be multiple <em:id> tags
|
|
# within <em:targetApplication> tags, so we have to check this way.
|
|
for rdfChild in document.documentElement.childNodes:
|
|
if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description":
|
|
for descChild in rdfChild.childNodes:
|
|
if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id":
|
|
return descChild.childNodes[0].data
|
|
return None
|
|
|
|
def installExtension(self, extensionSource, profileDir, extensionID=None):
|
|
# Bug 827504 - installing special-powers extension separately causes problems in B2G
|
|
if extensionID != "special-powers@mozilla.org":
|
|
if not os.path.isdir(profileDir):
|
|
self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir)
|
|
return
|
|
|
|
installRDFFilename = "install.rdf"
|
|
|
|
extensionsRootDir = os.path.join(profileDir, "extensions", "staged")
|
|
if not os.path.isdir(extensionsRootDir):
|
|
os.makedirs(extensionsRootDir)
|
|
|
|
if os.path.isfile(extensionSource):
|
|
reader = zipfile.ZipFile(extensionSource, "r")
|
|
|
|
for filename in reader.namelist():
|
|
# Sanity check the zip file.
|
|
if os.path.isabs(filename):
|
|
self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi")
|
|
return
|
|
|
|
# We may need to dig the extensionID out of the zip file...
|
|
if extensionID is None and filename == installRDFFilename:
|
|
extensionID = self.getExtensionIDFromRDF(reader.read(filename))
|
|
|
|
# We must know the extensionID now.
|
|
if extensionID is None:
|
|
self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
|
|
return
|
|
|
|
# Make the extension directory.
|
|
extensionDir = os.path.join(extensionsRootDir, extensionID)
|
|
os.mkdir(extensionDir)
|
|
|
|
# Extract all files.
|
|
reader.extractall(extensionDir)
|
|
|
|
elif os.path.isdir(extensionSource):
|
|
if extensionID is None:
|
|
filename = os.path.join(extensionSource, installRDFFilename)
|
|
if os.path.isfile(filename):
|
|
with open(filename, "r") as installRDF:
|
|
extensionID = self.getExtensionIDFromRDF(installRDF)
|
|
|
|
if extensionID is None:
|
|
self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
|
|
return
|
|
|
|
# Copy extension tree into its own directory.
|
|
# "destination directory must not already exist".
|
|
shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID))
|
|
|
|
else:
|
|
self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource)
|
|
|
|
# Set up what we need for the remote environment
|
|
def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False):
|
|
# 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 = {}
|
|
|
|
if crashreporter:
|
|
env['MOZ_CRASHREPORTER'] = '1'
|
|
env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
|
|
|
|
# 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 waitForNet(self):
|
|
active = False
|
|
time_out = 0
|
|
while not active and time_out < 40:
|
|
data = self._devicemanager._runCmd(['shell', '/system/bin/netcfg']).stdout.readlines()
|
|
data.pop(0)
|
|
for line in data:
|
|
if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)):
|
|
active = True
|
|
break
|
|
time_out += 1
|
|
time.sleep(1)
|
|
return active
|
|
|
|
def checkForCrashes(self, directory, symbolsPath):
|
|
crashed = False
|
|
remote_dump_dir = self._remoteProfile + '/minidumps'
|
|
print "checking for crashes in '%s'" % remote_dump_dir
|
|
if self._devicemanager.dirExists(remote_dump_dir):
|
|
local_dump_dir = tempfile.mkdtemp()
|
|
self._devicemanager.getDirectory(remote_dump_dir, local_dump_dir)
|
|
try:
|
|
logger = get_default_logger()
|
|
if logger is not None:
|
|
crashed = mozcrash.log_crashes(logger, local_dump_dir, symbolsPath, test=self.lastTestSeen)
|
|
else:
|
|
crashed = mozcrash.check_for_crashes(local_dump_dir, symbolsPath, test_name=self.lastTestSeen)
|
|
except:
|
|
traceback.print_exc()
|
|
finally:
|
|
shutil.rmtree(local_dump_dir)
|
|
self._devicemanager.removeDir(remote_dump_dir)
|
|
return crashed
|
|
|
|
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 waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime,
|
|
debuggerInfo, symbolsPath):
|
|
""" Wait for tests 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
|
|
while True:
|
|
currentlog = proc.getStdoutLines(timeout)
|
|
if currentlog:
|
|
print currentlog
|
|
# Match the test filepath from the last TEST-START line found in the new
|
|
# log content. These lines are in the form:
|
|
# ... INFO TEST-START | /filepath/we/wish/to/capture.html\n
|
|
testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", currentlog)
|
|
if testStartFilenames:
|
|
self.lastTestSeen = testStartFilenames[-1]
|
|
if hasattr(self, 'logFinish') and self.logFinish in currentlog:
|
|
return 0
|
|
else:
|
|
self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed "
|
|
"out after %d seconds with no output",
|
|
self.lastTestSeen, int(timeout))
|
|
self._devicemanager.killProcess('/system/b2g/b2g', sig=signal.SIGABRT)
|
|
|
|
timeout = 10 # seconds
|
|
starttime = datetime.datetime.now()
|
|
while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
|
|
if not self._devicemanager.processExist('/system/b2g/b2g'):
|
|
break
|
|
time.sleep(1)
|
|
else:
|
|
print "timed out after %d seconds waiting for b2g process to exit" % timeout
|
|
return 1
|
|
|
|
self.checkForCrashes(None, symbolsPath)
|
|
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):
|
|
# TODO hangs in subprocess.Popen without this delay
|
|
time.sleep(5)
|
|
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(self.marionette.port)
|
|
|
|
def rebootDevice(self):
|
|
# find device's current status and serial number
|
|
serial, status = self.getDeviceStatus()
|
|
|
|
# reboot!
|
|
self._devicemanager._runCmd(['shell', '/system/bin/reboot'])
|
|
|
|
# The above command can return while adb still thinks the device is
|
|
# connected, so wait a little bit for it to disconnect from adb.
|
|
time.sleep(10)
|
|
|
|
# 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 the tests. 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.
|
|
|
|
# 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 not self._is_emulator:
|
|
self.rebootDevice()
|
|
time.sleep(5)
|
|
#wait for wlan to come up
|
|
if not self.waitForNet():
|
|
raise Exception("network did not come up, please configure the network" +
|
|
" prior to running before running the automation framework")
|
|
|
|
# stop b2g
|
|
self._devicemanager._runCmd(['shell', 'stop', 'b2g'])
|
|
time.sleep(5)
|
|
|
|
# For some reason user.js in the profile doesn't get picked up.
|
|
# Manually copy it over to prefs.js. See bug 1009730 for more details.
|
|
self._devicemanager.moveTree(posixpath.join(self._remoteProfile, 'user.js'),
|
|
posixpath.join(self._remoteProfile, 'prefs.js'))
|
|
|
|
# relaunch b2g inside b2g instance
|
|
instance = self.B2GInstance(self._devicemanager, env=env)
|
|
|
|
time.sleep(5)
|
|
|
|
# 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])
|
|
|
|
if self._is_emulator:
|
|
self.marionette.emulator.wait_for_port(self.marionette.port)
|
|
else:
|
|
time.sleep(5)
|
|
|
|
# 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)
|
|
|
|
self.marionette.set_context(self.marionette.CONTEXT_CHROME)
|
|
self.marionette.execute_script("""
|
|
let SECURITY_PREF = "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer";
|
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
|
Services.prefs.setBoolPref(SECURITY_PREF, true);
|
|
|
|
if (!testUtils.hasOwnProperty("specialPowersObserver")) {
|
|
let loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
|
|
.getService(Components.interfaces.mozIJSSubScriptLoader);
|
|
loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js",
|
|
testUtils);
|
|
testUtils.specialPowersObserver = new testUtils.SpecialPowersObserver();
|
|
testUtils.specialPowersObserver.init();
|
|
testUtils.specialPowersObserver._loadFrameScript();
|
|
}
|
|
""")
|
|
|
|
if not self.context_chrome:
|
|
self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
|
|
|
|
# run the script that starts the tests
|
|
if self.test_script:
|
|
if os.path.isfile(self.test_script):
|
|
script = open(self.test_script, 'r')
|
|
self.marionette.execute_script(script.read(), script_args=self.test_script_args)
|
|
script.close()
|
|
elif isinstance(self.test_script, basestring):
|
|
self.marionette.execute_script(self.test_script, script_args=self.test_script_args)
|
|
else:
|
|
# assumes the tests are started on startup automatically
|
|
pass
|
|
|
|
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, env=None):
|
|
self.dm = dm
|
|
self.env = env or {}
|
|
self.stdout_proc = None
|
|
self.queue = Queue.Queue()
|
|
|
|
# Launch b2g 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('shell')
|
|
for k, v in self.env.iteritems():
|
|
cmd.append("%s=%s" % (k, v))
|
|
cmd.append('/system/bin/b2g.sh')
|
|
proc = threading.Thread(target=self._save_stdout_proc, args=(cmd, self.queue))
|
|
proc.daemon = True
|
|
proc.start()
|
|
|
|
def _save_stdout_proc(self, cmd, queue):
|
|
self.stdout_proc = StdOutProc(cmd, queue)
|
|
self.stdout_proc.run()
|
|
if hasattr(self.stdout_proc, 'processOutput'):
|
|
self.stdout_proc.processOutput()
|
|
self.stdout_proc.wait()
|
|
self.stdout_proc = None
|
|
|
|
@property
|
|
def pid(self):
|
|
# a dummy value to make the automation happy
|
|
return 0
|
|
|
|
def getStdoutLines(self, timeout):
|
|
# Return any lines in the queue used by the
|
|
# b2g process handler.
|
|
lines = []
|
|
# get all of the lines that are currently available
|
|
while True:
|
|
try:
|
|
lines.append(self.queue.get_nowait())
|
|
except Queue.Empty:
|
|
break
|
|
|
|
# wait 'timeout' for any additional lines
|
|
try:
|
|
lines.append(self.queue.get(True, timeout))
|
|
except Queue.Empty:
|
|
pass
|
|
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")
|
|
|