Bug 573263 - Refactor remote reftest to work on android, create shared remoteautomation class r=jmaher
authorClint Talbert <ctalbert@mozilla.com>
Thu, 24 Jun 2010 02:32:01 -0700
changeset 46150 323c9405300945ade2cd74b002ec1bfc0717254a
parent 46149 3c4932e0058ba77e52cc9c9d5233e02e1cf1a169
child 46151 abddae3485f93dbc0c5ea3f58c74fc6a8a0a0f34
push idunknown
push userunknown
push dateunknown
reviewersjmaher
bugs573263
milestone1.9.3a6pre
Bug 573263 - Refactor remote reftest to work on android, create shared remoteautomation class r=jmaher
build/automation.py.in
build/mobile/remoteautomation.py
layout/tools/reftest/Makefile.in
layout/tools/reftest/remotereftest.py
layout/tools/reftest/runreftest.py
testing/mochitest/Makefile.in
testing/mochitest/runtests.py.in
testing/mochitest/runtestsremote.py
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -45,16 +45,17 @@ import os
 import re
 import select
 import shutil
 import signal
 import subprocess
 import sys
 import threading
 import tempfile
+import zipfile
 
 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
 sys.path.insert(0, SCRIPT_DIR)
 import automationutils
 
 _DEFAULT_WEB_SERVER = "127.0.0.1"
 _DEFAULT_HTTP_PORT = 8888
 _DEFAULT_SSL_PORT = 4443
