Bug 668349 - Add or update script to run xpcshell tests on Android; r=jmaher

This commit is contained in:
Geoff Brown 2011-08-22 09:00:50 +01:00
Родитель f9e507aa2c
Коммит 957faf5ca3
3 изменённых файлов: 262 добавлений и 208 удалений

Просмотреть файл

@ -32,7 +32,11 @@ class DeviceManagerADB(DeviceManager):
self.tmpDir = None
try:
# a test to see if we have root privs
self.checkCmd(["shell", "ls", "/sbin"])
files = self.listFiles("/data/data")
if (len(files) == 1):
if (files[0].find("Permission denied") != -1):
print "NOT running as root"
raise Exception("not running as root")
except:
try:
self.checkCmd(["root"])
@ -103,7 +107,7 @@ class DeviceManagerADB(DeviceManager):
try:
if (not self.dirExists(remoteDir)):
self.mkDirs(remoteDir+"/x")
for root, dirs, files in os.walk(localDir):
for root, dirs, files in os.walk(localDir, followlinks='true'):
relRoot = os.path.relpath(root, localDir)
for file in files:
localFile = os.path.join(root, file)
@ -139,8 +143,12 @@ class DeviceManagerADB(DeviceManager):
# success: True
# failure: False
def fileExists(self, filepath):
self.checkCmd(["shell", "ls", filepath])
return True
p = self.runCmd(["shell", "ls", "-a", filepath])
data = p.stdout.readlines()
if (len(data) == 1):
if (data[0].rstrip() == filepath):
return True
return False
def removeFile(self, filename):
return self.runCmd(["shell", "rm", filename]).stdout.read()

Просмотреть файл

