mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-06 17:16:12 +00:00
c5f092dab4
It turns out that relying on the user to check return codes for every command was non-intuitive and resulted in many hard to trace bugs. Now most functinos just return "None", and raise a DMError when there's an exception. The exception to this are functions like dirExists, which now return booleans, and throw exceptions on error. This is a fairly major refactor, and also involved the following internal changes: * Removed FileError and AgentError exceptions, replaced with DMError (having to manage three different types of exceptions was confusing, all the more so when we're raising them) * Docstrings updated to remove references to return values where no longer relevant * pushFile no longer will create a directory to accomodate the file if it doesn't exist (this makes it consistent with devicemanagerADB) * dmSUT we validate the file, but assume that we get something back from the agent, instead of falling back to manual validation in the case that we didn't * isDir and dirExists had the same intention, but different implementations for dmSUT. Replaced the dmSUT impl of getDirectory with that of isDir's (which was much simpler). Removed isDir from devicemanager.py, since it wasn't used externally * killProcess modified to check for process existence before running (since the actual internal kill command will throw an exception if the process doesn't exist) In addition to all this, more unit tests have been added to test these changes for devicemanagerSUT.
532 lines
20 KiB
Python
532 lines
20 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 ConfigParser
|
|
import os
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import urllib
|
|
import traceback
|
|
|
|
# We need to know our current directory so that we can serve our test files from it.
|
|
SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
|
|
sys.path.insert(0, SCRIPT_DIRECTORY)
|
|
|
|
from automation import Automation
|
|
from b2gautomation import B2GRemoteAutomation
|
|
from runreftest import RefTest
|
|
from runreftest import ReftestOptions
|
|
from remotereftest import ReftestServer
|
|
|
|
from mozprofile import Profile
|
|
from mozrunner import Runner
|
|
|
|
import devicemanager
|
|
import devicemanagerADB
|
|
import manifestparser
|
|
|
|
from marionette import Marionette
|
|
|
|
|
|
class B2GOptions(ReftestOptions):
|
|
|
|
def __init__(self, automation, **kwargs):
|
|
defaults = {}
|
|
ReftestOptions.__init__(self, automation)
|
|
|
|
self.add_option("--b2gpath", action="store",
|
|
type = "string", dest = "b2gPath",
|
|
help = "path to B2G repo or qemu dir")
|
|
defaults["b2gPath"] = None
|
|
|
|
self.add_option("--marionette", action="store",
|
|
type = "string", dest = "marionette",
|
|
help = "host:port to use when connecting to Marionette")
|
|
defaults["marionette"] = None
|
|
|
|
self.add_option("--emulator", action="store",
|
|
type="string", dest = "emulator",
|
|
help = "Architecture of emulator to use: x86 or arm")
|
|
defaults["emulator"] = None
|
|
self.add_option("--emulator-res", action="store",
|
|
type="string", dest = "emulator_res",
|
|
help = "Emulator resolution of the format '<width>x<height>'")
|
|
defaults["emulator_res"] = None
|
|
|
|
self.add_option("--no-window", action="store_true",
|
|
dest = "noWindow",
|
|
help = "Pass --no-window to the emulator")
|
|
defaults["noWindow"] = False
|
|
|
|
self.add_option("--adbpath", action="store",
|
|
type = "string", dest = "adbPath",
|
|
help = "path to adb")
|
|
defaults["adbPath"] = "adb"
|
|
|
|
self.add_option("--deviceIP", action="store",
|
|
type = "string", dest = "deviceIP",
|
|
help = "ip address of remote device to test")
|
|
defaults["deviceIP"] = None
|
|
|
|
self.add_option("--devicePort", action="store",
|
|
type = "string", dest = "devicePort",
|
|
help = "port of remote device to test")
|
|
defaults["devicePort"] = 20701
|
|
|
|
self.add_option("--remote-logfile", action="store",
|
|
type = "string", dest = "remoteLogFile",
|
|
help = "Name of log file on the device relative to the device root. PLEASE ONLY USE A FILENAME.")
|
|
defaults["remoteLogFile"] = None
|
|
|
|
self.add_option("--remote-webserver", action = "store",
|
|
type = "string", dest = "remoteWebServer",
|
|
help = "ip address where the remote web server is hosted at")
|
|
defaults["remoteWebServer"] = None
|
|
|
|
self.add_option("--http-port", action = "store",
|
|
type = "string", dest = "httpPort",
|
|
help = "ip address where the remote web server is hosted at")
|
|
defaults["httpPort"] = automation.DEFAULT_HTTP_PORT
|
|
|
|
self.add_option("--ssl-port", action = "store",
|
|
type = "string", dest = "sslPort",
|
|
help = "ip address where the remote web server is hosted at")
|
|
defaults["sslPort"] = automation.DEFAULT_SSL_PORT
|
|
|
|
self.add_option("--pidfile", action = "store",
|
|
type = "string", dest = "pidFile",
|
|
help = "name of the pidfile to generate")
|
|
defaults["pidFile"] = ""
|
|
defaults["remoteTestRoot"] = None
|
|
defaults["logFile"] = "reftest.log"
|
|
defaults["autorun"] = True
|
|
defaults["closeWhenDone"] = True
|
|
defaults["testPath"] = ""
|
|
|
|
self.set_defaults(**defaults)
|
|
|
|
def verifyRemoteOptions(self, options):
|
|
options.remoteTestRoot = self._automation._devicemanager.getDeviceRoot() + "/reftest"
|
|
options.remoteProfile = options.remoteTestRoot + "/profile"
|
|
|
|
productRoot = options.remoteTestRoot + "/" + self._automation._product
|
|
if options.utilityPath == self._automation.DIST_BIN:
|
|
options.utilityPath = productRoot + "/bin"
|
|
|
|
if options.remoteWebServer == None:
|
|
if os.name != "nt":
|
|
options.remoteWebServer = self._automation.getLanIp()
|
|
else:
|
|
print "ERROR: you must specify a --remote-webserver=<ip address>\n"
|
|
return None
|
|
|
|
options.webServer = options.remoteWebServer
|
|
|
|
#if not options.emulator and not options.deviceIP:
|
|
# print "ERROR: you must provide a device IP"
|
|
# return None
|
|
|
|
if options.remoteLogFile == None:
|
|
options.remoteLogFile = "reftest.log"
|
|
|
|
options.localLogName = options.remoteLogFile
|
|
options.remoteLogFile = options.remoteTestRoot + '/' + options.remoteLogFile
|
|
|
|
# Ensure that the options.logfile (which the base class uses) is set to
|
|
# the remote setting when running remote. Also, if the user set the
|
|
# log file name there, use that instead of reusing the remotelogfile as above.
|
|
if (options.logFile):
|
|
# If the user specified a local logfile name use that
|
|
options.localLogName = options.logFile
|
|
options.logFile = options.remoteLogFile
|
|
|
|
# Only reset the xrePath if it wasn't provided
|
|
if options.xrePath == None:
|
|
options.xrePath = options.utilityPath
|
|
options.xrePath = os.path.abspath(options.xrePath)
|
|
|
|
if options.pidFile != "":
|
|
f = open(options.pidFile, 'w')
|
|
f.write("%s" % os.getpid())
|
|
f.close()
|
|
|
|
return options
|
|
|
|
|
|
class ProfileConfigParser(ConfigParser.RawConfigParser):
|
|
"""Subclass of RawConfigParser that outputs .ini files in the exact
|
|
format expected for profiles.ini, which is slightly different
|
|
than the default format.
|
|
"""
|
|
|
|
def optionxform(self, optionstr):
|
|
return optionstr
|
|
|
|
def write(self, fp):
|
|
if self._defaults:
|
|
fp.write("[%s]\n" % ConfigParser.DEFAULTSECT)
|
|
for (key, value) in self._defaults.items():
|
|
fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t')))
|
|
fp.write("\n")
|
|
for section in self._sections:
|
|
fp.write("[%s]\n" % section)
|
|
for (key, value) in self._sections[section].items():
|
|
if key == "__name__":
|
|
continue
|
|
if (value is not None) or (self._optcre == self.OPTCRE):
|
|
key = "=".join((key, str(value).replace('\n', '\n\t')))
|
|
fp.write("%s\n" % (key))
|
|
fp.write("\n")
|
|
|
|
|
|
class B2GReftest(RefTest):
|
|
|
|
_automation = None
|
|
_devicemanager = None
|
|
localProfile = None
|
|
remoteApp = ''
|
|
profile = None
|
|
|
|
def __init__(self, automation, devicemanager, options, scriptDir):
|
|
self._automation = automation
|
|
RefTest.__init__(self, self._automation)
|
|
self._devicemanager = devicemanager
|
|
self.runSSLTunnel = False
|
|
self.remoteTestRoot = options.remoteTestRoot
|
|
self.remoteProfile = options.remoteProfile
|
|
self._automation.setRemoteProfile(self.remoteProfile)
|
|
self.localLogName = options.localLogName
|
|
self.remoteLogFile = options.remoteLogFile
|
|
self.userJS = '/data/local/user.js'
|
|
self.testDir = '/data/local/tests'
|
|
self.remoteMozillaPath = '/data/b2g/mozilla'
|
|
self.remoteProfilesIniPath = os.path.join(self.remoteMozillaPath, 'profiles.ini')
|
|
self.originalProfilesIni = None
|
|
self.scriptDir = scriptDir
|
|
self.SERVER_STARTUP_TIMEOUT = 90
|
|
if self._automation.IS_DEBUG_BUILD:
|
|
self.SERVER_STARTUP_TIMEOUT = 180
|
|
|
|
def cleanup(self, profileDir):
|
|
# Pull results back from device
|
|
if (self.remoteLogFile):
|
|
try:
|
|
self._devicemanager.getFile(self.remoteLogFile, self.localLogName)
|
|
except:
|
|
print "ERROR: We were not able to retrieve the info from %s" % self.remoteLogFile
|
|
sys.exit(5)
|
|
|
|
# Restore the original profiles.ini.
|
|
if self.originalProfilesIni:
|
|
try:
|
|
if not self._automation._is_emulator:
|
|
self.restoreProfilesIni()
|
|
os.remove(self.originalProfilesIni)
|
|
except:
|
|
pass
|
|
|
|
if not self._automation._is_emulator:
|
|
self._devicemanager.removeFile(self.remoteLogFile)
|
|
self._devicemanager.removeDir(self.remoteProfile)
|
|
self._devicemanager.removeDir(self.remoteTestRoot)
|
|
|
|
# Restore the original user.js.
|
|
self._devicemanager.checkCmdAs(['shell', 'rm', '-f', self.userJS])
|
|
if self._devicemanager.useDDCopy:
|
|
self._devicemanager.checkCmdAs(['shell', 'dd', 'if=%s.orig' % self.userJS, 'of=%s' % self.userJS])
|
|
else:
|
|
self._devicemanager.checkCmdAs(['shell', 'cp', '%s.orig' % self.userJS, self.userJS])
|
|
|
|
# We've restored the original profile, so reboot the device so that
|
|
# it gets picked up.
|
|
self._automation.rebootDevice()
|
|
|
|
RefTest.cleanup(self, profileDir)
|
|
if getattr(self, 'pidFile', '') != '':
|
|
try:
|
|
os.remove(self.pidFile)
|
|
os.remove(self.pidFile + ".xpcshell.pid")
|
|
except:
|
|
print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % self.pidFile
|
|
|
|
def findPath(self, paths, filename = None):
|
|
for path in paths:
|
|
p = path
|
|
if filename:
|
|
p = os.path.join(p, filename)
|
|
if os.path.exists(self.getFullPath(p)):
|
|
return path
|
|
return None
|
|
|
|
def startWebServer(self, options):
|
|
""" Create the webserver on the host and start it up """
|
|
remoteXrePath = options.xrePath
|
|
remoteProfilePath = self.remoteProfile
|
|
remoteUtilityPath = options.utilityPath
|
|
localAutomation = Automation()
|
|
localAutomation.IS_WIN32 = False
|
|
localAutomation.IS_LINUX = False
|
|
localAutomation.IS_MAC = False
|
|
localAutomation.UNIXISH = False
|
|
hostos = sys.platform
|
|
if hostos in ['mac', 'darwin']:
|
|
localAutomation.IS_MAC = True
|
|
elif hostos in ['linux', 'linux2']:
|
|
localAutomation.IS_LINUX = True
|
|
localAutomation.UNIXISH = True
|
|
elif hostos in ['win32', 'win64']:
|
|
localAutomation.BIN_SUFFIX = ".exe"
|
|
localAutomation.IS_WIN32 = True
|
|
|
|
paths = [options.xrePath,
|
|
localAutomation.DIST_BIN,
|
|
self._automation._product,
|
|
os.path.join('..', self._automation._product)]
|
|
options.xrePath = self.findPath(paths)
|
|
if options.xrePath == None:
|
|
print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name)
|
|
sys.exit(1)
|
|
paths.append("bin")
|
|
paths.append(os.path.join("..", "bin"))
|
|
|
|
xpcshell = "xpcshell"
|
|
if (os.name == "nt"):
|
|
xpcshell += ".exe"
|
|
|
|
if (options.utilityPath):
|
|
paths.insert(0, options.utilityPath)
|
|
options.utilityPath = self.findPath(paths, xpcshell)
|
|
if options.utilityPath == None:
|
|
print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name)
|
|
sys.exit(1)
|
|
|
|
options.serverProfilePath = tempfile.mkdtemp()
|
|
self.server = ReftestServer(localAutomation, options, self.scriptDir)
|
|
retVal = self.server.start()
|
|
if retVal:
|
|
return retVal
|
|
|
|
if (options.pidFile != ""):
|
|
f = open(options.pidFile + ".xpcshell.pid", 'w')
|
|
f.write("%s" % self.server._process.pid)
|
|
f.close()
|
|
|
|
retVal = self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
|
|
if retVal:
|
|
return retVal
|
|
|
|
options.xrePath = remoteXrePath
|
|
options.utilityPath = remoteUtilityPath
|
|
options.profilePath = remoteProfilePath
|
|
return 0
|
|
|
|
def stopWebServer(self, options):
|
|
if hasattr(self, 'server'):
|
|
self.server.stop()
|
|
|
|
|
|
def restoreProfilesIni(self):
|
|
# restore profiles.ini on the device to its previous state
|
|
if not self.originalProfilesIni or not os.access(self.originalProfilesIni, os.F_OK):
|
|
raise devicemanager.DMError('Unable to install original profiles.ini; file not found: %s',
|
|
self.originalProfilesIni)
|
|
|
|
self._devicemanager.pushFile(self.originalProfilesIni, self.remoteProfilesIniPath)
|
|
|
|
def updateProfilesIni(self, profilePath):
|
|
# update profiles.ini on the device to point to the test profile
|
|
self.originalProfilesIni = tempfile.mktemp()
|
|
self._devicemanager.getFile(self.remoteProfilesIniPath, self.originalProfilesIni)
|
|
|
|
config = ProfileConfigParser()
|
|
config.read(self.originalProfilesIni)
|
|
for section in config.sections():
|
|
if 'Profile' in section:
|
|
config.set(section, 'IsRelative', 0)
|
|
config.set(section, 'Path', profilePath)
|
|
|
|
newProfilesIni = tempfile.mktemp()
|
|
with open(newProfilesIni, 'wb') as configfile:
|
|
config.write(configfile)
|
|
|
|
self._devicemanager.pushFile(newProfilesIni, self.remoteProfilesIniPath)
|
|
try:
|
|
os.remove(newProfilesIni)
|
|
except:
|
|
pass
|
|
|
|
|
|
def createReftestProfile(self, options, profileDir, reftestlist):
|
|
print "profileDir: " + str(profileDir)
|
|
retVal = RefTest.createReftestProfile(self, options, profileDir, reftestlist, server=options.remoteWebServer)
|
|
|
|
# Turn off the locale picker screen
|
|
fhandle = open(os.path.join(profileDir, "user.js"), 'a')
|
|
fhandle.write("""
|
|
user_pref("browser.firstrun.show.localepicker", false);
|
|
user_pref("browser.homescreenURL","app://system.gaiamobile.org");\n
|
|
user_pref("browser.manifestURL","app://system.gaiamobile.org/manifest.webapp");\n
|
|
user_pref("browser.tabs.remote", false);\n
|
|
user_pref("dom.ipc.browser_frames.oop_by_default", true);\n
|
|
user_pref("dom.ipc.tabs.disabled", false);\n
|
|
user_pref("dom.mozBrowserFramesEnabled", true);\n
|
|
user_pref("dom.mozBrowserFramesWhitelist","app://system.gaiamobile.org");\n
|
|
user_pref("network.dns.localDomains","app://system.gaiamobile.org");\n
|
|
user_pref("font.size.inflation.emPerLine", 0);
|
|
user_pref("font.size.inflation.minTwips", 0);
|
|
user_pref("reftest.browser.iframe.enabled", true);
|
|
user_pref("reftest.remote", true);
|
|
user_pref("reftest.uri", "%s");
|
|
user_pref("toolkit.telemetry.prompted", true);
|
|
""" % reftestlist)
|
|
|
|
#workaround for jsreftests.
|
|
if getattr(options, 'enablePrivilege', False):
|
|
fhandle.write("""
|
|
user_pref("capability.principal.codebase.p2.granted", "UniversalXPConnect");
|
|
user_pref("capability.principal.codebase.p2.id", "http://%s:%s");
|
|
""" % (options.remoteWebServer, options.httpPort))
|
|
|
|
# Close the file
|
|
fhandle.close()
|
|
|
|
# Copy the profile to the device.
|
|
self._devicemanager.removeDir(self.remoteProfile)
|
|
try:
|
|
self._devicemanager.pushDir(profileDir, self.remoteProfile)
|
|
except devicemanager.DMError:
|
|
print "Automation Error: Unable to copy profile to device."
|
|
raise
|
|
|
|
# In B2G, user.js is always read from /data/local, not the profile
|
|
# directory. Backup the original user.js first so we can restore it.
|
|
self._devicemanager.checkCmdAs(['shell', 'rm', '-f', '%s.orig' % self.userJS])
|
|
if self._devicemanager.useDDCopy:
|
|
self._devicemanager.checkCmdAs(['shell', 'dd', 'if=%s' % self.userJS, 'of=%s.orig' % self.userJS])
|
|
else:
|
|
self._devicemanager.checkCmdAs(['shell', 'cp', self.userJS, '%s.orig' % self.userJS])
|
|
self._devicemanager.pushFile(os.path.join(profileDir, "user.js"), self.userJS)
|
|
|
|
self.updateProfilesIni(self.remoteProfile)
|
|
|
|
options.profilePath = self.remoteProfile
|
|
return retVal
|
|
|
|
def copyExtraFilesToProfile(self, options, profileDir):
|
|
RefTest.copyExtraFilesToProfile(self, options, profileDir)
|
|
try:
|
|
self._devicemanager.pushDir(profileDir, options.remoteProfile)
|
|
except devicemanager.DMError:
|
|
print "Automation Error: Failed to copy extra files to device"
|
|
raise
|
|
|
|
def getManifestPath(self, path):
|
|
return path
|
|
|
|
|
|
def main(args=sys.argv[1:]):
|
|
auto = B2GRemoteAutomation(None, "fennec", context_chrome=True)
|
|
parser = B2GOptions(auto)
|
|
options, args = parser.parse_args(args)
|
|
|
|
# create our Marionette instance
|
|
kwargs = {}
|
|
if options.emulator:
|
|
kwargs['emulator'] = options.emulator
|
|
auto.setEmulator(True)
|
|
if options.noWindow:
|
|
kwargs['noWindow'] = True
|
|
if options.emulator_res:
|
|
kwargs['emulator_res'] = options.emulator_res
|
|
if options.b2gPath:
|
|
kwargs['homedir'] = options.b2gPath
|
|
if options.marionette:
|
|
host,port = options.marionette.split(':')
|
|
kwargs['host'] = host
|
|
kwargs['port'] = int(port)
|
|
marionette = Marionette(**kwargs)
|
|
auto.marionette = marionette
|
|
|
|
# create the DeviceManager
|
|
kwargs = {'adbPath': options.adbPath}
|
|
if options.deviceIP:
|
|
kwargs.update({'host': options.deviceIP,
|
|
'port': options.devicePort})
|
|
dm = devicemanagerADB.DeviceManagerADB(**kwargs)
|
|
auto.setDeviceManager(dm)
|
|
|
|
options = parser.verifyRemoteOptions(options)
|
|
if (options == None):
|
|
print "ERROR: Invalid options specified, use --help for a list of valid options"
|
|
sys.exit(1)
|
|
|
|
# TODO fix exception
|
|
if not options.ignoreWindowSize:
|
|
parts = dm.getInfo('screen')['screen'][0].split()
|
|
width = int(parts[0].split(':')[1])
|
|
height = int(parts[1].split(':')[1])
|
|
if (width < 1366 or height < 1050):
|
|
print "ERROR: Invalid screen resolution %sx%s, please adjust to 1366x1050 or higher" % (width, height)
|
|
return 1
|
|
|
|
auto.setProduct("b2g")
|
|
auto.test_script = os.path.join(SCRIPT_DIRECTORY, 'b2g_start_script.js')
|
|
auto.test_script_args = [options.remoteWebServer, options.httpPort]
|
|
auto.logFinish = "REFTEST TEST-START | Shutdown"
|
|
|
|
reftest = B2GReftest(auto, dm, options, SCRIPT_DIRECTORY)
|
|
# Create /data/local/tests, to force its use by DeviceManagerADB;
|
|
# B2G won't run correctly with the profile installed to /mnt/sdcard.
|
|
dm.mkDirs(reftest.testDir)
|
|
|
|
logParent = os.path.dirname(options.remoteLogFile)
|
|
dm.mkDir(logParent);
|
|
auto.setRemoteLog(options.remoteLogFile)
|
|
auto.setServerInfo(options.webServer, options.httpPort, options.sslPort)
|
|
|
|
# Dynamically build the reftest URL if possible, beware that args[0] should exist 'inside' the webroot
|
|
manifest = args[0]
|
|
if os.path.exists(os.path.join(SCRIPT_DIRECTORY, args[0])):
|
|
manifest = "http://%s:%s/%s" % (options.remoteWebServer, options.httpPort, args[0])
|
|
elif os.path.exists(args[0]):
|
|
manifestPath = os.path.abspath(args[0]).split(SCRIPT_DIRECTORY)[1].strip('/')
|
|
manifest = "http://%s:%s/%s" % (options.remoteWebServer, options.httpPort, manifestPath)
|
|
else:
|
|
print "ERROR: Could not find test manifest '%s'" % manifest
|
|
return 1
|
|
|
|
# Start the webserver
|
|
retVal = 1
|
|
try:
|
|
retVal = reftest.startWebServer(options)
|
|
if retVal:
|
|
return retVal
|
|
procName = options.app.split('/')[-1]
|
|
if (dm.processExist(procName)):
|
|
dm.killProcess(procName)
|
|
|
|
cmdlineArgs = ["-reftest", manifest]
|
|
if getattr(options, 'bootstrap', False):
|
|
cmdlineArgs = []
|
|
|
|
retVal = reftest.runTests(manifest, options, cmdlineArgs)
|
|
except:
|
|
print "TEST-UNEXPECTED-FAIL | %s | Exception caught while running tests." % sys.exc_info()[1]
|
|
traceback.print_exc()
|
|
reftest.stopWebServer(options)
|
|
try:
|
|
reftest.cleanup(None)
|
|
except:
|
|
pass
|
|
return 1
|
|
|
|
reftest.stopWebServer(options)
|
|
return retVal
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|
|
|