@@ -820,8 +821,53 @@ user_pref("camino.use_system_proxy_setti
 
     if os.path.exists(processLog):
       os.unlink(processLog)
 
     if self.IS_TEST_BUILD and runSSLTunnel:
       ssltunnelProcess.kill()
 
     return status
+
+  """ 
+   Copies an "installed" extension into the extensions directory of the given profile
+   extensionSource - the source location of the extension files.  This can be either 
+                     a directory or a path to an xpi file.
+   profileDir      - the profile directory we are copying into.  We will create the
+                     "extensions" directory there if it doesn't exist
+   extensionID     - the id of the extension to be used as the containing directory for the
+                     extension, i.e.
+                 this is the name of the folder in the <profileDir>/extensions/<extensionID>
+  """
+  def installExtension(self, extensionSource, profileDir, extensionID):
+    if (not os.path.exists(extensionSource)):
+      self.log.info("INFO | automation.py | Cannot install extension no source at: %s", extensionSource) 
+      
+    if (not os.path.exists(profileDir)):
+      self.log.info("INFO | automation.py | Cannot install extension invalid profileDir at: %s", profileDir)
+
+    # See if we have an XPI or a directory
+    if (os.path.isfile(extensionSource)):
+      tmpd = tempfile.mkdtemp()
+      extrootdir = self.extractZip(extensionSource, tmpd)
+    else:
+      extrootdir = extensionSource 
+    extnsdir = os.path.join(profileDir, "extensions")
+    extnshome = os.path.join(extnsdir, extensionID)
+
+    # Now we copy the extension source into the extnshome
+    shutil.copytree(extrootdir, extnshome)
+
+  def extractZip(self, filename, dest):
+    z = zipfile.ZipFile(filename, 'r')
+    for n in z.namelist():
+      fullpath = os.path.join(dest, n)
+      parentdir = os.path.dirname(fullpath)
+      if not os.path.isdir(parentdir):
+        os.makedirs(parentdir)
+      if (not n.endswith(os.sep)):
+        data = z.read(n)
+        f = open(fullpath, 'w')
+        f.write(data)
+        f.close()
+    z.close()
+    return dest
+
new file mode 100644
--- /dev/null
+++ b/build/mobile/remoteautomation.py
@@ -0,0 +1,144 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is Joel Maher.
+#
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Joel Maher <joel.maher@gmail.com> (Original Developer)
+# Clint Talbert <cmtalbert@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+import time
+import sys
+import os
+
+from automation import Automation
+from devicemanager import DeviceManager
+
+class RemoteAutomation(Automation):
+    _devicemanager = None
+    
+    def __init__(self, deviceManager, appName = ''):
+        self._devicemanager = deviceManager
+        self._appName = appName
+        self._remoteProfile = None
+        # Default our product to fennec
+        self._product = "fennec"
+        Automation.__init__(self)
+
+    def setDeviceManager(self, deviceManager):
+        self._devicemanager = deviceManager
+        
+    def setAppName(self, appName):
+        self._appName = appName
+
+    def setRemoteProfile(self, remoteProfile):
+        self._remoteProfile = remoteProfile
+
+    def setProduct(self, product):
+        self._product = product
+
+    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo):
+        # maxTime is used to override the default timeout, we should honor that
+        status = proc.wait(timeout = maxTime)
+
+        print proc.stdout
+
+        if (status == 1 and self._devicemanager.processExist(proc.procName)):
+            # Then we timed out, make sure Fennec is dead
+            proc.kill()
+
+        return status
+
+    def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
+        # If remote profile is specified, use that instead
+        if (self._remoteProfile):
+            profileDir = self._remoteProfile
+
+        cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
+        # Remove -foreground if it exists, if it doesn't this just returns
+        try:
+            args.remove('-foreground')
+        except:
+            pass
+#TODO: figure out which platform require NO_EM_RESTART
+#        return app, ['--environ:NO_EM_RESTART=1'] + args
+        return app, args
+
+    def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = '.'):
+        return self.RProcess(self._devicemanager, cmd, stdout, stderr, env, cwd)
+
+    # be careful here as this inner class doesn't have access to outer class members    
+    class RProcess(object):
+        # device manager process
+        dm = None
+        def __init__(self, dm, cmd, stdout = None, stderr = None, env = None, cwd = '.'):
+            self.dm = dm
+            print "going to launch process: " + str(self.dm.host)
+            self.proc = dm.launchProcess(cmd)
+            exepath = cmd[0]
+            name = exepath.split('/')[-1]
+            self.procName = name
+
+            # Setting timeout at 1 hour since on a remote device this takes much longer
+            self.timeout = 3600
+            time.sleep(15)
+
+        @property
+        def pid(self):
+            hexpid = self.dm.processExist(self.procName)
+            if (hexpid == '' or hexpid == None):
+                hexpid = "0x0"
+            return int(hexpid, 0)
+    
+        @property
+        def stdout(self):
+            return self.dm.getFile(self.proc)
+ 
+        def wait(self, timeout = None):
+            timer = 0
+            interval = 5
+
+            if timeout == None:
+                timeout = self.timeout
+
+            while (self.dm.processExist(self.procName)):
+                time.sleep(interval)
+                timer += interval
+                if (timer > timeout):
+                    break
+
+            if (timer >= timeout):
+                return 1
+            return 0
+ 
+        def kill(self):
+            self.dm.killProcess(self.procName)
--- a/layout/tools/reftest/Makefile.in
+++ b/layout/tools/reftest/Makefile.in
@@ -74,16 +74,17 @@ endif
 
 _HARNESS_FILES = \
   $(srcdir)/runreftest.py \
   $(srcdir)/remotereftest.py \
   automation.py \
   $(topsrcdir)/build/mobile/devicemanager.py \
   $(topsrcdir)/build/automationutils.py \
   $(topsrcdir)/build/poster.zip \
+  $(topsrcdir)/build/mobile/remoteautomation.py \
   $(NULL)
 
 $(_DEST_DIR):
 	$(NSINSTALL) -D $@
 
 $(_HARNESS_FILES): $(_DEST_DIR)
 
 # copy harness and the reftest extension bits to $(_DEST_DIR)
--- a/layout/tools/reftest/remotereftest.py
+++ b/layout/tools/reftest/remotereftest.py
@@ -42,205 +42,166 @@ import time
 SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
 sys.path.append(SCRIPT_DIRECTORY)
 #os.chdir(SCRIPT_DIRECTORY)         
 
 from runreftest import RefTest
 from runreftest import ReftestOptions
 from automation import Automation
 from devicemanager import DeviceManager