@ -36,265 +36,302 @@
#
# ***** END LICENSE BLOCK ***** */
import re, sys, os, os.path, logging, shutil, signal
from glob import glob
from optparse import OptionParser
from subprocess import Popen, PIPE, STDOUT
from tempfile import mkdtemp
import re, sys, os
import runxpcshelltests as xpcshell
from automationutils import *
import devicemanager
import devicemanager, devicemanagerADB, devicemanagerSUT
# A specialization of XPCShellTests that runs tests on an Android device
# via devicemanager.
class XPCShellRemote(xpcshell.XPCShellTests, object):
def __init__(self, devmgr):
self.device = devmgr
self.testRoot = "/tests/xpcshell"
def __init__(self, devmgr, options, args):
xpcshell.XPCShellTests.__init__(self)
self.profileDir = self.testRoot + '/profile'
self.device.mkDir(self.profileDir)
self.options = options
self.device = devmgr
self.pathMapping = []
self.remoteTestRoot = self.device.getTestRoot("xpcshell")
# Terse directory names are used here ("b" for a binaries directory)
# to minimize the length of the command line used to execute
# xpcshell on the remote device. adb has a limit to the number
# of characters used in a shell command, and the xpcshell command
# line can be quite complex.
self.remoteBinDir = self.remoteJoin(self.remoteTestRoot, "b")
self.remoteScriptsDir = self.remoteTestRoot
self.remoteComponentsDir = self.remoteJoin(self.remoteTestRoot, "c")
self.profileDir = self.remoteJoin(self.remoteTestRoot, "p")
if options.setup:
self.setupUtilities()
self.setupTestDir()
self.remoteAPK = self.remoteJoin(self.remoteBinDir, os.path.basename(options.localAPK))
self.remoteDebugger = options.debugger
self.remoteDebuggerArgs = options.debuggerArgs
#todo: figure out the remote version of this, only used for debuggerInfo
def getcwd(self):
return "/tests/"
def readManifest(self, manifest):
"""Given a manifest file containing a list of test directories,
return a list of absolute paths to the directories contained within."""
def remoteJoin(self, path1, path2):
joined = os.path.join(path1, path2)
joined = joined.replace('\\', '/')
return joined
manifestdir = self.testRoot + '/tests'
testdirs = []
try:
f = self.device.getFile(manifest, "temp.txt")
for line in f.split():
dir = line.rstrip()
path = manifestdir + '/' + dir
testdirs.append(path)
f.close()
except:
pass # just eat exceptions
return testdirs
def remoteForLocal(self, local):
for mapping in self.pathMapping:
if (os.path.abspath(mapping.local) == os.path.abspath(local)):
return mapping.remote
return local
def verifyFilePath(self, fileName):
# approot - path to root of application - firefox or fennec
# xreroot - path to xulrunner binaries - firefox or fennec/xulrunner
# xpcshell - full or relative path to xpcshell binary
#given fileName, returns full path of existing file
if (self.device.fileExists(fileName)):
return fileName
fileName = self.device.getAppRoot() + '/xulrunner/' + fileName.split('/')[-1]
if (self.device.fileExists(fileName)):
return fileName
fileName = self.device.getAppRoot() + '/' + fileName.split('/')[-1]
if (not self.device.fileExists(fileName)):
raise devicemanager.FileError("No File found for: " + str(fileName))
def setupUtilities(self):
remotePrefDir = self.remoteJoin(self.remoteBinDir, "defaults/pref")
if (not self.device.dirExists(remotePrefDir)):
self.device.mkDirs(self.remoteJoin(remotePrefDir, "extra"))
if (not self.device.dirExists(self.remoteScriptsDir)):
self.device.mkDir(self.remoteScriptsDir)
if (not self.device.dirExists(self.remoteComponentsDir)):
self.device.mkDir(self.remoteComponentsDir)
return fileName
local = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'head.js')
self.device.pushFile(local, self.remoteScriptsDir)
def verifyDirPath(self, fileName):
# approot - path to root of application - firefox or fennec
# xreroot - path to xulrunner binaries - firefox or fennec/xulrunner
# xpcshell - full or relative path to xpcshell binary
#given fileName, returns full path of existing file
if (self.device.dirExists(fileName)):
return fileName
fileName = self.device.getAppRoot() + '/' + fileName.split('/')[-1]
if (self.device.dirExists(fileName)):
return fileName
fileName = self.device.getDeviceRoot() + '/' + fileName.split('/')[-1]
if (not self.device.dirExists(fileName)):
raise devicemanager.FileError("No Dir found for: " + str(fileName))
return fileName
localBin = os.path.join(self.options.objdir, "dist/bin")
if not os.path.exists(localBin):
localBin = os.path.join(self.options.objdir, "bin")
if not os.path.exists(localBin):
print >>sys.stderr, "Error: could not find bin in objdir"
sys.exit(1)
def setAbsPath(self):
#testharnessdir is used for head.js
self.testharnessdir = "/tests/xpcshell/"
local = os.path.join(localBin, "xpcshell")
self.device.pushFile(local, self.remoteBinDir)
# If the file exists then we have a full path (no notion of cwd)
self.xpcshell = self.verifyFilePath(self.xpcshell)
if self.xrePath is None:
# If no xrePath, assume it is the directory containing xpcshell
self.xrePath = '/'.join(self.xpcshell.split('/')[:-1])
else:
self.xrePath = self.verifyDirPath(self.xrePath)
local = os.path.join(localBin, "plugin-container")
self.device.pushFile(local, self.remoteBinDir)
# we assume that httpd.js lives in components/ relative to xpcshell
self.httpdJSPath = self.xrePath + '/components/httpd.js'
local = os.path.join(localBin, "components/httpd.js")
self.device.pushFile(local, self.remoteComponentsDir)
local = os.path.join(localBin, "components/httpd.manifest")
self.device.pushFile(local, self.remoteComponentsDir)
local = os.path.join(localBin, "components/test_necko.xpt")
self.device.pushFile(local, self.remoteComponentsDir)
self.device.pushFile(self.options.localAPK, self.remoteBinDir)
localLib = os.path.join(self.options.objdir, "dist/fennec")
if not os.path.exists(localLib):
localLib = os.path.join(self.options.objdir, "fennec/lib")
if not os.path.exists(localLib):
print >>sys.stderr, "Error: could not find libs in objdir"
sys.exit(1)
for file in os.listdir(localLib):
if (file.endswith(".so")):
self.device.pushFile(os.path.join(localLib, file), self.remoteBinDir)
def setupTestDir(self):
xpcDir = os.path.join(self.options.objdir, "_tests/xpcshell")
self.device.pushDir(xpcDir, self.remoteScriptsDir)
def buildTestList(self):
xpcshell.XPCShellTests.buildTestList(self)
uniqueTestPaths = set([])
for test in self.alltests:
uniqueTestPaths.add(test['here'])
for testdir in uniqueTestPaths:
xpcDir = os.path.join(self.options.objdir, "_tests/xpcshell")
abbrevTestDir = os.path.relpath(testdir, xpcDir)
remoteScriptDir = self.remoteJoin(self.remoteScriptsDir, abbrevTestDir)
self.pathMapping.append(PathMapping(testdir, remoteScriptDir))
def buildXpcsCmd(self, testdir):
# <head.js> has to be loaded by xpchell: it can't load itself.
self.env["XPCSHELL_TEST_PROFILE_DIR"] = self.profileDir
self.xpcsCmd = [self.xpcshell, '-g', self.xrePath, '-v', '170', '-j', '-s', \
"--environ:CWD='" + testdir + "'", \
"--environ:XPCSHELL_TEST_PROFILE_DIR='" + self.env["XPCSHELL_TEST_PROFILE_DIR"] + "'", \
'-e', 'const _HTTPD_JS_PATH = \'%s\';' % self.httpdJSPath,
'-f', self.testharnessdir + '/head.js']
self.xpcsCmd = [
self.remoteJoin(self.remoteBinDir, "xpcshell"),
'-r', self.remoteJoin(self.remoteComponentsDir, 'httpd.manifest'),
'--greomni', self.remoteAPK,
'-j', '-s',
'-e', 'const _HTTPD_JS_PATH = "%s";' % self.remoteJoin(self.remoteComponentsDir, 'httpd.js'),
'-e', 'const _HEAD_JS_PATH = "%s";' % self.remoteJoin(self.remoteScriptsDir, 'head.js'),
'-f', self.remoteScriptsDir+'/head.js']
if self.debuggerInfo:
self.xpcsCmd = [self.debuggerInfo["path"]] + self.debuggerInfo["args"] + self.xpcsCmd
if self.remoteDebugger:
# for example, "/data/local/gdbserver" "localhost:12345"
self.xpcsCmd = [
self.remoteDebugger,
self.remoteDebuggerArgs,
self.xpcsCmd]
def getHeadFiles(self, testdir):
# get the list of head and tail files from the directory
testHeadFiles = []
for f in self.device.listFiles(testdir):
hdmtch = re.compile("head_.*\.js")
if (hdmtch.match(f)):
testHeadFiles += [(testdir + '/' + f).replace('/', '//')]
return sorted(testHeadFiles)
def getTailFiles(self, testdir):
testTailFiles = []
# Tails are executed in the reverse order, to "match" heads order,
# as in "h1-h2-h3 then t3-t2-t1".
for f in self.device.listFiles(testdir):
tlmtch = re.compile("tail_.*\.js")
if (tlmtch.match(f)):
testTailFiles += [(testdir + '/' + f).replace('/', '//')]
return reversed(sorted(testTailFiles))
def getHeadFiles(self, test):
self.remoteHere = self.remoteForLocal(test['here'])
return [f.strip() for f in sorted(test['head'].split(' ')) if self.device.fileExists(self.remoteJoin(self.remoteHere, f))]
def getTailFiles(self, test):
return [f.strip() for f in sorted(test['tail'].split(' ')) if self.device.fileExists(self.remoteJoin(self.remoteHere, f))]
def getTestFiles(self, testdir):
testfiles = []
# if a single test file was specified, we only want to execute that test
for f in self.device.listFiles(testdir):
tstmtch = re.compile("test_.*\.js")
if (tstmtch.match(f)):
testfiles += [(testdir + '/' + f).replace('/', '//')]
for f in testfiles:
if (self.singleFile == f.split('/')[-1]):
return [(testdir + '/' + f).replace('/', '//')]
else:
pass
return testfiles
def buildCmdTestFile(self, name):
remoteDir = self.remoteForLocal(os.path.dirname(name))
if remoteDir == self.remoteHere:
remoteName = os.path.basename(name)
else:
remoteName = self.remoteJoin(remoteDir, os.path.basename(name))
return ['-e', 'const _TEST_FILE = ["%s"];' %
replaceBackSlashes(remoteName)]
def setupProfileDir(self):
self.device.removeDir(self.profileDir)
self.device.mkDir(self.profileDir)
self.env["XPCSHELL_TEST_PROFILE_DIR"] = self.profileDir
if self.interactive or self.singleFile:
self.log.info("TEST-INFO | profile dir is %s" % self.profileDir)
return self.profileDir
def setupLeakLogging(self):
filename = "runxpcshelltests_leaks.log"
# Enable leaks (only) detection to its own log file.
leakLogFile = self.profileDir + '/' + filename
self.env["XPCOM_MEM_LEAK_LOG"] = leakLogFile
return leakLogFile
def launchProcess(self, cmd, stdout, stderr, env, cwd):
print "launching : " + " ".join(cmd)
proc = self.device.launchProcess(cmd, cwd=cwd)
# Some xpcshell arguments contain characters that are interpretted
# by the adb shell; enclose these arguments in quotes.
index = 0
for part in cmd:
if (part.find(" ")>=0 or part.find("(")>=0 or part.find(")")>=0 or part.find("\"")>=0):
part = '\''+part+'\''
cmd[index] = part
index = index + 1
xpcshell = self.remoteJoin(self.remoteBinDir, "xpcshell")
shellArgs = "cd "+self.remoteHere
shellArgs += "; LD_LIBRARY_PATH="+self.remoteBinDir
shellArgs += "; export CACHE_PATH="+self.remoteBinDir
shellArgs += "; export GRE_HOME="+self.device.getAppRoot()
shellArgs += "; export XPCSHELL_TEST_PROFILE_DIR="+self.profileDir
shellArgs += "; "+xpcshell+" "
shellArgs += " ".join(cmd[1:])
if self.verbose:
self.log.info(shellArgs)
# If the adb version of devicemanager is used and the arguments passed
# to adb exceed ~1024 characters, the command may not execute.
if len(shellArgs) > 1000:
self.log.info("adb command length is excessive and may cause failure")
proc = self.device.runCmd(["shell", shellArgs])
return proc
def setSignal(self, proc, sig1, sig2):
self.device.signal(proc, sig1, sig2)
def communicate(self, proc):
return self.device.communicate(proc)
return proc.communicate()
def removeDir(self, dirname):
self.device.removeDir(dirname)
def getReturnCode(self, proc):
return self.device.getReturnCode(proc)
return proc.returncode
#TODO: consider creating a separate log dir. We don't have the test file structure,
# so we use filename.log. Would rather see ./logs/filename.log
def createLogFile(self, test, stdout):
def createLogFile(self, test, stdout, leakLogs):
try:
f = None
filename = test.replace('\\', '/').split('/')[-1] + ".log"
f = open(filename, "w")
f.write(stdout)
if os.path.exists(self.leakLogFile):
leaks = open(self.leakLogFile, "r")
for leakLog in leakLogs:
if os.path.exists(leakLog):
leaks = open(leakLog, "r")
f.write(leaks.read())
leaks.close()
finally:
if f <> None:
f.close()
#NOTE: the only difference between this and parent is the " vs ' arond the filename
def buildCmdHead(self, headfiles, tailfiles, xpcscmd):
cmdH = ", ".join(['\'' + f.replace('\\', '/') + '\''
for f in headfiles])
cmdT = ", ".join(['\'' + f.replace('\\', '/') + '\''
for f in tailfiles])
cmdH = xpcscmd + \
['-e', 'const _HEAD_FILES = [%s];' % cmdH] + \
['-e', 'const _TAIL_FILES = [%s];' % cmdT]
return cmdH
class RemoteXPCShellOptions(xpcshell.XPCShellOptions):
def __init__(self):
xpcshell.XPCShellOptions.__init__(self)
self.add_option("--device",
type="string", dest="device", default='',
help="ip address for the device")
def __init__(self):
xpcshell.XPCShellOptions.__init__(self)
defaults = {}
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("--dm_trans", action="store",
type = "string", dest = "dm_trans",
help = "the transport to use to communicate with device: [adb|sut]; default=sut")
defaults["dm_trans"] = "sut"
self.add_option("--objdir", action="store",
type = "string", dest = "objdir",
help = "local objdir, containing xpcshell binaries")
defaults["objdir"] = None
self.add_option("--apk", action="store",
type = "string", dest = "localAPK",
help = "local path to Fennec APK")
defaults["localAPK"] = None
self.add_option("--noSetup", action="store_false",
dest = "setup",
help = "do not copy any files to device (to be used only if device is already setup)")
defaults["setup"] = True
self.set_defaults(**defaults)
class PathMapping:
def __init__(self, localDir, remoteDir):
self.local = localDir
self.remote = remoteDir
def main():
parser = RemoteXPCShellOptions()
options, args = parser.parse_args()
dm_none = devicemanagerADB.DeviceManagerADB(None, None)
parser = RemoteXPCShellOptions()
options, args = parser.parse_args()
if len(args) < 2 and options.manifest is None or \
(len(args) < 1 and options.manifest is not None):
print "len(args): " + str(len(args))
print >>sys.stderr, """Usage: %s <path to xpcshell> <test dirs>
or: %s --manifest=test.manifest <path to xpcshell>""" % (sys.argv[0],
sys.argv[0])
sys.exit(1)
if len(args) < 1 and options.manifest is None:
print >>sys.stderr, """Usage: %s <test dirs>
or: %s --manifest=test.manifest """ % (sys.argv[0], sys.argv[0])
sys.exit(1)
if (options.device == ''):
print >>sys.stderr, "Error: Please provide an ip address for the remote device with the --device option"
sys.exit(1)
if (options.dm_trans == "adb"):
if (options.deviceIP):
dm = devicemanagerADB.DeviceManagerADB(options.deviceIP, options.devicePort)
else:
dm = dm_none
else:
dm = devicemanagerSUT.DeviceManagerSUT(options.deviceIP, options.devicePort)
if (options.deviceIP == None):
print "Error: you must provide a device IP to connect to via the --device option"
sys.exit(1)
if options.interactive and not options.testPath:
print >>sys.stderr, "Error: You must specify a test filename in interactive mode!"
sys.exit(1)
dm = devicemanager.DeviceManager(options.device, 20701)
xpcsh = XPCShellRemote(dm)
debuggerInfo = getDebuggerInfo(xpcsh.oldcwd, options.debugger, options.debuggerArgs,
options.debuggerInteractive);
if not options.objdir:
print >>sys.stderr, "Error: You must specify an objdir"
sys.exit(1)
if options.interactive and not options.testPath:
print >>sys.stderr, "Error: You must specify a test filename in interactive mode!"
sys.exit(1)
if not options.localAPK:
for file in os.listdir(os.path.join(options.objdir, "dist")):
if (file.endswith(".apk") and file.startswith("fennec")):
options.localAPK = os.path.join(options.objdir, "dist")
options.localAPK = os.path.join(options.localAPK, file)
print >>sys.stderr, "using APK: " + options.localAPK
break
if not options.localAPK:
print >>sys.stderr, "Error: please specify an APK"
sys.exit(1)
# Zip up the xpcshell directory: 7z a <zipName> xpcshell/*, assuming we are in the xpcshell directory
# TODO: ensure the system has 7z, this is adding a new dependency to the overall system
zipName = 'xpcshell.7z'
try:
Popen(['7z', 'a', zipName, '../xpcshell']).wait()
except:
print "to run these tests remotely, we require 7z to be installed and in your path"
sys.exit(1)
xpcsh = XPCShellRemote(dm, options, args)
if dm.pushFile(zipName, '/tests/xpcshell.7z') == None:
raise devicemanager.FileError("failed to copy xpcshell.7z to device")
if dm.unpackFile('xpcshell.7z') == None:
raise devicemanager.FileError("failed to unpack xpcshell.7z on the device")
if not xpcsh.runTests(xpcshell='xpcshell',
testdirs=args[0:],
**options.__dict__):
sys.exit(1)
if not xpcsh.runTests(args[0],
xrePath=options.xrePath,
symbolsPath=options.symbolsPath,
manifest=options.manifest,
testdirs=args[1:],
testPath=options.testPath,
interactive=options.interactive,
logfiles=options.logfiles,
debuggerInfo=debuggerInfo):
sys.exit(1)
if __name__ == '__main__':
main()

