gecko-dev/layout/tools/reftest/runreftestb2g.py
William Lachance c5f092dab4 Bug 795496 - Make mozdevice raise exceptions on error;r=ahal,jmaher
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.
2012-10-04 11:28:07 -04:00

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())