-
-class RemoteAutomation(Automation):
-    _devicemanager = None
-
-    def __init__(self, deviceManager, product = ''):
-        self._devicemanager = deviceManager
-        self._product = product
-        Automation.__init__(self)
-
-    def setDeviceManager(self, deviceManager):
-        self._devicemanager = deviceManager
-
-    def setProduct(self, productName):
-        self._product = productName
-
-    def setRemoteApp(self, remoteAppName):
-        self._remoteAppName = remoteAppName
-
-    def setTestRoot(self, testRoot):
-        self._testRoot = testRoot
-
-    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime):
-        status = proc.wait()
-        print proc.stdout
-        # todo: consider pulling log file from remote
-        return status
-
-    def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
-        remoteProfileDir = self._testRoot + 'profile'
-        cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, remoteProfileDir, testURL, extraArgs)
-        return app, ['--environ:NO_EM_RESTART=1'] + args
-
-    def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = '.'):
-        return self.RProcess(self._devicemanager, self._remoteAppName, cmd, stdout, stderr, env, cwd)
-
-    class RProcess(object):
-        #device manager process
-        dm = None
-        def __init__(self, dm, appName, cmd, stdout = None, stderr = None, env = None, cwd = '.'):
-            self.dm = dm
-            print "going to launch process: " + str(self.dm.host)
-            self.proc = dm.launchProcess(cmd)
-            self.procName = appName
-
-            # Setting this at 1 hour since remote testing is much slower
-            self.timeout = 3600
-            time.sleep(5)
-
-        @property
-        def pid(self):
-            hexpid = self.dm.processExist(self.procName)
-            if (hexpid == '' or hexpid == None):
-                hexpid = 0
-            return int(hexpid, 0)
-
-        @property
-        def stdout(self):
-            return self.dm.getFile(self.proc)
-
-        def wait(self, timeout = None):
-            timer = 0
-            if timeout == None:
-                timeout = self.timeout
-
-            while (self.dm.process.isAlive()):
-                time.sleep(1)
-                timer += 1
-                if (timer > timeout):
-                    break
-
-            if (timer >= timeout):
-                return 1
-            return 0
-
-        def kill(self):
-            self.dm.killProcess(self.procName)
- 
+from remoteautomation import RemoteAutomation
 
 class RemoteOptions(ReftestOptions):
     def __init__(self, automation):
         ReftestOptions.__init__(self, automation)
 
         defaults = {}
         defaults["logFile"] = "reftest.log"
         # app, xrePath and utilityPath variables are set in main function
-        defaults["testRoot"] = "/tests/"
+        defaults["remoteTestRoot"] = None
         defaults["app"] = ""
         defaults["xrePath"] = ""
         defaults["utilityPath"] = ""
 