Просмотреть файл

@ -261,7 +261,7 @@ class XPCShellTests(object):
test['head'] is a whitespace delimited list of head files.
return the list of head files as paths including the subdir if the head file exists
On a remote system, this is overloaded to list files in a remote directory structure.
On a remote system, this may be overloaded to list files in a remote directory structure.
"""
return [os.path.join(test['here'], f).strip() for f in sorted(test['head'].split(' ')) if os.path.isfile(os.path.join(test['here'], f))]
@ -270,7 +270,7 @@ class XPCShellTests(object):
test['tail'] is a whitespace delimited list of head files.
return the list of tail files as paths including the subdir if the tail file exists
On a remote system, this is overloaded to list files in a remote directory structure.
On a remote system, this may be overloaded to list files in a remote directory structure.
"""
return [os.path.join(test['here'], f).strip() for f in sorted(test['tail'].split(' ')) if os.path.isfile(os.path.join(test['here'], f))]
@ -280,7 +280,7 @@ class XPCShellTests(object):
When running check-interactive and check-one, the directory is well-defined and
retained for inspection once the tests complete.
On a remote system, we overload this to use a remote path structure.
On a remote system, this may be overloaded to use a remote path structure.
"""
if self.interactive or self.singleFile:
profileDir = os.path.join(gettempdir(), self.profileName, "xpcshellprofile")
@ -301,7 +301,7 @@ class XPCShellTests(object):
"""
Enable leaks (only) detection to its own log file and set environment variables.
On a remote system, we overload this to use a remote filename and path structure
On a remote system, this may be overloaded to use a remote filename and path structure
"""
filename = "runxpcshelltests_leaks.log"
@ -381,12 +381,20 @@ class XPCShellTests(object):
'-e', 'const _HEAD_FILES = [%s];' % cmdH,
'-e', 'const _TAIL_FILES = [%s];' % cmdT]
def buildCmdTestFile(self, name):
"""
Build the command line arguments for the test file.
On a remote system, this may be overloaded to use a remote path structure.
"""
return ['-e', 'const _TEST_FILE = ["%s"];' %
replaceBackSlashes(name)]
def runTests(self, xpcshell, xrePath=None, appPath=None, symbolsPath=None,
manifest=None, testdirs=[], testPath=None,
interactive=False, verbose=False, keepGoing=False, logfiles=True,
thisChunk=1, totalChunks=1, debugger=None,
debuggerArgs=None, debuggerInteractive=False,
profileName=None, mozInfo=None):
profileName=None, mozInfo=None, **otherOptions):
"""Run xpcshell tests.
|xpcshell|, is the xpcshell executable to use to run the tests.
@ -410,6 +418,7 @@ class XPCShellTests(object):
|profileName|, if set, specifies the name of the application for the profile
directory if running only a subset of tests.
|mozInfo|, if set, specifies specifies build configuration information, either as a filename containing JSON, or a dict.
|otherOptions| may be present for the convenience of subclasses
"""
global gotSIGINT
@ -491,8 +500,7 @@ class XPCShellTests(object):
self.leakLogFile = self.setupLeakLogging()
# The test file will have to be loaded after the head files.
cmdT = ['-e', 'const _TEST_FILE = ["%s"];' %
replaceBackSlashes(name)]
cmdT = self.buildCmdTestFile(name)
try:
self.log.info("TEST-INFO | %s | running test ..." % name)
@ -515,8 +523,9 @@ class XPCShellTests(object):
def print_stdout(stdout):
"""Print stdout line-by-line to avoid overflowing buffers."""
self.log.info(">>>>>>>")
for line in stdout.splitlines():
self.log.info(line)
if (stdout):
for line in stdout.splitlines():
self.log.info(line)
self.log.info("<<<<<<<")
result = not ((self.getReturnCode(proc) != 0) or