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 id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjmaher
bugs573263
milestone1.9.3a6pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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):