-        self.add_option("--device", action="store",
-                    type = "string", dest = "device",
+        self.add_option("--remote-app-path", action="store",
+                    type = "string", dest = "remoteAppPath",
+                    help = "Path to remote executable relative to device root using only forward slashes.  Either this or app must be specified, but not both.")
+        defaults["remoteAppPath"] = None
+
+        self.add_option("--deviceIP", action="store",
+                    type = "string", dest = "deviceIP",
                     help = "ip address of remote device to test")
-        defaults["device"] = None
+        defaults["deviceIP"] = None
 
         self.add_option("--devicePort", action="store",
                     type = "string", dest = "devicePort",
                     help = "port of remote device to test")
-        defaults["devicePort"] = 27020
+        defaults["devicePort"] = 20701
 
-        self.add_option("--remoteProductName", action="store",
+        self.add_option("--remote-product-name", action="store",
                     type = "string", dest = "remoteProductName",
-                    help = "Name of remote product to test - either fennec or firefox, defaults to fennec")
+                    help = "Name of product to test - either fennec or firefox, defaults to fennec")
         defaults["remoteProductName"] = "fennec"
 
-        self.add_option("--remoteAppName", action="store",
-                    type = "string", dest = "remoteAppName",
-                    help = "Executable name for remote device, OS dependent, defaults to fennec.exe")
-        defaults["remoteAppName"] = "fennec.exe"
-
         self.add_option("--remote-webserver", action="store",
                     type = "string", dest = "remoteWebServer",
                     help = "IP Address of the webserver hosting the reftest content")
-        defaults["remoteWebServer"] = "127.0.0.1"
+        defaults["remoteWebServer"] = None
+
+        self.add_option("--http-port", action = "store",
+                    type = "string", dest = "httpPort",
+                    help = "port of the web server for http traffic")
+        defaults["httpPort"] = automation.DEFAULT_HTTP_PORT
+
+        self.add_option("--ssl-port", action = "store",
+                    type = "string", dest = "sslPort",
+                    help = "Port for https traffic to the web server")
+        defaults["sslPort"] = automation.DEFAULT_SSL_PORT
+
+        self.add_option("--remote-logfile", action="store",
+                    type = "string", dest = "remoteLogFile",
+                    help = "Name of log file on the device relative to device root.  PLEASE USE ONLY A FILENAME.")
+        defaults["remoteLogFile"] = "reftest.log"
 
         self.set_defaults(**defaults)
 
+    def verifyRemoteOptions(self, options):
+        # Ensure our defaults are set properly for everything we can infer
+        options.remoteTestRoot = self._automation._devicemanager.getDeviceRoot() + '/reftest'
+        options.remoteProfile = options.remoteTestRoot + "/profile"
+
+        # One of remoteAppPath (relative path to application) or the app (executable) must be
+        # set, but not both.  If both are set, we destroy the user's selection for app
+        # so instead of silently destroying a user specificied setting, we error.
+        if (options.remoteAppPath and options.app):
+            print "ERROR: You cannot specify both the remoteAppPath and the app"
+            return None
+        elif (options.remoteAppPath):
+            options.app = options.remoteTestRoot + "/" + options.remoteAppPath
+        elif (options.app == None):
+            # Neither remoteAppPath nor app are set -- error
+            print "ERROR: You must specify either appPath or app"
+            return None
+        
+        if (options.xrePath == None):
+            print "ERROR: You must specify the path to the controller xre directory"
+            return None
+
+        # TODO: Copied from main, but I think these are no longer used in a post xulrunner world
+        #options.xrePath = options.remoteTestRoot + self._automation._product + '/xulrunner'
+        #options.utilityPath = options.testRoot + self._automation._product + '/bin'
+        return options
+
 class RemoteReftest(RefTest):
     remoteApp = ''
 
     def __init__(self, automation, devicemanager, options, scriptDir):
         RefTest.__init__(self, automation)
         self._devicemanager = devicemanager
         self.scriptDir = scriptDir
         self.remoteApp = options.app
-        self.remoteTestRoot = options.testRoot
-        self.remoteProfileDir = options.testRoot + 'profile'
+        self.remoteTestRoot = options.remoteTestRoot
 
     def createReftestProfile(self, options, profileDir):
         RefTest.createReftestProfile(self, options, profileDir)
 
-        self.remoteTestRoot += "reftest/"
-
-        # install the reftest extension bits into the remoteProfile
-        profileExtensionsPath = os.path.join(profileDir, "extensions")
-        reftestExtensionPath = self.remoteTestRoot.replace('/', '\\')
-        extFile = open(os.path.join(profileExtensionsPath, "reftest@mozilla.org"), "w")
-        extFile.write(reftestExtensionPath)
-        extFile.close()
-
-        if (self._devicemanager.pushDir(profileDir, self.remoteProfileDir) == None):
-          raise devicemanager.FileError("Failed to copy profiledir to device")
-
-        if (self._devicemanager.pushDir(self.scriptDir + '/reftest', self.remoteTestRoot) == None):
-          raise devicemanager.FileError("Failed to copy extension dir to device")
-
+        if (self._devicemanager.pushDir(profileDir, options.remoteProfile) == None):
+            raise devicemanager.FileError("Failed to copy profiledir to device")
 
     def copyExtraFilesToProfile(self, options, profileDir):
         RefTest.copyExtraFilesToProfile(self, options, profileDir)
-        if (self._devicemanager.pushDir(profileDir, self.remoteProfileDir) == None):
-          raise devicemanager.FileError("Failed to copy extra files in profile dir to device")
+        if (self._devicemanager.pushDir(profileDir, options.remoteProfile) == None):
+            raise devicemanager.FileError("Failed to copy extra files to device") 
 
-    def registerExtension(self, browserEnv, options, profileDir):
-        """
-          It appears that we do not need to do the extension registration on winmo.
-          This is something we should look into for winmo as the -silent option isn't working
-        """
-        pass
+    def registerExtension(self, browserEnv, options, profileDir, extraArgs = ['-silent'] ):
+        self.automation.log.info("REFTEST INFO | runreftest.py | Performing extension manager registration: start.\n")
+        # Because our startProcess code doesn't return until fennec starts we just give it
+        # a maxTime of 20 secs before timing it out and ensuring it is dead.
+        # Besides registering the extension, this works around fennec bug 570027
+        status = self.automation.runApp(None, browserEnv, options.app, profileDir,
+                                   extraArgs,
+                                   utilityPath = options.utilityPath,
+                                   xrePath=options.xrePath,
+                                   symbolsPath=options.symbolsPath,
+                                   maxTime = 20)
+        # We don't care to call |processLeakLog()| for this step.
+        self.automation.log.info("\nREFTEST INFO | runreftest.py | Performing extension manager registration: end.")
 
     def getManifestPath(self, path):
         return path
 
     def cleanup(self, profileDir):
         self._devicemanager.removeDir(self.remoteProfileDir)
         self._devicemanager.removeDir(self.remoteTestRoot)
         RefTest.cleanup(self, profileDir)
 
 def main():
     dm = DeviceManager(None, None)
     automation = RemoteAutomation(dm)
     parser = RemoteOptions(automation)
     options, args = parser.parse_args()
 
-    if (options.device == None):
+    if (options.deviceIP == None):
         print "Error: you must provide a device IP to connect to via the --device option"
         sys.exit(1)
 
-    if options.remoteAppName.rfind('/') < 0:
-      options.app = options.testRoot + options.remoteProductName + '/'
-    options.app += str(options.remoteAppName)
+    dm = DeviceManager(options.deviceIP, options.devicePort)
+    automation.setDeviceManager(dm)
+
+    if (options.remoteProductName != None):
+        automation.setProduct(options.remoteProductName)
 
-    options.xrePath = options.testRoot + options.remoteProductName + '/xulrunner'
-    options.utilityPath = options.testRoot + options.remoteProductName + '/bin'
+    # Set up the defaults and ensure options are set
+    options = parser.verifyRemoteOptions(options)
+    if (options == None):
+        print "ERROR: Invalid options specified, use --help for a list of valid options"
+        sys.exit(1)
 
-    dm = DeviceManager(options.device, options.devicePort)
-    automation.setDeviceManager(dm)
-    automation.setProduct(options.remoteProductName)
-    automation.setRemoteApp(options.remoteAppName)
-    automation.setTestRoot(options.testRoot)
+    automation.setAppName(options.app)
+    automation.setRemoteProfile(options.remoteProfile)
     reftest = RemoteReftest(automation, dm, options, SCRIPT_DIRECTORY)
 
     if (options.remoteWebServer == "127.0.0.1"):
         print "Error: remoteWebServer must be non localhost"
         sys.exit(1)
 
 #an example manifest name to use on the cli
 #    manifest = "http://" + options.remoteWebServer + "/reftests/layout/reftests/reftest-sanity/reftest.list"
--- a/layout/tools/reftest/runreftest.py
+++ b/layout/tools/reftest/runreftest.py
@@ -88,22 +88,20 @@ class RefTest(object):
       part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
       prefsFile.write(part)
     # no slow script dialogs
     prefsFile.write('user_pref("dom.max_script_run_time", 0);')
     prefsFile.write('user_pref("dom.max_chrome_script_run_time", 0);')
     prefsFile.close()
 
     # install the reftest extension bits into the profile
-    profileExtensionsPath = os.path.join(profileDir, "extensions")
-    os.mkdir(profileExtensionsPath)
-    reftestExtensionPath = os.path.join(SCRIPT_DIRECTORY, "reftest")
-    extFile = open(os.path.join(profileExtensionsPath, "reftest@mozilla.org"), "w")
-    extFile.write(reftestExtensionPath)
-    extFile.close()
+    self.automation.installExtension(os.path.join(SCRIPT_DIRECTORY, "reftest"),
+                                                  profileDir,
+                                                  "reftest@mozilla.org")
+
 
   def registerExtension(self, browserEnv, options, profileDir, extraArgs = ['-silent']):
     # run once with -silent to let the extension manager do its thing
     # and then exit the app
     self.automation.log.info("REFTEST INFO | runreftest.py | Performing extension manager registration: start.\n")
     # Don't care about this |status|: |runApp()| reporting it should be enough.
     status = self.automation.runApp(None, browserEnv, options.app, profileDir,
                                  extraArgs,
@@ -144,17 +142,16 @@ class RefTest(object):
       # browser environment
       browserEnv = self.buildBrowserEnv(options, profileDir)
 
       self.registerExtension(browserEnv, options, profileDir)
 
       # then again to actually run reftest
       self.automation.log.info("REFTEST INFO | runreftest.py | Running tests: start.\n")
       reftestlist = self.getManifestPath(manifest)
-
       status = self.automation.runApp(None, browserEnv, options.app, profileDir,
                                  ["-reftest", reftestlist],
                                  utilityPath = options.utilityPath,
                                  xrePath=options.xrePath,
                                  debuggerInfo=debuggerInfo,
                                  symbolsPath=options.symbolsPath,
                                  # give the JS harness 30 seconds to deal
                                  # with its own timeouts
@@ -174,24 +171,25 @@ class RefTest(object):
         shutil.copytree(abspath, dest)
       else:
         shutil.copy(abspath, dest)
 
 
 class ReftestOptions(OptionParser):
 
   def __init__(self, automation):
+    self._automation = automation
     OptionParser.__init__(self)
     defaults = {}
 
     # we want to pass down everything from automation.__all__
     addCommonOptions(self, 
-                     defaults=dict(zip(automation.__all__, 
-                            [getattr(automation, x) for x in automation.__all__])))
-    automation.addCommonOptions(self)
+                     defaults=dict(zip(self._automation.__all__, 
+                            [getattr(self._automation, x) for x in self._automation.__all__])))
+    self._automation.addCommonOptions(self)
     self.add_option("--appname",
                     action = "store", type = "string", dest = "app",
                     default = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP),
                     help = "absolute path to application, overriding default")
     self.add_option("--extra-profile-file",
                     action = "append", dest = "extraProfileFiles",
                     default = [],
                     help = "copy specified files/dirs to testing profile")
@@ -203,20 +201,20 @@ class ReftestOptions(OptionParser):
                     action = "store", type = "int", dest = "leakThreshold",
                     default = 0,
                     help = "fail if the number of bytes leaked through "
                            "refcounted objects (or bytes in classes with "
                            "MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) is greater "
                            "than the given number")
     self.add_option("--utility-path",
                     action = "store", type = "string", dest = "utilityPath",
-                    default = automation.DIST_BIN,
+                    default = self._automation.DIST_BIN,
                     help = "absolute path to directory containing utility "
                            "programs (xpcshell, ssltunnel, certutil)")
