diff options
Diffstat (limited to 'testing/xpcshell/remotexpcshelltests.py')
-rw-r--r-- | testing/xpcshell/remotexpcshelltests.py | 614 |
1 files changed, 614 insertions, 0 deletions
diff --git a/testing/xpcshell/remotexpcshelltests.py b/testing/xpcshell/remotexpcshelltests.py new file mode 100644 index 000000000..c5124880f --- /dev/null +++ b/testing/xpcshell/remotexpcshelltests.py @@ -0,0 +1,614 @@ +#!/usr/bin/env 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 posixpath +import sys, os +import subprocess +import runxpcshelltests as xpcshell +import tempfile +import time +from zipfile import ZipFile +from mozlog import commandline +import shutil +import mozdevice +import mozfile +import mozinfo + +from xpcshellcommandline import parser_remote + +here = os.path.dirname(os.path.abspath(__file__)) + +def remoteJoin(path1, path2): + return posixpath.join(path1, path2) + +class RemoteXPCShellTestThread(xpcshell.XPCShellTestThread): + def __init__(self, *args, **kwargs): + xpcshell.XPCShellTestThread.__init__(self, *args, **kwargs) + + self.shellReturnCode = None + # embed the mobile params from the harness into the TestThread + mobileArgs = kwargs.get('mobileArgs') + for key in mobileArgs: + setattr(self, key, mobileArgs[key]) + + def buildCmdTestFile(self, name): + remoteDir = self.remoteForLocal(os.path.dirname(name)) + if remoteDir == self.remoteHere: + remoteName = os.path.basename(name) + else: + remoteName = remoteJoin(remoteDir, os.path.basename(name)) + return ['-e', 'const _TEST_FILE = ["%s"];' % + remoteName.replace('\\', '/')] + + 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 setupTempDir(self): + # make sure the temp dir exists + self.clearRemoteDir(self.remoteTmpDir) + # env var is set in buildEnvironment + return self.remoteTmpDir + + def setupPluginsDir(self): + if not os.path.isdir(self.pluginsPath): + return None + + # making sure tmp dir is set up + self.setupTempDir() + + pluginsDir = remoteJoin(self.remoteTmpDir, "plugins") + self.device.pushDir(self.pluginsPath, pluginsDir) + if self.interactive: + self.log.info("plugins dir is %s" % pluginsDir) + return pluginsDir + + def setupProfileDir(self): + self.clearRemoteDir(self.profileDir) + if self.interactive or self.singleFile: + self.log.info("profile dir is %s" % self.profileDir) + return self.profileDir + + def setupMozinfoJS(self): + local = tempfile.mktemp() + mozinfo.output_to_file(local) + mozInfoJSPath = remoteJoin(self.profileDir, "mozinfo.json") + self.device.pushFile(local, mozInfoJSPath) + os.remove(local) + return mozInfoJSPath + + def logCommand(self, name, completeCmd, testdir): + self.log.info("%s | full command: %r" % (name, completeCmd)) + self.log.info("%s | current directory: %r" % (name, self.remoteHere)) + self.log.info("%s | environment: %s" % (name, self.env)) + + def getHeadAndTailFiles(self, test): + """Override parent method to find files on remote device. + + Obtains lists of head- and tail files. Returns a tuple containing + a list of head files and a list of tail files. + """ + def sanitize_list(s, kind): + for f in s.strip().split(' '): + f = f.strip() + if len(f) < 1: + continue + + path = remoteJoin(self.remoteHere, f) + + # skip check for file existence: the convenience of discovering + # a missing file does not justify the time cost of the round trip + # to the device + yield path + + self.remoteHere = self.remoteForLocal(test['here']) + + headlist = test.get('head', '') + taillist = test.get('tail', '') + return (list(sanitize_list(headlist, 'head')), + list(sanitize_list(taillist, 'tail'))) + + def buildXpcsCmd(self): + # change base class' paths to remote paths and use base class to build command + self.xpcshell = remoteJoin(self.remoteBinDir, "xpcw") + self.headJSPath = remoteJoin(self.remoteScriptsDir, 'head.js') + self.httpdJSPath = remoteJoin(self.remoteComponentsDir, 'httpd.js') + self.httpdManifest = remoteJoin(self.remoteComponentsDir, 'httpd.manifest') + self.testingModulesDir = self.remoteModulesDir + self.testharnessdir = self.remoteScriptsDir + xpcshell.XPCShellTestThread.buildXpcsCmd(self) + # remove "-g <dir> -a <dir>" and add "--greomni <apk>" + del(self.xpcsCmd[1:5]) + if self.options.localAPK: + self.xpcsCmd.insert(3, '--greomni') + self.xpcsCmd.insert(4, self.remoteAPK) + + if self.remoteDebugger: + # for example, "/data/local/gdbserver" "localhost:12345" + self.xpcsCmd = [ + self.remoteDebugger, + self.remoteDebuggerArgs, + self.xpcsCmd] + + def killTimeout(self, proc): + self.kill(proc) + + def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None): + self.timedout = False + cmd.insert(1, self.remoteHere) + outputFile = "xpcshelloutput" + with open(outputFile, 'w+') as f: + try: + self.shellReturnCode = self.device.shell(cmd, f, timeout=timeout+10) + except mozdevice.DMError as e: + if self.timedout: + # If the test timed out, there is a good chance the SUTagent also + # timed out and failed to return a return code, generating a + # DMError. Ignore the DMError to simplify the error report. + self.shellReturnCode = None + pass + else: + raise e + # The device manager may have timed out waiting for xpcshell. + # Guard against an accumulation of hung processes by killing + # them here. Note also that IPC tests may spawn new instances + # of xpcshell. + self.device.killProcess("xpcshell") + return outputFile + + def checkForCrashes(self, + dump_directory, + symbols_path, + test_name=None): + if not self.device.dirExists(self.remoteMinidumpDir): + # The minidumps directory is automatically created when Fennec + # (first) starts, so its lack of presence is a hint that + # something went wrong. + print "Automation Error: No crash directory (%s) found on remote device" % self.remoteMinidumpDir + # Whilst no crash was found, the run should still display as a failure + return True + with mozfile.TemporaryDirectory() as dumpDir: + self.device.getDirectory(self.remoteMinidumpDir, dumpDir) + crashed = xpcshell.XPCShellTestThread.checkForCrashes(self, dumpDir, symbols_path, test_name) + self.clearRemoteDir(self.remoteMinidumpDir) + return crashed + + def communicate(self, proc): + f = open(proc, "r") + contents = f.read() + f.close() + os.remove(proc) + return contents, "" + + def poll(self, proc): + if self.device.processExist("xpcshell") is None: + return self.getReturnCode(proc) + # Process is still running + return None + + def kill(self, proc): + return self.device.killProcess("xpcshell", True) + + def getReturnCode(self, proc): + if self.shellReturnCode is not None: + return self.shellReturnCode + else: + return -1 + + def removeDir(self, dirname): + self.device.removeDir(dirname) + + def clearRemoteDir(self, remoteDir): + out = "" + try: + out = self.device.shellCheckOutput([self.remoteClearDirScript, remoteDir]) + except mozdevice.DMError: + self.log.info("unable to delete %s: '%s'" % (remoteDir, str(out))) + self.log.info("retrying after 10 seconds...") + time.sleep(10) + try: + out = self.device.shellCheckOutput([self.remoteClearDirScript, remoteDir]) + except mozdevice.DMError: + self.log.error("failed to delete %s: '%s'" % (remoteDir, str(out))) + + #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): + try: + f = None + filename = test.replace('\\', '/').split('/')[-1] + ".log" + f = open(filename, "w") + f.write(stdout) + + finally: + if f is not None: + f.close() + + +# A specialization of XPCShellTests that runs tests on an Android device +# via devicemanager. +class XPCShellRemote(xpcshell.XPCShellTests, object): + + def __init__(self, devmgr, options, log): + xpcshell.XPCShellTests.__init__(self, log) + + # Add Android version (SDK level) to mozinfo so that manifest entries + # can be conditional on android_version. + androidVersion = devmgr.shellCheckOutput(['getprop', 'ro.build.version.sdk']) + mozinfo.info['android_version'] = androidVersion + + self.localLib = options.localLib + self.localBin = options.localBin + self.options = options + self.device = devmgr + self.pathMapping = [] + self.remoteTestRoot = "%s/xpc" % self.device.deviceRoot + # remoteBinDir contains xpcshell and its wrapper script, both of which must + # be executable. Since +x permissions cannot usually be set on /mnt/sdcard, + # and the test root may be on /mnt/sdcard, remoteBinDir is set to be on + # /data/local, always. + self.remoteBinDir = "/data/local/xpcb" + # Terse directory names are used here ("c" for the components 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.remoteTmpDir = remoteJoin(self.remoteTestRoot, "tmp") + self.remoteScriptsDir = self.remoteTestRoot + self.remoteComponentsDir = remoteJoin(self.remoteTestRoot, "c") + self.remoteModulesDir = remoteJoin(self.remoteTestRoot, "m") + self.remoteMinidumpDir = remoteJoin(self.remoteTestRoot, "minidumps") + self.remoteClearDirScript = remoteJoin(self.remoteBinDir, "cleardir") + self.profileDir = remoteJoin(self.remoteTestRoot, "p") + self.remoteDebugger = options.debugger + self.remoteDebuggerArgs = options.debuggerArgs + self.testingModulesDir = options.testingModulesDir + + self.env = {} + + if self.options.objdir: + self.xpcDir = os.path.join(self.options.objdir, "_tests/xpcshell") + elif os.path.isdir(os.path.join(here, 'tests')): + self.xpcDir = os.path.join(here, 'tests') + else: + print >> sys.stderr, "Couldn't find local xpcshell test directory" + sys.exit(1) + + if options.localAPK: + self.localAPKContents = ZipFile(options.localAPK) + if options.setup: + self.setupTestDir() + self.setupUtilities() + self.setupModules() + self.setupMinidumpDir() + self.remoteAPK = None + if options.localAPK: + self.remoteAPK = remoteJoin(self.remoteBinDir, os.path.basename(options.localAPK)) + self.setAppRoot() + + # data that needs to be passed to the RemoteXPCShellTestThread + self.mobileArgs = { + 'device': self.device, + 'remoteBinDir': self.remoteBinDir, + 'remoteScriptsDir': self.remoteScriptsDir, + 'remoteComponentsDir': self.remoteComponentsDir, + 'remoteModulesDir': self.remoteModulesDir, + 'options': self.options, + 'remoteDebugger': self.remoteDebugger, + 'pathMapping': self.pathMapping, + 'profileDir': self.profileDir, + 'remoteTmpDir': self.remoteTmpDir, + 'remoteMinidumpDir': self.remoteMinidumpDir, + 'remoteClearDirScript': self.remoteClearDirScript, + } + if self.remoteAPK: + self.mobileArgs['remoteAPK'] = self.remoteAPK + + def setLD_LIBRARY_PATH(self): + self.env["LD_LIBRARY_PATH"] = self.remoteBinDir + + def pushWrapper(self): + # Rather than executing xpcshell directly, this wrapper script is + # used. By setting environment variables and the cwd in the script, + # the length of the per-test command line is shortened. This is + # often important when using ADB, as there is a limit to the length + # of the ADB command line. + localWrapper = tempfile.mktemp() + f = open(localWrapper, "w") + f.write("#!/system/bin/sh\n") + for envkey, envval in self.env.iteritems(): + f.write("export %s=%s\n" % (envkey, envval)) + f.writelines([ + "cd $1\n", + "echo xpcw: cd $1\n", + "shift\n", + "echo xpcw: xpcshell \"$@\"\n", + "%s/xpcshell \"$@\"\n" % self.remoteBinDir]) + f.close() + remoteWrapper = remoteJoin(self.remoteBinDir, "xpcw") + self.device.pushFile(localWrapper, remoteWrapper) + os.remove(localWrapper) + + # Removing and re-creating a directory is a common operation which + # can be implemented more efficiently with a shell script. + localWrapper = tempfile.mktemp() + f = open(localWrapper, "w") + # The directory may not exist initially, so rm may fail. 'rm -f' is not + # supported on some Androids. Similarly, 'test' and 'if [ -d ]' are not + # universally available, so we just ignore errors from rm. + f.writelines([ + "#!/system/bin/sh\n", + "rm -r \"$1\"\n", + "mkdir \"$1\"\n"]) + f.close() + self.device.pushFile(localWrapper, self.remoteClearDirScript) + os.remove(localWrapper) + + self.device.chmodDir(self.remoteBinDir) + + def buildEnvironment(self): + self.buildCoreEnvironment() + self.setLD_LIBRARY_PATH() + self.env["MOZ_LINKER_CACHE"] = self.remoteBinDir + if self.options.localAPK and self.appRoot: + self.env["GRE_HOME"] = self.appRoot + self.env["XPCSHELL_TEST_PROFILE_DIR"] = self.profileDir + self.env["TMPDIR"] = self.remoteTmpDir + self.env["HOME"] = self.profileDir + self.env["XPCSHELL_TEST_TEMP_DIR"] = self.remoteTmpDir + self.env["XPCSHELL_MINIDUMP_DIR"] = self.remoteMinidumpDir + if self.options.setup: + self.pushWrapper() + + def setAppRoot(self): + # Determine the application root directory associated with the package + # name used by the Fennec APK. + self.appRoot = None + packageName = None + if self.options.localAPK: + try: + packageName = self.localAPKContents.read("package-name.txt") + if packageName: + self.appRoot = self.device.getAppRoot(packageName.strip()) + except Exception as detail: + print "unable to determine app root: " + str(detail) + pass + return None + + def setupUtilities(self): + if (not self.device.dirExists(self.remoteBinDir)): + # device.mkDir may fail here where shellCheckOutput may succeed -- see bug 817235 + try: + self.device.shellCheckOutput(["mkdir", self.remoteBinDir]); + except mozdevice.DMError: + # Might get a permission error; try again as root, if available + self.device.shellCheckOutput(["mkdir", self.remoteBinDir], root=True); + self.device.shellCheckOutput(["chmod", "777", self.remoteBinDir], root=True); + + remotePrefDir = remoteJoin(self.remoteBinDir, "defaults/pref") + if (self.device.dirExists(self.remoteTmpDir)): + self.device.removeDir(self.remoteTmpDir) + self.device.mkDir(self.remoteTmpDir) + if (not self.device.dirExists(remotePrefDir)): + self.device.mkDirs(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) + + local = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'head.js') + remoteFile = remoteJoin(self.remoteScriptsDir, "head.js") + self.device.pushFile(local, remoteFile) + + # The xpcshell binary is required for all tests. Additional binaries + # are required for some tests. This list should be similar to + # TEST_HARNESS_BINS in testing/mochitest/Makefile.in. + binaries = ["xpcshell", + "ssltunnel", + "certutil", + "pk12util", + "BadCertServer", + "OCSPStaplingServer", + "GenerateOCSPResponse"] + for fname in binaries: + local = os.path.join(self.localBin, fname) + if os.path.isfile(local): + print >> sys.stderr, "Pushing %s.." % fname + remoteFile = remoteJoin(self.remoteBinDir, fname) + self.device.pushFile(local, remoteFile) + else: + print >> sys.stderr, "*** Expected binary %s not found in %s!" % (fname, self.localBin) + + local = os.path.join(self.localBin, "components/httpd.js") + remoteFile = remoteJoin(self.remoteComponentsDir, "httpd.js") + self.device.pushFile(local, remoteFile) + + local = os.path.join(self.localBin, "components/httpd.manifest") + remoteFile = remoteJoin(self.remoteComponentsDir, "httpd.manifest") + self.device.pushFile(local, remoteFile) + + local = os.path.join(self.localBin, "components/test_necko.xpt") + remoteFile = remoteJoin(self.remoteComponentsDir, "test_necko.xpt") + self.device.pushFile(local, remoteFile) + + if self.options.localAPK: + remoteFile = remoteJoin(self.remoteBinDir, os.path.basename(self.options.localAPK)) + self.device.pushFile(self.options.localAPK, remoteFile) + + self.pushLibs() + + def pushLibs(self): + pushed_libs_count = 0 + if self.options.localAPK: + try: + dir = tempfile.mkdtemp() + for info in self.localAPKContents.infolist(): + if info.filename.endswith(".so"): + print >> sys.stderr, "Pushing %s.." % info.filename + remoteFile = remoteJoin(self.remoteBinDir, os.path.basename(info.filename)) + self.localAPKContents.extract(info, dir) + localFile = os.path.join(dir, info.filename) + with open(localFile) as f: + # Decompress xz-compressed file. + if f.read(5)[1:] == '7zXZ': + cmd = ['xz', '-df', '--suffix', '.so', localFile] + subprocess.check_output(cmd) + # xz strips the ".so" file suffix. + os.rename(localFile[:-3], localFile) + self.device.pushFile(localFile, remoteFile) + pushed_libs_count += 1 + finally: + shutil.rmtree(dir) + return pushed_libs_count + + for file in os.listdir(self.localLib): + if (file.endswith(".so")): + print >> sys.stderr, "Pushing %s.." % file + if 'libxul' in file: + print >> sys.stderr, "This is a big file, it could take a while." + localFile = os.path.join(self.localLib, file) + remoteFile = remoteJoin(self.remoteBinDir, file) + self.device.pushFile(localFile, remoteFile) + pushed_libs_count += 1 + + # Additional libraries may be found in a sub-directory such as "lib/armeabi-v7a" + localArmLib = os.path.join(self.localLib, "lib") + if os.path.exists(localArmLib): + for root, dirs, files in os.walk(localArmLib): + for file in files: + if (file.endswith(".so")): + print >> sys.stderr, "Pushing %s.." % file + localFile = os.path.join(root, file) + remoteFile = remoteJoin(self.remoteBinDir, file) + self.device.pushFile(localFile, remoteFile) + pushed_libs_count += 1 + + return pushed_libs_count + + def setupModules(self): + if self.testingModulesDir: + self.device.pushDir(self.testingModulesDir, self.remoteModulesDir) + + def setupTestDir(self): + print 'pushing %s' % self.xpcDir + try: + # The tests directory can be quite large: 5000 files and growing! + # Sometimes - like on a low-end aws instance running an emulator - the push + # may exceed the default 5 minute timeout, so we increase it here to 10 minutes. + self.device.pushDir(self.xpcDir, self.remoteScriptsDir, timeout=600, retryLimit=10) + except TypeError: + # Foopies have an older mozdevice ver without retryLimit + self.device.pushDir(self.xpcDir, self.remoteScriptsDir) + + def setupMinidumpDir(self): + if self.device.dirExists(self.remoteMinidumpDir): + self.device.removeDir(self.remoteMinidumpDir) + self.device.mkDir(self.remoteMinidumpDir) + + def buildTestList(self, test_tags=None, test_paths=None): + xpcshell.XPCShellTests.buildTestList(self, test_tags=test_tags, test_paths=test_paths) + uniqueTestPaths = set([]) + for test in self.alltests: + uniqueTestPaths.add(test['here']) + for testdir in uniqueTestPaths: + abbrevTestDir = os.path.relpath(testdir, self.xpcDir) + remoteScriptDir = remoteJoin(self.remoteScriptsDir, abbrevTestDir) + self.pathMapping.append(PathMapping(testdir, remoteScriptDir)) + +def verifyRemoteOptions(parser, options): + if options.localLib is None: + if options.localAPK and options.objdir: + for path in ['dist/fennec', 'fennec/lib']: + options.localLib = os.path.join(options.objdir, path) + if os.path.isdir(options.localLib): + break + else: + parser.error("Couldn't find local library dir, specify --local-lib-dir") + elif options.objdir: + options.localLib = os.path.join(options.objdir, 'dist/bin') + elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')): + # assume tests are being run from a tests.zip + options.localLib = os.path.abspath(os.path.join(here, '..', 'bin')) + else: + parser.error("Couldn't find local library dir, specify --local-lib-dir") + + if options.localBin is None: + if options.objdir: + for path in ['dist/bin', 'bin']: + options.localBin = os.path.join(options.objdir, path) + if os.path.isdir(options.localBin): + break + else: + parser.error("Couldn't find local binary dir, specify --local-bin-dir") + elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')): + # assume tests are being run from a tests.zip + options.localBin = os.path.abspath(os.path.join(here, '..', 'bin')) + else: + parser.error("Couldn't find local binary dir, specify --local-bin-dir") + return options + +class PathMapping: + + def __init__(self, localDir, remoteDir): + self.local = localDir + self.remote = remoteDir + +def main(): + if sys.version_info < (2,7): + print >>sys.stderr, "Error: You must use python version 2.7 or newer but less than 3.0" + sys.exit(1) + + parser = parser_remote() + options = parser.parse_args() + 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 + else: + print >>sys.stderr, "Error: please specify an APK" + sys.exit(1) + + options = verifyRemoteOptions(parser, options) + log = commandline.setup_logging("Remote XPCShell", + options, + {"tbpl": sys.stdout}) + + if options.dm_trans == "adb": + if options.deviceIP: + dm = mozdevice.DroidADB(options.deviceIP, options.devicePort, packageName=None, deviceRoot=options.remoteTestRoot) + else: + dm = mozdevice.DroidADB(packageName=None, deviceRoot=options.remoteTestRoot) + else: + if not options.deviceIP: + print "Error: you must provide a device IP to connect to via the --device option" + sys.exit(1) + dm = mozdevice.DroidSUT(options.deviceIP, options.devicePort, deviceRoot=options.remoteTestRoot) + + if options.interactive and not options.testPath: + print >>sys.stderr, "Error: You must specify a test filename in interactive mode!" + sys.exit(1) + + if options.xpcshell is None: + options.xpcshell = "xpcshell" + + xpcsh = XPCShellRemote(dm, options, log) + + # we don't run concurrent tests on mobile + options.sequential = True + + if not xpcsh.runTests(testClass=RemoteXPCShellTestThread, + mobileArgs=xpcsh.mobileArgs, + **vars(options)): + sys.exit(1) + + +if __name__ == '__main__': + main() |