summaryrefslogtreecommitdiffstats
path: root/layout/tools/reftest/remotereftest.py
diff options
context:
space:
mode:
Diffstat (limited to 'layout/tools/reftest/remotereftest.py')
-rw-r--r--layout/tools/reftest/remotereftest.py581
1 files changed, 581 insertions, 0 deletions
diff --git a/layout/tools/reftest/remotereftest.py b/layout/tools/reftest/remotereftest.py
new file mode 100644
index 0000000000..ac2f0bbc2c
--- /dev/null
+++ b/layout/tools/reftest/remotereftest.py
@@ -0,0 +1,581 @@
+# 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/.
+
+from __future__ import absolute_import, print_function
+
+import datetime
+import os
+import posixpath
+import shutil
+import signal
+import subprocess
+import sys
+import tempfile
+import time
+import traceback
+from contextlib import closing
+
+from six.moves.urllib_request import urlopen
+
+from mozdevice import ADBDeviceFactory, ADBTimeoutError, RemoteProcessMonitor
+import mozcrash
+
+from output import OutputHandler
+from runreftest import RefTest, ReftestResolver, build_obj
+import reftestcommandline
+
+# 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(__file__)))
+
+
+class RemoteReftestResolver(ReftestResolver):
+ def absManifestPath(self, path):
+ script_abs_path = os.path.join(SCRIPT_DIRECTORY, path)
+ if os.path.exists(script_abs_path):
+ rv = script_abs_path
+ elif os.path.exists(os.path.abspath(path)):
+ rv = os.path.abspath(path)
+ else:
+ print("Could not find manifest %s" % script_abs_path, file=sys.stderr)
+ sys.exit(1)
+ return os.path.normpath(rv)
+
+ def manifestURL(self, options, path):
+ # Dynamically build the reftest URL if possible, beware that
+ # args[0] should exist 'inside' webroot. It's possible for
+ # this url to have a leading "..", but reftest.js will fix
+ # that. Use the httpdPath to determine if we are running in
+ # production or locally. If we are running the jsreftests
+ # locally, strip text up to jsreftest. We want the docroot of
+ # the server to include a link jsreftest that points to the
+ # test-stage location of the test files. The desktop oriented
+ # setup has already created a link for tests which points
+ # directly into the source tree. For the remote tests we need
+ # a separate symbolic link to point to the staged test files.
+ if "jsreftest" not in path or os.environ.get("MOZ_AUTOMATION"):
+ relPath = os.path.relpath(path, SCRIPT_DIRECTORY)
+ else:
+ relPath = "jsreftest/" + path.split("jsreftest/")[-1]
+ return "http://%s:%s/%s" % (options.remoteWebServer, options.httpPort, relPath)
+
+
+class ReftestServer:
+ """Web server used to serve Reftests, for closer fidelity to the real web.
+ It is virtually identical to the server used in mochitest and will only
+ be used for running reftests remotely.
+ Bug 581257 has been filed to refactor this wrapper around httpd.js into
+ it's own class and use it in both remote and non-remote testing."""
+
+ def __init__(self, options, scriptDir, log):
+ self.log = log
+ self.utilityPath = options.utilityPath
+ self.xrePath = options.xrePath
+ self.profileDir = options.serverProfilePath
+ self.webServer = options.remoteWebServer
+ self.httpPort = options.httpPort
+ self.scriptDir = scriptDir
+ self.httpdPath = os.path.abspath(options.httpdPath)
+ if options.remoteWebServer == "10.0.2.2":
+ # probably running an Android emulator and 10.0.2.2 will
+ # not be visible from host
+ shutdownServer = "127.0.0.1"
+ else:
+ shutdownServer = self.webServer
+ self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % {
+ "server": shutdownServer,
+ "port": self.httpPort,
+ }
+
+ def start(self):
+ "Run the Refest server, returning the process ID of the server."
+
+ env = dict(os.environ)
+ env["XPCOM_DEBUG_BREAK"] = "warn"
+ bin_suffix = ""
+ if sys.platform in ("win32", "msys", "cygwin"):
+ env["PATH"] = env["PATH"] + ";" + self.xrePath
+ bin_suffix = ".exe"
+ else:
+ if "LD_LIBRARY_PATH" not in env or env["LD_LIBRARY_PATH"] is None:
+ env["LD_LIBRARY_PATH"] = self.xrePath
+ else:
+ env["LD_LIBRARY_PATH"] = ":".join(
+ [self.xrePath, env["LD_LIBRARY_PATH"]]
+ )
+
+ args = [
+ "-g",
+ self.xrePath,
+ "-f",
+ os.path.join(self.httpdPath, "httpd.js"),
+ "-e",
+ "const _PROFILE_PATH = '%(profile)s';const _SERVER_PORT = "
+ "'%(port)s'; const _SERVER_ADDR ='%(server)s';"
+ % {
+ "profile": self.profileDir.replace("\\", "\\\\"),
+ "port": self.httpPort,
+ "server": self.webServer,
+ },
+ "-f",
+ os.path.join(self.scriptDir, "server.js"),
+ ]
+
+ xpcshell = os.path.join(self.utilityPath, "xpcshell" + bin_suffix)
+
+ if not os.access(xpcshell, os.F_OK):
+ raise Exception("xpcshell not found at %s" % xpcshell)
+ if RemoteProcessMonitor.elf_arm(xpcshell):
+ raise Exception(
+ "xpcshell at %s is an ARM binary; please use "
+ "the --utility-path argument to specify the path "
+ "to a desktop version." % xpcshell
+ )
+
+ self._process = subprocess.Popen([xpcshell] + args, env=env)
+ pid = self._process.pid
+ if pid < 0:
+ self.log.error(
+ "TEST-UNEXPECTED-FAIL | remotereftests.py | Error starting server."
+ )
+ return 2
+ self.log.info("INFO | remotereftests.py | Server pid: %d" % pid)
+
+ def ensureReady(self, timeout):
+ assert timeout >= 0
+
+ aliveFile = os.path.join(self.profileDir, "server_alive.txt")
+ i = 0
+ while i < timeout:
+ if os.path.exists(aliveFile):
+ break
+ time.sleep(1)
+ i += 1
+ else:
+ self.log.error(
+ "TEST-UNEXPECTED-FAIL | remotereftests.py | "
+ "Timed out while waiting for server startup."
+ )
+ self.stop()
+ return 1
+
+ def stop(self):
+ if hasattr(self, "_process"):
+ try:
+ with closing(urlopen(self.shutdownURL)) as c:
+ c.read()
+
+ rtncode = self._process.poll()
+ if rtncode is None:
+ self._process.terminate()
+ except Exception:
+ self.log.info("Failed to shutdown server at %s" % self.shutdownURL)
+ traceback.print_exc()
+ self._process.kill()
+
+
+class RemoteReftest(RefTest):
+ use_marionette = False
+ resolver_cls = RemoteReftestResolver
+
+ def __init__(self, options, scriptDir):
+ RefTest.__init__(self, options.suite)
+ self.run_by_manifest = False
+ self.scriptDir = scriptDir
+ self.localLogName = options.localLogName
+
+ verbose = False
+ if (
+ options.log_mach_verbose
+ or options.log_tbpl_level == "debug"
+ or options.log_mach_level == "debug"
+ or options.log_raw_level == "debug"
+ ):
+ verbose = True
+ print("set verbose!")
+ expected = options.app.split("/")[-1]
+ self.device = ADBDeviceFactory(
+ adb=options.adb_path or "adb",
+ device=options.deviceSerial,
+ test_root=options.remoteTestRoot,
+ verbose=verbose,
+ run_as_package=expected,
+ )
+ if options.remoteTestRoot is None:
+ options.remoteTestRoot = posixpath.join(self.device.test_root, "reftest")
+ options.remoteProfile = posixpath.join(options.remoteTestRoot, "profile")
+ options.remoteLogFile = posixpath.join(options.remoteTestRoot, "reftest.log")
+ options.logFile = options.remoteLogFile
+ self.remoteProfile = options.remoteProfile
+ self.remoteTestRoot = options.remoteTestRoot
+
+ if not options.ignoreWindowSize:
+ parts = self.device.get_info("screen")["screen"][0].split()
+ width = int(parts[0].split(":")[1])
+ height = int(parts[1].split(":")[1])
+ if width < 1366 or height < 1050:
+ self.error(
+ "ERROR: Invalid screen resolution %sx%s, "
+ "please adjust to 1366x1050 or higher" % (width, height)
+ )
+
+ self._populate_logger(options)
+ self.outputHandler = OutputHandler(
+ self.log, options.utilityPath, options.symbolsPath
+ )
+
+ self.SERVER_STARTUP_TIMEOUT = 90
+
+ self.remoteCache = os.path.join(options.remoteTestRoot, "cache/")
+
+ # Check that Firefox is installed
+ expected = options.app.split("/")[-1]
+ if not self.device.is_app_installed(expected):
+ raise Exception("%s is not installed on this device" % expected)
+ self.device.run_as_package = expected
+ self.device.clear_logcat()
+
+ self.device.rm(self.remoteCache, force=True, recursive=True)
+
+ procName = options.app.split("/")[-1]
+ self.device.stop_application(procName)
+ if self.device.process_exist(procName):
+ self.log.error("unable to kill %s before starting tests!" % procName)
+
+ 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
+ remoteUtilityPath = options.utilityPath
+
+ paths = [options.xrePath]
+ if build_obj:
+ paths.append(os.path.join(build_obj.topobjdir, "dist", "bin"))
+ options.xrePath = self.findPath(paths)
+ if options.xrePath is None:
+ print(
+ "ERROR: unable to find xulrunner path for %s, "
+ "please specify with --xre-path" % (os.name)
+ )
+ return 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 is None:
+ print(
+ "ERROR: unable to find utility path for %s, "
+ "please specify with --utility-path" % (os.name)
+ )
+ return 1
+
+ options.serverProfilePath = tempfile.mkdtemp()
+ self.server = ReftestServer(options, self.scriptDir, self.log)
+ retVal = self.server.start()
+ if retVal:
+ return retVal
+ retVal = self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
+ if retVal:
+ return retVal
+
+ options.xrePath = remoteXrePath
+ options.utilityPath = remoteUtilityPath
+ return 0
+
+ def stopWebServer(self, options):
+ self.server.stop()
+
+ def killNamedProc(self, pname, orphans=True):
+ """ Kill processes matching the given command name """
+ try:
+ import psutil
+ except ImportError as e:
+ self.log.warning("Unable to import psutil: %s" % str(e))
+ self.log.warning("Unable to verify that %s is not already running." % pname)
+ return
+
+ self.log.info("Checking for %s processes..." % pname)
+
+ for proc in psutil.process_iter():
+ try:
+ if proc.name() == pname:
+ procd = proc.as_dict(attrs=["pid", "ppid", "name", "username"])
+ if proc.ppid() == 1 or not orphans:
+ self.log.info("killing %s" % procd)
+ try:
+ os.kill(
+ proc.pid, getattr(signal, "SIGKILL", signal.SIGTERM)
+ )
+ except Exception as e:
+ self.log.info(
+ "Failed to kill process %d: %s" % (proc.pid, str(e))
+ )
+ else:
+ self.log.info("NOT killing %s (not an orphan?)" % procd)
+ except Exception:
+ # may not be able to access process info for all processes
+ continue
+
+ def createReftestProfile(self, options, **kwargs):
+ profile = RefTest.createReftestProfile(
+ self,
+ options,
+ server=options.remoteWebServer,
+ port=options.httpPort,
+ **kwargs
+ )
+ profileDir = profile.profile
+ prefs = {}
+ prefs["app.update.url.android"] = ""
+ prefs["reftest.remote"] = True
+ prefs["datareporting.policy.dataSubmissionPolicyBypassAcceptance"] = True
+ # move necko cache to a location that can be cleaned up
+ prefs["browser.cache.disk.parent_directory"] = self.remoteCache
+
+ prefs["layout.css.devPixelsPerPx"] = "1.0"
+ # Because Fennec is a little wacky (see bug 1156817) we need to load the
+ # reftest pages at 1.0 zoom, rather than zooming to fit the CSS viewport.
+ prefs["apz.allow_zooming"] = False
+
+ # Set the extra prefs.
+ profile.set_preferences(prefs)
+
+ try:
+ self.device.push(profileDir, options.remoteProfile)
+ # make sure the parent directories of the profile which
+ # may have been created by the push, also have their
+ # permissions set to allow access.
+ self.device.chmod(options.remoteTestRoot, recursive=True)
+ except Exception:
+ print("Automation Error: Failed to copy profiledir to device")
+ raise
+
+ return profile
+
+ def printDeviceInfo(self, printLogcat=False):
+ try:
+ if printLogcat:
+ logcat = self.device.get_logcat()
+ for l in logcat:
+ ul = l.decode("utf-8", errors="replace")
+ sl = ul.encode("iso8859-1", errors="replace")
+ print("%s\n" % sl)
+ print("Device info:")
+ devinfo = self.device.get_info()
+ for category in devinfo:
+ if type(devinfo[category]) is list:
+ print(" %s:" % category)
+ for item in devinfo[category]:
+ print(" %s" % item)
+ else:
+ print(" %s: %s" % (category, devinfo[category]))
+ print("Test root: %s" % self.device.test_root)
+ except ADBTimeoutError:
+ raise
+ except Exception as e:
+ print("WARNING: Error getting device information: %s" % str(e))
+
+ def environment(self, env=None, crashreporter=True, **kwargs):
+ # Since running remote, do not mimic the local env: do not copy os.environ
+ if env is None:
+ env = {}
+
+ if crashreporter:
+ env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
+ env["MOZ_CRASHREPORTER"] = "1"
+ env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
+ else:
+ env["MOZ_CRASHREPORTER_DISABLE"] = "1"
+
+ # Crash on non-local network connections by default.
+ # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
+ # enable non-local connections for the purposes of local testing.
+ # Don't override the user's choice here. See bug 1049688.
+ env.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1")
+
+ # Send an env var noting that we are in automation. Passing any
+ # value except the empty string will declare the value to exist.
+ #
+ # This may be used to disabled network connections during testing, e.g.
+ # Switchboard & telemetry uploads.
+ env.setdefault("MOZ_IN_AUTOMATION", "1")
+
+ # Set WebRTC logging in case it is not set yet.
+ env.setdefault("R_LOG_LEVEL", "6")
+ env.setdefault("R_LOG_DESTINATION", "stderr")
+ env.setdefault("R_LOG_VERBOSE", "1")
+
+ return env
+
+ def buildBrowserEnv(self, options, profileDir):
+ browserEnv = RefTest.buildBrowserEnv(self, options, profileDir)
+ # remove desktop environment not used on device
+ if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
+ del browserEnv["XPCOM_MEM_BLOAT_LOG"]
+ return browserEnv
+
+ def runApp(
+ self,
+ options,
+ cmdargs=None,
+ timeout=None,
+ debuggerInfo=None,
+ symbolsPath=None,
+ valgrindPath=None,
+ valgrindArgs=None,
+ valgrindSuppFiles=None,
+ **profileArgs
+ ):
+ if cmdargs is None:
+ cmdargs = []
+
+ if self.use_marionette:
+ cmdargs.append("-marionette")
+
+ binary = options.app
+ profile = self.createReftestProfile(options, **profileArgs)
+
+ # browser environment
+ env = self.buildBrowserEnv(options, profile.profile)
+
+ self.log.info("Running with e10s: {}".format(options.e10s))
+ self.log.info("Running with fission: {}".format(options.fission))
+
+ rpm = RemoteProcessMonitor(
+ binary,
+ self.device,
+ self.log,
+ self.outputHandler,
+ options.remoteLogFile,
+ self.remoteProfile,
+ )
+ startTime = datetime.datetime.now()
+ status = 0
+ profileDirectory = self.remoteProfile + "/"
+ cmdargs.extend(("-no-remote", "-profile", profileDirectory))
+
+ pid = rpm.launch(
+ binary,
+ debuggerInfo,
+ None,
+ cmdargs,
+ env=env,
+ e10s=options.e10s,
+ )
+ self.log.info("remotereftest.py | Application pid: %d" % pid)
+ if not rpm.wait(timeout):
+ status = 1
+ self.log.info(
+ "remotereftest.py | Application ran for: %s"
+ % str(datetime.datetime.now() - startTime)
+ )
+ crashed = self.check_for_crashes(symbolsPath, rpm.last_test_seen)
+ if crashed:
+ status = 1
+
+ self.cleanup(profile.profile)
+ return status
+
+ def check_for_crashes(self, symbols_path, last_test_seen):
+ """
+ Pull any minidumps from remote profile and log any associated crashes.
+ """
+ try:
+ dump_dir = tempfile.mkdtemp()
+ remote_crash_dir = posixpath.join(self.remoteProfile, "minidumps")
+ if not self.device.is_dir(remote_crash_dir):
+ return False
+ self.device.pull(remote_crash_dir, dump_dir)
+ crashed = mozcrash.log_crashes(
+ self.log, dump_dir, symbols_path, test=last_test_seen
+ )
+ finally:
+ try:
+ shutil.rmtree(dump_dir)
+ except Exception as e:
+ self.log.warning(
+ "unable to remove directory %s: %s" % (dump_dir, str(e))
+ )
+ return crashed
+
+ def cleanup(self, profileDir):
+ self.device.rm(self.remoteTestRoot, force=True, recursive=True)
+ self.device.rm(self.remoteProfile, force=True, recursive=True)
+ self.device.rm(self.remoteCache, force=True, recursive=True)
+ RefTest.cleanup(self, profileDir)
+
+
+def run_test_harness(parser, options):
+ reftest = RemoteReftest(options, SCRIPT_DIRECTORY)
+ parser.validate_remote(options)
+ parser.validate(options, reftest)
+
+ # Hack in a symbolic link for jsreftest in the SCRIPT_DIRECTORY
+ # which is the document root for the reftest web server. This
+ # allows a separate redirection for the jsreftests which must
+ # run through the web server using the staged tests files and
+ # the desktop which will use the tests symbolic link to find
+ # the JavaScript tests.
+ jsreftest_target = str(os.path.join(SCRIPT_DIRECTORY, "jsreftest"))
+ if os.environ.get("MOZ_AUTOMATION"):
+ os.system("ln -s ../jsreftest " + jsreftest_target)
+ else:
+ jsreftest_source = os.path.join(
+ build_obj.topobjdir, "dist", "test-stage", "jsreftest"
+ )
+ if not os.path.islink(jsreftest_target):
+ os.symlink(jsreftest_source, jsreftest_target)
+
+ # Despite our efforts to clean up servers started by this script, in practice
+ # we still see infrequent cases where a process is orphaned and interferes
+ # with future tests, typically because the old server is keeping the port in use.
+ # Try to avoid those failures by checking for and killing servers before
+ # trying to start new ones.
+ reftest.killNamedProc("ssltunnel")
+ reftest.killNamedProc("xpcshell")
+
+ # Start the webserver
+ retVal = reftest.startWebServer(options)
+ if retVal:
+ return retVal
+
+ if options.printDeviceInfo and not options.verify:
+ reftest.printDeviceInfo()
+
+ retVal = 0
+ try:
+ if options.verify:
+ retVal = reftest.verifyTests(options.tests, options)
+ else:
+ retVal = reftest.runTests(options.tests, options)
+ except Exception:
+ print("Automation Error: Exception caught while running tests")
+ traceback.print_exc()
+ retVal = 1
+
+ reftest.stopWebServer(options)
+
+ if options.printDeviceInfo and not options.verify:
+ reftest.printDeviceInfo(printLogcat=(retVal != 0))
+
+ return retVal
+
+
+if __name__ == "__main__":
+ parser = reftestcommandline.RemoteArgumentsParser()
+ options = parser.parse_args()
+ sys.exit(run_test_harness(parser, options))