-    defaults["utilityPath"] = automation.DIST_BIN
+    defaults["utilityPath"] = self._automation.DIST_BIN
 
     self.add_option("--total-chunks",
                     type = "int", dest = "totalChunks",
                     help = "how many chunks to split the tests up into")
     defaults["totalChunks"] = None
 
     self.add_option("--this-chunk",
                     type = "int", dest = "thisChunk",
--- a/testing/mochitest/Makefile.in
+++ b/testing/mochitest/Makefile.in
@@ -61,16 +61,17 @@ include $(topsrcdir)/build/automation-bu
 _SERV_FILES = 	\
 		runtests.py \
 		automation.py \
 		runtestsremote.py \
 		runtestsvmware.py \
 		$(topsrcdir)/build/mobile/devicemanager.py \
 		$(topsrcdir)/build/automationutils.py \
 		$(topsrcdir)/build/poster.zip \
+		$(topsrcdir)/build/mobile/remoteautomation.py \
 		gen_template.pl \
 		server.js \
 		harness-a11y.xul \
 		harness-overlay.xul \
 		harness.xul \
 		browser-test-overlay.xul \
 		browser-test.js \
 		browser-harness.xul \
--- a/testing/mochitest/runtests.py.in
+++ b/testing/mochitest/runtests.py.in
@@ -336,17 +336,20 @@ class MochitestServer:
       self.stop()
       sys.exit(1)
 
   def stop(self):
     try:
       c = urllib2.urlopen(self.shutdownURL)
       c.read()
       c.close()
-      self._process.wait()
+
+      rtncode = self._process.poll()
+      if (rtncode == None):
+        self._process.terminate()
     except:
       self._process.kill()
 
 class WebSocketServer(object):
   "Class which encapsulates the mod_pywebsocket server"
 
   def __init__(self, automation, options, scriptdir):
     self.port = options.webSocketPort
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -39,121 +39,45 @@ import sys
 import os
 import time
 import socket
 import tempfile
 
 sys.path.insert(0, os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))))
 
 from automation import Automation
