layout/tools/reftest/remotereftest.py

Fri, 16 Jan 2015 04:50:19 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 04:50:19 +0100
branch
TOR_BUG_9701
changeset 13
44a2da4a2ab2
permissions
-rw-r--r--

Replace accessor implementation with direct member state manipulation, by
request https://trac.torproject.org/projects/tor/ticket/9701#comment:32

     1 # This Source Code Form is subject to the terms of the Mozilla Public
     2 # License, v. 2.0. If a copy of the MPL was not distributed with this
     3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     5 import sys
     6 import os
     7 import time
     8 import tempfile
     9 import traceback
    11 # We need to know our current directory so that we can serve our test files from it.
    12 SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
    14 from runreftest import RefTest
    15 from runreftest import ReftestOptions
    16 from automation import Automation
    17 import devicemanager
    18 import droid
    19 import moznetwork
    20 from remoteautomation import RemoteAutomation, fennecLogcatFilters
    22 class RemoteOptions(ReftestOptions):
    23     def __init__(self, automation):
    24         ReftestOptions.__init__(self, automation)
    26         defaults = {}
    27         defaults["logFile"] = "reftest.log"
    28         # app, xrePath and utilityPath variables are set in main function
    29         defaults["app"] = ""
    30         defaults["xrePath"] = ""
    31         defaults["utilityPath"] = ""
    32         defaults["runTestsInParallel"] = False
    34         self.add_option("--remote-app-path", action="store",
    35                     type = "string", dest = "remoteAppPath",
    36                     help = "Path to remote executable relative to device root using only forward slashes.  Either this or app must be specified, but not both.")
    37         defaults["remoteAppPath"] = None
    39         self.add_option("--deviceIP", action="store",
    40                     type = "string", dest = "deviceIP",
    41                     help = "ip address of remote device to test")
    42         defaults["deviceIP"] = None
    44         self.add_option("--devicePort", action="store",
    45                     type = "string", dest = "devicePort",
    46                     help = "port of remote device to test")
    47         defaults["devicePort"] = 20701
    49         self.add_option("--remote-product-name", action="store",
    50                     type = "string", dest = "remoteProductName",
    51                     help = "Name of product to test - either fennec or firefox, defaults to fennec")
    52         defaults["remoteProductName"] = "fennec"
    54         self.add_option("--remote-webserver", action="store",
    55                     type = "string", dest = "remoteWebServer",
    56                     help = "IP Address of the webserver hosting the reftest content")
    57         defaults["remoteWebServer"] = moznetwork.get_ip()
    59         self.add_option("--http-port", action = "store",
    60                     type = "string", dest = "httpPort",
    61                     help = "port of the web server for http traffic")
    62         defaults["httpPort"] = automation.DEFAULT_HTTP_PORT
    64         self.add_option("--ssl-port", action = "store",
    65                     type = "string", dest = "sslPort",
    66                     help = "Port for https traffic to the web server")
    67         defaults["sslPort"] = automation.DEFAULT_SSL_PORT
    69         self.add_option("--remote-logfile", action="store",
    70                     type = "string", dest = "remoteLogFile",
    71                     help = "Name of log file on the device relative to device root.  PLEASE USE ONLY A FILENAME.")
    72         defaults["remoteLogFile"] = None
    74         self.add_option("--enable-privilege", action="store_true", dest = "enablePrivilege",
    75                     help = "add webserver and port to the user.js file for remote script access and universalXPConnect")
    76         defaults["enablePrivilege"] = False
    78         self.add_option("--pidfile", action = "store",
    79                     type = "string", dest = "pidFile",
    80                     help = "name of the pidfile to generate")
    81         defaults["pidFile"] = ""
    83         self.add_option("--bootstrap", action="store_true", dest = "bootstrap",
    84                     help = "test with a bootstrap addon required for native Fennec")
    85         defaults["bootstrap"] = False
    87         self.add_option("--dm_trans", action="store",
    88                     type = "string", dest = "dm_trans",
    89                     help = "the transport to use to communicate with device: [adb|sut]; default=sut")
    90         defaults["dm_trans"] = "sut"
    92         self.add_option("--remoteTestRoot", action = "store",
    93                     type = "string", dest = "remoteTestRoot",
    94                     help = "remote directory to use as test root (eg. /mnt/sdcard/tests or /data/local/tests)")
    95         defaults["remoteTestRoot"] = None
    97         self.add_option("--httpd-path", action = "store",
    98                     type = "string", dest = "httpdPath",
    99                     help = "path to the httpd.js file")
   100         defaults["httpdPath"] = None
   102         defaults["localLogName"] = None
   104         self.set_defaults(**defaults)
   106     def verifyRemoteOptions(self, options):
   107         if options.runTestsInParallel:
   108             self.error("Cannot run parallel tests here")
   110         # Ensure our defaults are set properly for everything we can infer
   111         if not options.remoteTestRoot:
   112             options.remoteTestRoot = self.automation._devicemanager.getDeviceRoot() + '/reftest'
   113         options.remoteProfile = options.remoteTestRoot + "/profile"
   115         # Verify that our remotewebserver is set properly
   116         if (options.remoteWebServer == None or
   117             options.remoteWebServer == '127.0.0.1'):
   118             print "ERROR: Either you specified the loopback for the remote webserver or ",
   119             print "your local IP cannot be detected.  Please provide the local ip in --remote-webserver"
   120             return None
   122         # One of remoteAppPath (relative path to application) or the app (executable) must be
   123         # set, but not both.  If both are set, we destroy the user's selection for app
   124         # so instead of silently destroying a user specificied setting, we error.
   125         if (options.remoteAppPath and options.app):
   126             print "ERROR: You cannot specify both the remoteAppPath and the app"
   127             return None
   128         elif (options.remoteAppPath):
   129             options.app = options.remoteTestRoot + "/" + options.remoteAppPath
   130         elif (options.app == None):
   131             # Neither remoteAppPath nor app are set -- error
   132             print "ERROR: You must specify either appPath or app"
   133             return None
   135         if (options.xrePath == None):
   136             print "ERROR: You must specify the path to the controller xre directory"
   137             return None
   138         else:
   139             # Ensure xrepath is a full path
   140             options.xrePath = os.path.abspath(options.xrePath)
   142         # Default to <deviceroot>/reftest/reftest.log
   143         if (options.remoteLogFile == None):
   144             options.remoteLogFile = 'reftest.log'
   146         options.localLogName = options.remoteLogFile
   147         options.remoteLogFile = options.remoteTestRoot + '/' + options.remoteLogFile
   149         # Ensure that the options.logfile (which the base class uses) is set to
   150         # the remote setting when running remote. Also, if the user set the
   151         # log file name there, use that instead of reusing the remotelogfile as above.
   152         if (options.logFile):
   153             # If the user specified a local logfile name use that
   154             options.localLogName = options.logFile
   156         options.logFile = options.remoteLogFile
   158         if (options.pidFile != ""):
   159             f = open(options.pidFile, 'w')
   160             f.write("%s" % os.getpid())
   161             f.close()
   163         # httpd-path is specified by standard makefile targets and may be specified
   164         # on the command line to select a particular version of httpd.js. If not
   165         # specified, try to select the one from hostutils.zip, as required in bug 882932.
   166         if not options.httpdPath:
   167             options.httpdPath = os.path.join(options.utilityPath, "components")
   169         # TODO: Copied from main, but I think these are no longer used in a post xulrunner world
   170         #options.xrePath = options.remoteTestRoot + self.automation._product + '/xulrunner'
   171         #options.utilityPath = options.testRoot + self.automation._product + '/bin'
   172         return options
   174 class ReftestServer:
   175     """ Web server used to serve Reftests, for closer fidelity to the real web.
   176         It is virtually identical to the server used in mochitest and will only
   177         be used for running reftests remotely.
   178         Bug 581257 has been filed to refactor this wrapper around httpd.js into
   179         it's own class and use it in both remote and non-remote testing. """
   181     def __init__(self, automation, options, scriptDir):
   182         self.automation = automation
   183         self._utilityPath = options.utilityPath
   184         self._xrePath = options.xrePath
   185         self._profileDir = options.serverProfilePath
   186         self.webServer = options.remoteWebServer
   187         self.httpPort = options.httpPort
   188         self.scriptDir = scriptDir
   189         self.pidFile = options.pidFile
   190         self._httpdPath = os.path.abspath(options.httpdPath)
   191         self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { "server" : self.webServer, "port" : self.httpPort }
   193     def start(self):
   194         "Run the Refest server, returning the process ID of the server."
   196         env = self.automation.environment(xrePath = self._xrePath)
   197         env["XPCOM_DEBUG_BREAK"] = "warn"
   198         if self.automation.IS_WIN32:
   199             env["PATH"] = env["PATH"] + ";" + self._xrePath
   201         args = ["-g", self._xrePath,
   202                 "-v", "170",
   203                 "-f", os.path.join(self._httpdPath, "httpd.js"),
   204                 "-e", "const _PROFILE_PATH = '%(profile)s';const _SERVER_PORT = '%(port)s'; const _SERVER_ADDR ='%(server)s';" %
   205                        {"profile" : self._profileDir.replace('\\', '\\\\'), "port" : self.httpPort, "server" : self.webServer },
   206                 "-f", os.path.join(self.scriptDir, "server.js")]
   208         xpcshell = os.path.join(self._utilityPath,
   209                                 "xpcshell" + self.automation.BIN_SUFFIX)
   211         if not os.access(xpcshell, os.F_OK):
   212             raise Exception('xpcshell not found at %s' % xpcshell)
   213         if self.automation.elf_arm(xpcshell):
   214             raise Exception('xpcshell at %s is an ARM binary; please use '
   215                             'the --utility-path argument to specify the path '
   216                             'to a desktop version.' % xpcshell)
   218         self._process = self.automation.Process([xpcshell] + args, env = env)
   219         pid = self._process.pid
   220         if pid < 0:
   221             print "TEST-UNEXPECTED-FAIL | remotereftests.py | Error starting server."
   222             return 2
   223         self.automation.log.info("INFO | remotereftests.py | Server pid: %d", pid)
   225         if (self.pidFile != ""):
   226             f = open(self.pidFile + ".xpcshell.pid", 'w')
   227             f.write("%s" % pid)
   228             f.close()
   230     def ensureReady(self, timeout):
   231         assert timeout >= 0
   233         aliveFile = os.path.join(self._profileDir, "server_alive.txt")
   234         i = 0
   235         while i < timeout:
   236             if os.path.exists(aliveFile):
   237                 break
   238             time.sleep(1)
   239             i += 1
   240         else:
   241             print "TEST-UNEXPECTED-FAIL | remotereftests.py | Timed out while waiting for server startup."
   242             self.stop()
   243             return 1
   245     def stop(self):
   246         if hasattr(self, '_process'):
   247             try:
   248                 c = urllib2.urlopen(self.shutdownURL)
   249                 c.read()
   250                 c.close()
   252                 rtncode = self._process.poll()
   253                 if (rtncode == None):
   254                     self._process.terminate()
   255             except:
   256                 self._process.kill()
   258 class RemoteReftest(RefTest):
   259     remoteApp = ''
   261     def __init__(self, automation, devicemanager, options, scriptDir):
   262         RefTest.__init__(self, automation)
   263         self._devicemanager = devicemanager
   264         self.scriptDir = scriptDir
   265         self.remoteApp = options.app
   266         self.remoteProfile = options.remoteProfile
   267         self.remoteTestRoot = options.remoteTestRoot
   268         self.remoteLogFile = options.remoteLogFile
   269         self.localLogName = options.localLogName
   270         self.pidFile = options.pidFile
   271         if self.automation.IS_DEBUG_BUILD:
   272             self.SERVER_STARTUP_TIMEOUT = 180
   273         else:
   274             self.SERVER_STARTUP_TIMEOUT = 90
   275         self.automation.deleteANRs()
   277     def findPath(self, paths, filename = None):
   278         for path in paths:
   279             p = path
   280             if filename:
   281                 p = os.path.join(p, filename)
   282             if os.path.exists(self.getFullPath(p)):
   283                 return path
   284         return None
   286     def startWebServer(self, options):
   287         """ Create the webserver on the host and start it up """
   288         remoteXrePath = options.xrePath
   289         remoteUtilityPath = options.utilityPath
   290         localAutomation = Automation()
   291         localAutomation.IS_WIN32 = False
   292         localAutomation.IS_LINUX = False
   293         localAutomation.IS_MAC = False
   294         localAutomation.UNIXISH = False
   295         hostos = sys.platform
   296         if (hostos == 'mac' or  hostos == 'darwin'):
   297             localAutomation.IS_MAC = True
   298         elif (hostos == 'linux' or hostos == 'linux2'):
   299             localAutomation.IS_LINUX = True
   300             localAutomation.UNIXISH = True
   301         elif (hostos == 'win32' or hostos == 'win64'):
   302             localAutomation.BIN_SUFFIX = ".exe"
   303             localAutomation.IS_WIN32 = True
   305         paths = [options.xrePath, localAutomation.DIST_BIN, self.automation._product, os.path.join('..', self.automation._product)]
   306         options.xrePath = self.findPath(paths)
   307         if options.xrePath == None:
   308             print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name)
   309             return 1
   310         paths.append("bin")
   311         paths.append(os.path.join("..", "bin"))
   313         xpcshell = "xpcshell"
   314         if (os.name == "nt"):
   315             xpcshell += ".exe"
   317         if (options.utilityPath):
   318             paths.insert(0, options.utilityPath)
   319         options.utilityPath = self.findPath(paths, xpcshell)
   320         if options.utilityPath == None:
   321             print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name)
   322             return 1
   324         options.serverProfilePath = tempfile.mkdtemp()
   325         self.server = ReftestServer(localAutomation, options, self.scriptDir)
   326         retVal = self.server.start()
   327         if retVal:
   328             return retVal
   329         retVal = self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
   330         if retVal:
   331             return retVal
   333         options.xrePath = remoteXrePath
   334         options.utilityPath = remoteUtilityPath
   335         return 0
   337     def stopWebServer(self, options):
   338         self.server.stop()
   340     def createReftestProfile(self, options, reftestlist):
   341         profile = RefTest.createReftestProfile(self, options, reftestlist, server=options.remoteWebServer)
   342         profileDir = profile.profile
   344         prefs = {}
   345         prefs["browser.firstrun.show.localepicker"] = False
   346         prefs["font.size.inflation.emPerLine"] = 0
   347         prefs["font.size.inflation.minTwips"] = 0
   348         prefs["reftest.remote"] = True
   349         # Set a future policy version to avoid the telemetry prompt.
   350         prefs["toolkit.telemetry.prompted"] = 999
   351         prefs["toolkit.telemetry.notifiedOptOut"] = 999
   352         prefs["reftest.uri"] = "%s" % reftestlist
   353         prefs["datareporting.policy.dataSubmissionPolicyBypassAcceptance"] = True
   355         # Point the url-classifier to the local testing server for fast failures
   356         prefs["browser.safebrowsing.gethashURL"] = "http://127.0.0.1:8888/safebrowsing-dummy/gethash"
   357         prefs["browser.safebrowsing.updateURL"] = "http://127.0.0.1:8888/safebrowsing-dummy/update"
   358         # Point update checks to the local testing server for fast failures
   359         prefs["extensions.update.url"] = "http://127.0.0.1:8888/extensions-dummy/updateURL"
   360         prefs["extensions.update.background.url"] = "http://127.0.0.1:8888/extensions-dummy/updateBackgroundURL"
   361         prefs["extensions.blocklist.url"] = "http://127.0.0.1:8888/extensions-dummy/blocklistURL"
   362         prefs["extensions.hotfix.url"] = "http://127.0.0.1:8888/extensions-dummy/hotfixURL"
   363         # Turn off extension updates so they don't bother tests
   364         prefs["extensions.update.enabled"] = False
   365         # Make sure opening about:addons won't hit the network
   366         prefs["extensions.webservice.discoverURL"] = "http://127.0.0.1:8888/extensions-dummy/discoveryURL"
   367         # Make sure AddonRepository won't hit the network
   368         prefs["extensions.getAddons.maxResults"] = 0
   369         prefs["extensions.getAddons.get.url"] = "http://127.0.0.1:8888/extensions-dummy/repositoryGetURL"
   370         prefs["extensions.getAddons.getWithPerformance.url"] = "http://127.0.0.1:8888/extensions-dummy/repositoryGetWithPerformanceURL"
   371         prefs["extensions.getAddons.search.browseURL"] = "http://127.0.0.1:8888/extensions-dummy/repositoryBrowseURL"
   372         prefs["extensions.getAddons.search.url"] = "http://127.0.0.1:8888/extensions-dummy/repositorySearchURL"
   373         # Make sure that opening the plugins check page won't hit the network
   374         prefs["plugins.update.url"] = "http://127.0.0.1:8888/plugins-dummy/updateCheckURL"
   375         prefs["layout.css.devPixelsPerPx"] = "1.0"
   377         # Disable skia-gl: see bug 907351
   378         prefs["gfx.canvas.azure.accelerated"] = False
   380         # Set the extra prefs.
   381         profile.set_preferences(prefs)
   383         try:
   384             self._devicemanager.pushDir(profileDir, options.remoteProfile)
   385         except devicemanager.DMError:
   386             print "Automation Error: Failed to copy profiledir to device"
   387             raise
   389         return profile
   391     def copyExtraFilesToProfile(self, options, profile):
   392         profileDir = profile.profile
   393         RefTest.copyExtraFilesToProfile(self, options, profile)
   394         try:
   395             self._devicemanager.pushDir(profileDir, options.remoteProfile)
   396         except devicemanager.DMError:
   397             print "Automation Error: Failed to copy extra files to device"
   398             raise
   400     def getManifestPath(self, path):
   401         return path
   403     def printDeviceInfo(self, printLogcat=False):
   404         try:
   405             if printLogcat:
   406                 logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
   407                 print ''.join(logcat)
   408             print "Device info: %s" % self._devicemanager.getInfo()
   409             print "Test root: %s" % self._devicemanager.getDeviceRoot()
   410         except devicemanager.DMError:
   411             print "WARNING: Error getting device information"
   413     def cleanup(self, profileDir):
   414         # Pull results back from device
   415         if self.remoteLogFile and \
   416                 self._devicemanager.fileExists(self.remoteLogFile):
   417             self._devicemanager.getFile(self.remoteLogFile, self.localLogName)
   418         else:
   419             print "WARNING: Unable to retrieve log file (%s) from remote " \
   420                 "device" % self.remoteLogFile
   421         self._devicemanager.removeDir(self.remoteProfile)
   422         self._devicemanager.removeDir(self.remoteTestRoot)
   423         RefTest.cleanup(self, profileDir)
   424         if (self.pidFile != ""):
   425             try:
   426                 os.remove(self.pidFile)
   427                 os.remove(self.pidFile + ".xpcshell.pid")
   428             except:
   429                 print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % self.pidFile
   431 def main(args):
   432     automation = RemoteAutomation(None)
   433     parser = RemoteOptions(automation)
   434     options, args = parser.parse_args()
   436     if (options.deviceIP == None):
   437         print "Error: you must provide a device IP to connect to via the --device option"
   438         return 1
   440     try:
   441         if (options.dm_trans == "adb"):
   442             if (options.deviceIP):
   443                 dm = droid.DroidADB(options.deviceIP, options.devicePort, deviceRoot=options.remoteTestRoot)
   444             else:
   445                 dm = droid.DroidADB(None, None, deviceRoot=options.remoteTestRoot)
   446         else:
   447             dm = droid.DroidSUT(options.deviceIP, options.devicePort, deviceRoot=options.remoteTestRoot)
   448     except devicemanager.DMError:
   449         print "Automation Error: exception while initializing devicemanager.  Most likely the device is not in a testable state."
   450         return 1
   452     automation.setDeviceManager(dm)
   454     if (options.remoteProductName != None):
   455         automation.setProduct(options.remoteProductName)
   457     # Set up the defaults and ensure options are set
   458     options = parser.verifyRemoteOptions(options)
   459     if (options == None):
   460         print "ERROR: Invalid options specified, use --help for a list of valid options"
   461         return 1
   463     if not options.ignoreWindowSize:
   464         parts = dm.getInfo('screen')['screen'][0].split()
   465         width = int(parts[0].split(':')[1])
   466         height = int(parts[1].split(':')[1])
   467         if (width < 1050 or height < 1050):
   468             print "ERROR: Invalid screen resolution %sx%s, please adjust to 1366x1050 or higher" % (width, height)
   469             return 1
   471     automation.setAppName(options.app)
   472     automation.setRemoteProfile(options.remoteProfile)
   473     automation.setRemoteLog(options.remoteLogFile)
   474     reftest = RemoteReftest(automation, dm, options, SCRIPT_DIRECTORY)
   475     options = parser.verifyCommonOptions(options, reftest)
   477     # Hack in a symbolic link for jsreftest
   478     os.system("ln -s ../jsreftest " + str(os.path.join(SCRIPT_DIRECTORY, "jsreftest")))
   480     # Dynamically build the reftest URL if possible, beware that args[0] should exist 'inside' the webroot
   481     manifest = args[0]
   482     if os.path.exists(os.path.join(SCRIPT_DIRECTORY, args[0])):
   483         manifest = "http://" + str(options.remoteWebServer) + ":" + str(options.httpPort) + "/" + args[0]
   484     elif os.path.exists(args[0]):
   485         manifestPath = os.path.abspath(args[0]).split(SCRIPT_DIRECTORY)[1].strip('/')
   486         manifest = "http://" + str(options.remoteWebServer) + ":" + str(options.httpPort) + "/" + manifestPath
   487     else:
   488         print "ERROR: Could not find test manifest '%s'" % manifest
   489         return 1
   491     # Start the webserver
   492     retVal = reftest.startWebServer(options)
   493     if retVal:
   494         return retVal
   496     procName = options.app.split('/')[-1]
   497     if (dm.processExist(procName)):
   498         dm.killProcess(procName)
   500     reftest.printDeviceInfo()
   502 #an example manifest name to use on the cli
   503 #    manifest = "http://" + options.remoteWebServer + "/reftests/layout/reftests/reftest-sanity/reftest.list"
   504     retVal = 0
   505     try:
   506         cmdlineArgs = ["-reftest", manifest]
   507         if options.bootstrap:
   508             cmdlineArgs = []
   509         dm.recordLogcat()
   510         retVal = reftest.runTests(manifest, options, cmdlineArgs)
   511     except:
   512         print "Automation Error: Exception caught while running tests"
   513         traceback.print_exc()
   514         retVal = 1
   516     reftest.stopWebServer(options)
   518     reftest.printDeviceInfo(printLogcat=True)
   520     return retVal
   522 if __name__ == "__main__":
   523     sys.exit(main(sys.argv[1:]))

mercurial