+from remoteautomation import RemoteAutomation
 from runtests import Mochitest
 from runtests import MochitestOptions
 from runtests import MochitestServer
 
 import devicemanager
 
-class RemoteAutomation(Automation):
-    _devicemanager = None
-    
-    def __init__(self, deviceManager, product):
-        self._devicemanager = deviceManager
-        self._product = product
-        Automation.__init__(self)
-
-    def setDeviceManager(self, deviceManager):
-        self._devicemanager = deviceManager
-        
-    def setProduct(self, productName):
-        self._product = productName
-        
-    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo):
-        status = proc.wait()
-        print proc.stdout
-        # todo: consider pulling log file from remote
-        return status
-        
-    def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
-        cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
-        # Remove -foreground if it exists, if it doesn't this just returns
-        try:
-          args.remove('-foreground')
-        except:
-          pass
-#TODO: figure out which platform require NO_EM_RESTART
-#        return app, ['--environ:NO_EM_RESTART=1'] + args
-        return app, args
-
-    def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = '.'):
-        return self.RProcess(self._devicemanager, self._product, cmd, stdout, stderr, env, cwd)
-
-    # be careful here as this inner class doesn't have access to outer class members    
-    class RProcess(object):
-        # device manager process
-        dm = None
-        def __init__(self, dm, product, cmd, stdout = None, stderr = None, env = None, cwd = '.'):
-            self.dm = dm
-            print "going to launch process: " + str(self.dm.host)
-            self.proc = dm.launchProcess(cmd)
-            exepath = cmd[0]
-            name = exepath.split('/')[-1]
-            self.procName = name
-
-            # Setting timeout at 1 hour since on a remote device this takes much longer
-            self.timeout = 3600
-            time.sleep(15)
-
-        @property
-        def pid(self):
-            hexpid = self.dm.processExist(self.procName)
-            if (hexpid == '' or hexpid == None):
-                hexpid = "0x0"
-            return int(hexpid, 0)
-    
-        @property
-        def stdout(self):
-            return self.dm.getFile(self.proc)
- 
-        def wait(self, timeout = None):
-            timer = 0
-            interval = 5
-
-            if timeout == None:
-                timeout = self.timeout
-
-            while (self.dm.processExist(self.procName)):
-                time.sleep(interval)
-                timer += interval
-                if (timer > timeout):
-                    break
-
-            if (timer >= timeout):
-                return 1
-            return 0
- 
-        def kill(self):
-            self.dm.killProcess(self.procName)
- 
-
 class RemoteOptions(MochitestOptions):
 
     def __init__(self, automation, scriptdir, **kwargs):
         defaults = {}
         MochitestOptions.__init__(self, automation, scriptdir)
 
+        self.add_option("--remote-app-path", action="store",
+                    type = "string", dest = "remoteAppPath",
+                    help = "Path to remote executable relative to device root using only forward slashes. Either this or app must be specified but not both")
+        defaults["remoteAppPath"] = None
+
         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("--remoteProductName", action="store",
+        self.add_option("--remote-product-name", action="store",
                     type = "string", dest = "remoteProductName",
                     help = "The executable's name of remote product to test - either fennec or firefox, defaults to fennec")
         defaults["remoteProductName"] = "fennec"
 
         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
@@ -180,51 +104,56 @@ class RemoteOptions(MochitestOptions):
         defaults["testPath"] = ""
         defaults["app"] = None
 
         self.set_defaults(**defaults)
 
     def verifyRemoteOptions(self, options, automation):
         options.remoteTestRoot = automation._devicemanager.getDeviceRoot()
 
+        options.utilityPath = options.remoteTestRoot + "/bin"
         options.certPath = options.remoteTestRoot + "/certs"
 
-        if options.remoteWebServer == None:
-          if os.name != "nt":
+       
+        if options.remoteWebServer == None and os.name != "nt":
             options.remoteWebServer = get_lan_ip()
-          else:
+        elif os.name == "nt":
             print "ERROR: you must specify a remoteWebServer ip address\n"
             return None
 
         options.webServer = options.remoteWebServer
 
         if (options.deviceIP == None):
-          print "ERROR: you must provide a device IP"
-          return None
+            print "ERROR: you must provide a device IP"
+            return None
 
         if (options.remoteLogFile == None):
-          options.remoteLogFile =  automation._devicemanager.getDeviceRoot() + '/test.log'
+            options.remoteLogFile =  automation._devicemanager.getDeviceRoot() + '/test.log'
 
         # Set up our options that we depend on based on the above
         productRoot = options.remoteTestRoot + "/" + automation._product
+        options.utilityPath = productRoot + "/bin"
 
-        # Set this only if the user hasn't set it
-        if (options.utilityPath == None):
-          options.utilityPath = productRoot + "/bin"
-
-        # If provided, use cli value, otherwise reset as remoteTestRoot
-        if (options.app == None):
-          options.app = productRoot + "/" + options.remoteProductName
+        # remoteAppPath or app must be specified to find the product to launch
+        if (options.remoteAppPath and options.app):
+            print "ERROR: You cannot specify both the remoteAppPath and the app setting"
+            return None
+        elif (options.remoteAppPath):
+            options.app = options.remoteTestRoot + "/" + options.remoteAppPath
+        elif (options.app == None):
+            # Neither remoteAppPath nor app are set -- error
+            print "ERROR: You must specify either appPath or app"
+            return None
 
         # Only reset the xrePath if it wasn't provided
         if (options.xrePath == None):
-          if (automation._product == "fennec"):
-            options.xrePath = productRoot + "/xulrunner"
-          else:
-            options.xrePath = options.utilityPath
+            if (automation._product == "fennec"):
+                options.xrePath = productRoot + "/xulrunner"
+            else:
+                options.xrePath = options.utilityPath
 
         return options
 
     def verifyOptions(self, options, mochitest):
         # since we are reusing verifyOptions, it will exit if App is not found
         temp = options.app
         options.app = sys.argv[0]
         tempPort = options.httpPort
@@ -278,17 +207,17 @@ class MochiRemote(Mochitest):
         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.profilePath = tempfile.mkdtemp()
@@ -298,28 +227,42 @@ class MochiRemote(Mochitest):
       self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
       options.xrePath = remoteXrePath
       options.utilityPath = remoteUtilityPath
       options.profilePath = remoteProfilePath
          
     def stopWebServer(self, options):
         self.server.stop()
         
-    def runExtensionRegistration(self, options, browserEnv):
-        pass
-        
     def buildProfile(self, options):
         manifest = Mochitest.buildProfile(self, options)
         self.localProfile = options.profilePath
         if self._dm.pushDir(options.profilePath, self.remoteProfile) == None:
             raise devicemanager.FileError("Unable to copy profile to device.")
 
         options.profilePath = self.remoteProfile
         return manifest
-        
+    
+    def runExtensionRegistration(self, options, browserEnv):
+        """ run once with -silent to let the extension manager do its thing
+            and then exit the app
+            We do this on every run because we need to work around bug 570027
+        """
+        self._automation.log.info("INFO | runtestsremote.py | Performing extension manager registration: start.\n")
+        # Don't care about this |status|: |runApp()| reporting it should be enough.
+        # Because process() doesn't return until fennec starts, we just give it a fudge
+        # factor of 20s before timing it out and killing it.
+        status = self._automation.runApp(None, browserEnv, options.app,
+                                options.profilePath, ["-silent"],
+                                utilityPath = options.utilityPath,
+                                xrePath = options.xrePath,
+                                symbolsPath=options.symbolsPath,
+                                maxTime = 20)
+        # We don't care to call |processLeakLog()| for this step.
+        self._automation.log.info("\nINFO | runtestsremote.py | Performing extension manager registration: end.")
     def buildURLOptions(self, options):
         self.localLog = options.logFile
         options.logFile = self.remoteLog
         retVal = Mochitest.buildURLOptions(self, options)
         options.logFile = self.localLog
         return retVal
 
     def installChromeFile(self, filename, options):