Bug 826111 - Support mochitests on b2g desktop build, r=ahal
☠☠ backed out by 85ccfb160eb0 ☠ ☠
authorJonathan Griffin <jgriffin@mozilla.com>
Fri, 04 Jan 2013 10:41:34 -0800
changeset 118815 a5b75feea6dd383dc274b92903f93bdf8ed66ba4
parent 118814 10a5b939100d49499941f0e0b060a348cb8eecb7
child 118816 4ec09b9230837fe1c29b5b5445525e9ee3c339b9
push id24180
push useremorley@mozilla.com
push dateTue, 15 Jan 2013 22:58:27 +0000
treeherdermozilla-central@72e34ce7fd92 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersahal
bugs826111
milestone21.0a1
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 826111 - Support mochitests on b2g desktop build, r=ahal
build/automation.py.in
build/mobile/b2gautomation.py
testing/marionette/client/marionette/geckoinstance.py
testing/marionette/client/marionette/marionette.py
testing/mochitest/runtests.py
testing/mochitest/runtestsb2g.py
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -269,48 +269,47 @@ class Automation(object):
   def setupPermissionsDatabase(self, profileDir, permissions):
     # Open database and create table
     permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite"))
     cursor = permDB.cursor();
 
     cursor.execute("PRAGMA user_version=3");
 
     # SQL copied from nsPermissionManager.cpp
-    cursor.execute("""CREATE TABLE moz_hosts (
-       id INTEGER PRIMARY KEY,
-       host TEXT,
-       type TEXT,
-       permission INTEGER,
-       expireType INTEGER,
-       expireTime INTEGER,
-       appId INTEGER,
-       isInBrowserElement INTEGER)""")
+    cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
+      id INTEGER PRIMARY KEY,
+      host TEXT,
+      type TEXT,
+      permission INTEGER,
+      expireType INTEGER,
+      expireTime INTEGER,
+      appId INTEGER,
+      isInBrowserElement INTEGER)""")
 
     # Insert desired permissions
-    c = 0
     for perm in permissions.keys():
       for host,allow in permissions[perm]:
-        c += 1
-        cursor.execute("INSERT INTO moz_hosts values(?, ?, ?, ?, 0, 0, 0, 0)",
-                       (c, host, perm, 1 if allow else 2))
+        cursor.execute("INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)",
+                       (host, perm, 1 if allow else 2))
 
     # Commit and close
     permDB.commit()
     cursor.close()
 
   def setupTestApps(self, profileDir, apps):
-    webappJSONTemplate = Template(""""$name": {
-"origin": "$origin",
-"installOrigin": "$origin",
-"receipt": null,
-"installTime": 132333986000,
-"manifestURL": "$manifestURL",
-"localId": $localId,
-"appStatus": $appStatus,
-"csp": "$csp"
+    webappJSONTemplate = Template(""""$id": {
+  "origin": "$origin",
+  "installOrigin": "$origin",
+  "receipt": null,
+  "installTime": 132333986000,
+  "manifestURL": "$manifestURL",
+  "localId": $localId,
+  "id": "$id",
+  "appStatus": $appStatus,
+  "csp": "$csp"
 }""")
 
     manifestTemplate = Template("""{
   "name": "$name",
   "csp": "$csp",
   "description": "$description",
   "launch_path": "/",
   "developer": {
@@ -328,46 +327,109 @@ class Automation(object):
   "default_locale": "en-US",
   "icons": {
   }
 }
 """)
 
     # Create webapps/webapps.json
     webappsDir = os.path.join(profileDir, "webapps")
-    os.mkdir(webappsDir);
+    if not os.access(webappsDir, os.F_OK):
+      os.mkdir(webappsDir)
+
+    lineRe = re.compile(r'(.*?)"(.*?)": (.*)')
+
+    webappsJSONFilename = os.path.join(webappsDir, "webapps.json")
+    webappsJSON = []
+    if os.access(webappsJSONFilename, os.F_OK):
+      # If there is an existing webapps.json file (which will be the case for
+      # b2g), we parse the data in the existing file before appending test
+      # test apps to it.
+      startId = 1
+      webappsJSONFile = open(webappsJSONFilename, "r")
+      contents = webappsJSONFile.read()
 
-    webappsJSON = []
+      for app_content in contents.split('},'):
+        app = {}
+        # ghetto json parser needed due to lack of json/simplejson on test slaves
+        for line in app_content.split('\n'):
+          m = lineRe.match(line)
+          if m:
+            value = m.groups()[2]
+            # remove any trailing commas
+            if value[-1:] == ',':
+              value = value[:-1]
+            # set the app name from a line that looks like this:
+            # "name.gaiamobile.org": {
+            if value == '{':
+              app['id'] = m.groups()[1]
+            # parse string, None, bool and int types
+            elif value[0:1] == '"':
+              app[m.groups()[1]] = value[1:-1]
+            elif value == "null":
+              app[m.groups()[1]] = None
+            elif value == "true":
+              app[m.groups()[1]] = True
+            elif value == "false":
+              app[m.groups()[1]] = False
+            else:
+              app[m.groups()[1]] = int(value)
+        if app:
+          webappsJSON.append(app)
+
+      webappsJSONFile.close()
+
+    startId = 1
+    for app in webappsJSON:
+      if app['localId'] >= startId:
+        startId = app['localId'] + 1
+      if not app.get('csp'):
+        app['csp'] = ''
+      if not app.get('appStatus'):
+        app['appStatus'] = 3
+
     for localId, app in enumerate(apps):
-      app['localId'] = localId + 1 # Has to be 1..n
-      webappsJSON.append(webappJSONTemplate.substitute(app))
-    webappsJSON = '{\n' + ',\n'.join(webappsJSON) + '\n}\n'
+      app['localId'] = localId + startId # localId must be from 1..N
+      if not app.get('id'):
+        app['id'] = app['name']
+      webappsJSON.append(app)
 
-    webappsJSONFile = open(os.path.join(webappsDir, "webapps.json"), "a")
-    webappsJSONFile.write(webappsJSON)
+    contents = []
+    for app in webappsJSON:
+      contents.append(webappJSONTemplate.substitute(app))
+    contents = '{\n' + ',\n'.join(contents) + '\n}\n'
+
+    webappsJSONFile = open(webappsJSONFilename, "w")
+    webappsJSONFile.write(contents)
     webappsJSONFile.close()
 
     # Create manifest file for each app.
     for app in apps:
       manifest = manifestTemplate.substitute(app)
 
       manifestDir = os.path.join(webappsDir, app['name'])
       os.mkdir(manifestDir)
 
       manifestFile = open(os.path.join(manifestDir, "manifest.webapp"), "a")
       manifestFile.write(manifest)
       manifestFile.close()
 
-  def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
+  def initializeProfile(self, profileDir, extraPrefs=[],
+                        useServerLocations=False,
+                        initialProfile=None):
     " Sets up the standard testing profile."
 
     prefs = []
     # Start with a clean slate.
     shutil.rmtree(profileDir, True)
-    os.mkdir(profileDir)
+
+    if initialProfile:
+      shutil.copytree(initialProfile, profileDir)
+    else:
+      os.mkdir(profileDir)
 
     # Set up permissions database
     locations = self.readLocations()
     self.setupPermissionsDatabase(profileDir,
       {'allowXULXBL':[(l.host, 'noxul' not in l.options) for l in locations]});
 
     part = """\
 user_pref("social.skipLoadingProviders", true);
@@ -595,49 +657,49 @@ user_pref("camino.use_system_proxy_setti
       },
       {
         'name': 'https_example_csp_certified',
         'csp': "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'",
         'origin': 'https://example.com',
         'manifestURL': 'https://example.com/manifest_csp_cert.webapp',
         'description': 'https://example.com Certified App with manifest policy',
         'appStatus': _APP_STATUS_CERTIFIED
-      }, 
+      },
       {
         'name': 'https_example_csp_installed',
         'csp': "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'",
         'origin': 'https://example.com',
         'manifestURL': 'https://example.com/manifest_csp_inst.webapp',
         'description': 'https://example.com Installed App with manifest policy',
         'appStatus': _APP_STATUS_INSTALLED
-      }, 
+      },
       {
         'name': 'https_example_csp_privileged',
         'csp': "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'",
         'origin': 'https://example.com',
         'manifestURL': 'https://example.com/manifest_csp_priv.webapp',
         'description': 'https://example.com Privileged App with manifest policy',
         'appStatus': _APP_STATUS_PRIVILEGED
-      }, 
+      },
       {
         'name': 'https_a_domain_certified',
-        'csp' : "",
+        'csp': "",
         'origin': 'https://acertified.com',
         'manifestURL': 'https://acertified.com/manifest.webapp',
         'description': 'https://acertified.com Certified App',
         'appStatus': _APP_STATUS_CERTIFIED
-      }, 
+      },
       {
         'name': 'https_a_domain_privileged',
         'csp': "",
         'origin': 'https://aprivileged.com',
         'manifestURL': 'https://aprivileged.com/manifest.webapp',
         'description': 'https://aprivileged.com Privileged App ',
         'appStatus': _APP_STATUS_PRIVILEGED
-      }, 
+      },
     ];
     self.setupTestApps(profileDir, apps)
 
   def addCommonOptions(self, parser):
     "Adds command-line options which are common to mochitest and reftest."
 
     parser.add_option("--setpref",
                       action = "append", type = "string",
@@ -1028,17 +1090,17 @@ user_pref("camino.use_system_proxy_setti
 
   def checkForCrashes(self, profileDir, symbolsPath):
     return automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath, self.lastTestSeen)
 
   def runApp(self, testURL, env, app, profileDir, extraArgs,
              runSSLTunnel = False, utilityPath = None,
              xrePath = None, certPath = None,
              debuggerInfo = None, symbolsPath = None,
-             timeout = -1, maxTime = None):
+             timeout = -1, maxTime = None, onLaunch = None):
     """
     Run the app, log the duration it took to execute, return the status code.
     Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
     """
 
     if utilityPath == None:
       utilityPath = self.DIST_BIN
     if xrePath == None:
@@ -1085,16 +1147,21 @@ user_pref("camino.use_system_proxy_setti
     self.lastTestSeen = "automation.py"
     proc = self.Process([cmd] + args,
                  env = self.environment(env, xrePath = xrePath,
                                    crashreporter = not debuggerInfo),
                  stdout = outputPipe,
                  stderr = subprocess.STDOUT)
     self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
 
+    if onLaunch is not None:
+      # Allow callers to specify an onLaunch callback to be fired after the
+      # app is launched.
+      onLaunch()
+
     status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
     self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
 
     # Do a final check for zombie child processes.
     zombieProcesses = self.checkForZombies(processLog)
 
     crashed = self.checkForCrashes(profileDir, symbolsPath)
 
--- a/build/mobile/b2gautomation.py
+++ b/build/mobile/b2gautomation.py
@@ -43,17 +43,17 @@ class B2GRemoteAutomation(Automation):
         self._is_emulator = False
         self.test_script = None
         self.test_script_args = None
 
         # Default our product to b2g
         self._product = "b2g"
         self.lastTestSeen = "b2gautomation.py"
         # Default log finish to mochitest standard
-        self.logFinish = 'INFO SimpleTest FINISHED' 
+        self.logFinish = 'INFO SimpleTest FINISHED'
         Automation.__init__(self)
 
     def setEmulator(self, is_emulator):
         self._is_emulator = is_emulator
 
     def setDeviceManager(self, deviceManager):
         self._devicemanager = deviceManager
 
@@ -80,17 +80,17 @@ class B2GRemoteAutomation(Automation):
         # so no copying of os.environ
         if env is None:
             env = {}
 
         # We always hide the results table in B2G; it's much slower if we don't.
         env['MOZ_HIDE_RESULTS_TABLE'] = '1'
         return env
 
-    def waitForNet(self): 
+    def waitForNet(self):
         active = False
         time_out = 0
         while not active and time_out < 40:
             data = self._devicemanager._runCmd(['shell', '/system/bin/netcfg']).stdout.readlines()
             data.pop(0)
             for line in data:
                 if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)):
                     active = True
@@ -101,25 +101,30 @@ class B2GRemoteAutomation(Automation):
 
     def checkForCrashes(self, directory, symbolsPath):
         # XXX: This will have to be updated after crash reporting on b2g
         # is in place.
         dumpDir = tempfile.mkdtemp()
         self._devicemanager.getDirectory(self._remoteProfile + '/minidumps/', dumpDir)
         crashed = automationutils.checkForCrashes(dumpDir, symbolsPath, self.lastTestSeen)
         try:
-          shutil.rmtree(dumpDir)
+            shutil.rmtree(dumpDir)
         except:
-          print "WARNING: unable to remove directory: %s" % (dumpDir)
+            print "WARNING: unable to remove directory: %s" % (dumpDir)
         return crashed
 
-    def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
+    def initializeProfile(self,  profileDir, extraPrefs=[],
+                          useServerLocations=False,
+                          initialProfile=None):
         # add b2g specific prefs
         extraPrefs.extend(["browser.manifestURL='dummy (bug 772307)'"])
-        return Automation.initializeProfile(self, profileDir, extraPrefs, useServerLocations)
+        return Automation.initializeProfile(self, profileDir,
+                                            extraPrefs,
+                                            useServerLocations,
+                                            initialProfile)
 
     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)
 
@@ -160,17 +165,17 @@ class B2GRemoteAutomation(Automation):
     def getDeviceStatus(self, serial=None):
         # Get the current status of the device.  If we know the device
         # serial number, we look for that, otherwise we use the (presumably
         # only) device shown in 'adb devices'.
         serial = serial or self._devicemanager.deviceSerial
         status = 'unknown'
 
         for line in self._devicemanager._runCmd(['devices']).stdout.readlines():
-            result =  re.match('(.*?)\t(.*)', line)
+            result = re.match('(.*?)\t(.*)', line)
             if result:
                 thisSerial = result.group(1)
                 if not serial or thisSerial == serial:
                     serial = thisSerial
                     status = result.group(2)
 
         return (serial, status)
 
@@ -218,19 +223,19 @@ class B2GRemoteAutomation(Automation):
 
         # reboot device so it starts up with the mochitest profile
         # XXX:  We could potentially use 'stop b2g' + 'start b2g' to achieve
         # a similar effect; will see which is more stable while attempting
         # to bring up the continuous integration.
         if not self._is_emulator:
             self.rebootDevice()
             time.sleep(5)
-            #wait for wlan to come up 
+            #wait for wlan to come up
             if not self.waitForNet():
-                raise Exception("network did not come up, please configure the network" + 
+                raise Exception("network did not come up, please configure the network" +
                                 " prior to running before running the automation framework")
 
         # stop b2g
         self._devicemanager._runCmd(['shell', 'stop', 'b2g'])
         time.sleep(5)
 
         # relaunch b2g inside b2g instance
         instance = self.B2GInstance(self._devicemanager)
@@ -329,15 +334,38 @@ class B2GRemoteAutomation(Automation):
             lines = []
             while True:
                 try:
                     lines.append(self.queue.get_nowait())
                 except Queue.Empty:
                     break
             return '\n'.join(lines)
 
-        def wait(self, timeout = None):
+        def wait(self, timeout=None):
             # this should never happen
             raise Exception("'wait' called on B2GInstance")
 
         def kill(self):
             # this should never happen
             raise Exception("'kill' called on B2GInstance")
+
+
+class B2GDesktopAutomation(Automation):
+
+    def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
+        """ build the application command line """
+
+        cmd = os.path.abspath(app)
+        args = []
+
+        if debuggerInfo:
+            args.extend(debuggerInfo["args"])
+            args.append(cmd)
+            cmd = os.path.abspath(debuggerInfo["path"])
+
+        if self.IS_MAC:
+            args.append("-foreground")
+
+        profileDirectory = profileDir + "/"
+
+        args.extend(("-profile", profileDirectory))
+        args.extend(extraArgs)
+        return cmd, args
--- a/testing/marionette/client/marionette/geckoinstance.py
+++ b/testing/marionette/client/marionette/geckoinstance.py
@@ -1,15 +1,12 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/
 
-import datetime
-import socket
-import time
 from mozrunner import Runner
 
 
 class GeckoInstance(object):
 
     def __init__(self, host, port, bin, profile):
         self.marionette_host = host
         self.marionette_port = port
@@ -26,28 +23,8 @@ class GeckoInstance(object):
             profile = {"preferences": prefs, "restore":False}
         print "starting runner"
         self.runner = Runner.create(binary=self.bin, profile_args=profile, cmdargs=['-no-remote'])
         self.runner.start()
 
     def close(self):
         self.runner.stop()
         self.runner.cleanup()
-
-    def wait_for_port(self, timeout=3000):
-        assert(self.marionette_port)
-        starttime = datetime.datetime.now()
-        while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
-            try:
-                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-                sock.connect((self.marionette_host, self.marionette_port))
-                data = sock.recv(16)
-                print "closing socket"
-                sock.close()
-                if '"from"' in data:
-                    print "got data"
-                    time.sleep(5)
-                    return True
-            except:
-                import traceback
-                print traceback.format_exc()
-            time.sleep(1)
-        return False
--- a/testing/marionette/client/marionette/marionette.py
+++ b/testing/marionette/client/marionette/marionette.py
@@ -1,14 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+import datetime
 import socket
 import sys
+import time
 import traceback
 
 from client import MarionetteClient
 from application_cache import ApplicationCache
 from keys import Keys
 from errors import *
 from emulator import Emulator
 from geckoinstance import GeckoInstance
@@ -122,17 +124,17 @@ class Marionette(object):
         if bin:
             port = int(self.port)
             if not Marionette.is_port_available(port, host=self.host):
                 ex_msg = "%s:%d is unavailable." % (self.host, port)
                 raise MarionetteException(message=ex_msg)
             self.instance = GeckoInstance(host=self.host, port=self.port,
                                           bin=self.bin, profile=self.profile)
             self.instance.start()
-            assert(self.instance.wait_for_port())
+            assert(self.wait_for_port())
 
         if emulator:
             self.emulator = Emulator(homedir=homedir,
                                      noWindow=self.noWindow,
                                      logcat_dir=self.logcat_dir,
                                      arch=emulator,
                                      sdcard=sdcard,
                                      emulatorBinary=emulatorBinary,
@@ -191,16 +193,32 @@ class Marionette(object):
             # This string will get caught by mozharness and will cause it
             # to retry the tests.
             print "Error installing gecko!"
 
             # Exit without a normal exception to prevent mozharness from
             # flagging the error.
             sys.exit()
 
+    def wait_for_port(self, timeout=3000):
+        starttime = datetime.datetime.now()
+        while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
+            try:
+                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                sock.connect((self.host, self.port))
+                data = sock.recv(16)
+                sock.close()
+                if '"from"' in data:
+                    time.sleep(1)
+                    return True
+            except socket.error:
+                pass
+            time.sleep(1)
+        return False
+
     def _send_message(self, command, response_key, **kwargs):
         if not self.session and command not in ('newSession', 'getStatus'):
             raise MarionetteException(message="Please start a session")
 
         message = { 'type': command }
         if self.session:
             message['session'] = self.session
         if kwargs:
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -558,17 +558,19 @@ class Mochitest(object):
   def getLogFilePath(self, logFile):
     """ return the log file path relative to the device we are testing on, in most cases 
         it will be the full path on the local system
     """
     return self.getFullPath(logFile)
 
   def buildProfile(self, options):
     """ create the profile and add optional chrome bits and files if requested """
-    self.automation.initializeProfile(options.profilePath, options.extraPrefs, useServerLocations = True)
+    self.automation.initializeProfile(options.profilePath,
+                                      options.extraPrefs,
+                                      useServerLocations=True)
     manifest = self.addChromeToProfile(options)
     self.copyExtraFilesToProfile(options)
     self.installExtensionsToProfile(options)
     return manifest
 
   def buildBrowserEnv(self, options):
     """ build the environment variables for the specific test and operating system """
     browserEnv = self.automation.environment(xrePath = options.xrePath)
@@ -677,17 +679,17 @@ class Mochitest(object):
                                "recording.")
       try:
         self.vmwareHelper.StopRecording()
       except Exception, e:
         self.automation.log.warning("WARNING | runtests.py | Failed to stop "
                                     "VMware recording: (%s)" % str(e))
       self.vmwareHelper = None
 
-  def runTests(self, options):
+  def runTests(self, options, onLaunch=None):
     """ Prepare, configure, run tests and cleanup """
     debuggerInfo = getDebuggerInfo(self.oldcwd, options.debugger, options.debuggerArgs,
                       options.debuggerInteractive);
 
     self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
 
     browserEnv = self.buildBrowserEnv(options)
     if browserEnv is None:
@@ -724,23 +726,24 @@ class Mochitest(object):
 
     if options.vmwareRecording:
       self.startVMwareRecording(options);
 
     self.automation.log.info("INFO | runtests.py | Running tests: start.\n")
     try:
       status = self.automation.runApp(testURL, browserEnv, options.app,
                                   options.profilePath, options.browserArgs,
-                                  runSSLTunnel = self.runSSLTunnel,
-                                  utilityPath = options.utilityPath,
-                                  xrePath = options.xrePath,
+                                  runSSLTunnel=self.runSSLTunnel,
+                                  utilityPath=options.utilityPath,
+                                  xrePath=options.xrePath,
                                   certPath=options.certPath,
                                   debuggerInfo=debuggerInfo,
                                   symbolsPath=options.symbolsPath,
-                                  timeout = timeout)
+                                  timeout=timeout,
+                                  onLaunch=onLaunch)
     except KeyboardInterrupt:
       self.automation.log.info("INFO | runtests.py | Received keyboard interrupt.\n");
       status = -1
     except:
       self.automation.log.exception("INFO | runtests.py | Received unexpected exception while running application\n")
       status = 1
 
     if options.vmwareRecording:
--- a/testing/mochitest/runtestsb2g.py
+++ b/testing/mochitest/runtestsb2g.py
@@ -1,112 +1,192 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import ConfigParser
 import os
+import shutil
 import sys
 import tempfile
+import threading
 import traceback
 
 sys.path.insert(0, os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))))
 
 from automation import Automation
-from b2gautomation import B2GRemoteAutomation
+from b2gautomation import B2GRemoteAutomation, B2GDesktopAutomation
 from runtests import Mochitest
 from runtests import MochitestOptions
 from runtests import MochitestServer
 
 import devicemanager
 import devicemanagerADB
 
 from marionette import Marionette
 
 
+class B2GMochitestMixin(object):
+
+    def setupCommonOptions(self, options, OOP=True):
+        # set the testURL
+        testURL = self.buildTestPath(options)
+        if len(self.urlOpts) > 0:
+            testURL += "?" + "&".join(self.urlOpts)
+        self.automation.testURL = testURL
+
+        if OOP:
+            OOP_pref = "true"
+            OOP_script = """
+let specialpowers = {};
+let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
+loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js", specialpowers);
+let specialPowersObserver = new specialpowers.SpecialPowersObserver();
+specialPowersObserver.init();
+
+let mm = container.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
+mm.addMessageListener("SPPrefService", specialPowersObserver);
+mm.addMessageListener("SPProcessCrashService", specialPowersObserver);
+mm.addMessageListener("SPPingService", specialPowersObserver);
+mm.addMessageListener("SpecialPowers.Quit", specialPowersObserver);
+mm.addMessageListener("SPPermissionManager", specialPowersObserver);
+
+mm.loadFrameScript(CHILD_LOGGER_SCRIPT, true);
+mm.loadFrameScript(CHILD_SCRIPT_API, true);
+mm.loadFrameScript(CHILD_SCRIPT, true);
+specialPowersObserver._isFrameScriptLoaded = true;
+"""
+        else:
+            OOP_pref = "false"
+            OOP_script = ""
+
+        # Execute this script on start up: loads special powers and sets
+        # the test-container apps's iframe to the mochitest URL.
+        self.automation.test_script = """
+const CHILD_SCRIPT = "chrome://specialpowers/content/specialpowers.js";
+const CHILD_SCRIPT_API = "chrome://specialpowers/content/specialpowersAPI.js";
+const CHILD_LOGGER_SCRIPT = "chrome://specialpowers/content/MozillaLogger.js";
+
+let homescreen = document.getElementById('homescreen');
+let container = homescreen.contentWindow.document.getElementById('test-container');
+container.setAttribute('mozapp', 'http://mochi.test:8888/manifest.webapp');
+
+%s
+
+container.src = '%s';
+""" % (OOP_script, testURL)
+
+        # Set extra prefs for B2G.
+        f = open(os.path.join(options.profilePath, "user.js"), "a")
+        f.write("""
+user_pref("browser.homescreenURL","app://test-container.gaiamobile.org/index.html");
+user_pref("browser.manifestURL","app://test-container.gaiamobile.org/manifest.webapp");
+user_pref("dom.mozBrowserFramesEnabled", %s);
+user_pref("dom.ipc.tabs.disabled", false);
+user_pref("dom.ipc.browser_frames.oop_by_default", false);
+user_pref("dom.mozBrowserFramesWhitelist","app://test-container.gaiamobile.org,http://mochi.test:8888");
+user_pref("marionette.loadearly", true);
+user_pref("marionette.force-local", true);
+""" % OOP_pref)
+        f.close()
+
+
 class B2GOptions(MochitestOptions):
 
     def __init__(self, automation, scriptdir, **kwargs):
         defaults = {}
         MochitestOptions.__init__(self, automation, scriptdir)
 
         self.add_option("--b2gpath", action="store",
-                    type = "string", dest = "b2gPath",
-                    help = "path to B2G repo or qemu dir")
+                        type="string", dest="b2gPath",
+                        help="path to B2G repo or qemu dir")
         defaults["b2gPath"] = None
 
+        self.add_option("--desktop", action="store_true",
+                        dest="desktop",
+                        help="Run the tests on a B2G desktop build")
+        defaults["desktop"] = False
+
         self.add_option("--marionette", action="store",
-                    type = "string", dest = "marionette",
-                    help = "host:port to use when connecting to Marionette")
+                        type="string", dest="marionette",
+                        help="host:port to use when connecting to Marionette")
         defaults["marionette"] = None
 
         self.add_option("--emulator", action="store",
-                    type="string", dest = "emulator",
-                    help = "Architecture of emulator to use: x86 or arm")
+                        type="string", dest="emulator",
+                        help="Architecture of emulator to use: x86 or arm")
         defaults["emulator"] = None
 
-        self.add_option("--sdcard", action="store", 
-                    type="string", dest = "sdcard", 
-                    help = "Define size of sdcard: 1MB, 50MB...etc")
+        self.add_option("--sdcard", action="store",
+                        type="string", dest="sdcard",
+                        help="Define size of sdcard: 1MB, 50MB...etc")
         defaults["sdcard"] = None
 
         self.add_option("--no-window", action="store_true",
-                    dest = "noWindow",
-                    help = "Pass --no-window to the emulator")
+                        dest="noWindow",
+                        help="Pass --no-window to the emulator")
         defaults["noWindow"] = False
 
         self.add_option("--adbpath", action="store",
-                    type = "string", dest = "adbPath",
-                    help = "path to adb")
+                        type="string", dest="adbPath",
+                        help="path to adb")
         defaults["adbPath"] = "adb"
 
         self.add_option("--deviceIP", action="store",
-                    type = "string", dest = "deviceIP",
-                    help = "ip address of remote device to test")
+                        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")
+                        type="string", dest="devicePort",
+                        help="port of remote device to test")
         defaults["devicePort"] = 20701
 
         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.")
+                        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
 
-        self.add_option("--remote-webserver", action = "store",
-                    type = "string", dest = "remoteWebServer",
-                    help = "ip address where the remote web server is hosted at")
+        self.add_option("--remote-webserver", action="store",
+                        type="string", dest="remoteWebServer",
+                        help="ip address where the remote web server is hosted at")
         defaults["remoteWebServer"] = None
 
-        self.add_option("--http-port", action = "store",
-                    type = "string", dest = "httpPort",
-                    help = "ip address where the remote web server is hosted at")
+        self.add_option("--http-port", action="store",
+                        type="string", dest="httpPort",
+                        help="ip address where the remote web server is hosted at")
         defaults["httpPort"] = automation.DEFAULT_HTTP_PORT
 
-        self.add_option("--ssl-port", action = "store",
-                    type = "string", dest = "sslPort",
-                    help = "ip address where the remote web server is hosted at")
+        self.add_option("--ssl-port", action="store",
+                        type="string", dest="sslPort",
+                        help="ip address where the remote web server is hosted at")
         defaults["sslPort"] = automation.DEFAULT_SSL_PORT
 
-        self.add_option("--pidfile", action = "store",
-                    type = "string", dest = "pidFile",
-                    help = "name of the pidfile to generate")
+        self.add_option("--pidfile", action="store",
+                        type="string", dest="pidFile",
+                        help="name of the pidfile to generate")
         defaults["pidFile"] = ""
 
         self.add_option("--gecko-path", action="store",
                         type="string", dest="geckoPath",
                         help="the path to a gecko distribution that should "
                         "be installed on the emulator prior to test")
         defaults["geckoPath"] = None
+
+        self.add_option("--profile", action="store",
+                        type="string", dest="profile",
+                        help="for desktop testing, the path to the "
+                        "gaia profile to use")
+        defaults["profile"] = None
+
         self.add_option("--logcat-dir", action="store",
                         type="string", dest="logcat_dir",
                         help="directory to store logcat dump files")
         defaults["logcat_dir"] = None
+
         self.add_option('--busybox', action='store',
                         type='string', dest='busybox',
                         help="Path to busybox binary to install on device")
         defaults['busybox'] = None
 
         defaults["remoteTestRoot"] = "/data/local/tests"
         defaults["logFile"] = "mochitest.log"
         defaults["autorun"] = True
@@ -195,17 +275,17 @@ class ProfileConfigParser(ConfigParser.R
                 if key == "__name__":
                     continue
                 if (value is not None) or (self._optcre == self.OPTCRE):
                     key = "=".join((key, str(value).replace('\n', '\n\t')))
                 fp.write("%s\n" % (key))
             fp.write("\n")
 
 
-class B2GMochitest(Mochitest):
+class B2GMochitest(Mochitest, B2GMochitestMixin):
 
     _automation = None
     _dm = None
     localProfile = None
 
     def __init__(self, automation, devmgr, options):
         self._automation = automation
         Mochitest.__init__(self, self._automation)
@@ -218,17 +298,17 @@ class B2GMochitest(Mochitest):
         self.userJS = '/data/local/user.js'
         self.remoteMozillaPath = '/data/b2g/mozilla'
         self.bundlesDir = '/system/b2g/distribution/bundles'
         self.remoteProfilesIniPath = os.path.join(self.remoteMozillaPath, 'profiles.ini')
         self.originalProfilesIni = None
 
     def copyRemoteFile(self, src, dest):
         if self._dm._useDDCopy:
-            self._dm._checkCmdAs(['shell', 'dd', 'if=%s' % src,'of=%s' % dest])
+            self._dm._checkCmdAs(['shell', 'dd', 'if=%s' % src, 'of=%s' % dest])
         else:
             self._dm._checkCmdAs(['shell', 'cp', src, dest])
 
     def origUserJSExists(self):
         return self._dm.fileExists('/data/local/user.js.orig')
 
     def cleanup(self, manifest, options):
         if self.localLog:
@@ -268,17 +348,17 @@ class B2GMochitest(Mochitest):
 
         if options.pidFile != "":
             try:
                 os.remove(options.pidFile)
                 os.remove(options.pidFile + ".xpcshell.pid")
             except:
                 print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % options.pidFile
 
-    def findPath(self, paths, filename = None):
+    def findPath(self, paths, filename=None):
         for path in paths:
             p = path
             if filename:
                 p = os.path.join(p, filename)
             if os.path.exists(self.getFullPath(p)):
                 return path
         return None
 
@@ -380,67 +460,17 @@ class B2GMochitest(Mochitest):
             pass
 
     def buildURLOptions(self, options, env):
         self.localLog = options.logFile
         options.logFile = self.remoteLog
         options.profilePath = self.localProfile
         retVal = Mochitest.buildURLOptions(self, options, env)
 
-        # set the testURL
-        testURL = self.buildTestPath(options)
-        if len(self.urlOpts) > 0:
-            testURL += "?" + "&".join(self.urlOpts)
-        self._automation.testURL = testURL
-
-        # execute this script on start up.
-        # loads special powers and sets the test-container
-        # apps's iframe to the mochitest URL.
-        self._automation.test_script = """
-const CHILD_SCRIPT = "chrome://specialpowers/content/specialpowers.js";
-const CHILD_SCRIPT_API = "chrome://specialpowers/content/specialpowersAPI.js";
-const CHILD_LOGGER_SCRIPT = "chrome://specialpowers/content/MozillaLogger.js";
-
-let homescreen = document.getElementById('homescreen');
-let container = homescreen.contentWindow.document.getElementById('test-container');
-container.setAttribute('mozapp', 'http://mochi.test:8888/manifest.webapp');
-
-let specialpowers = {};
-let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
-loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js", specialpowers);
-let specialPowersObserver = new specialpowers.SpecialPowersObserver();
-specialPowersObserver.init();
-
-let mm = container.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
-mm.addMessageListener("SPPrefService", specialPowersObserver);
-mm.addMessageListener("SPProcessCrashService", specialPowersObserver);
-mm.addMessageListener("SPPingService", specialPowersObserver);
-mm.addMessageListener("SpecialPowers.Quit", specialPowersObserver);
-mm.addMessageListener("SPPermissionManager", specialPowersObserver);
-
-mm.loadFrameScript(CHILD_LOGGER_SCRIPT, true);
-mm.loadFrameScript(CHILD_SCRIPT_API, true);
-mm.loadFrameScript(CHILD_SCRIPT, true);
-specialPowersObserver._isFrameScriptLoaded = true;
-
-container.src = '%s';
-""" % testURL
-
-        # Set extra prefs for B2G.
-        f = open(os.path.join(options.profilePath, "user.js"), "a")
-        f.write("""
-user_pref("browser.homescreenURL","app://test-container.gaiamobile.org/index.html");
-user_pref("browser.manifestURL","app://test-container.gaiamobile.org/manifest.webapp");
-user_pref("dom.mozBrowserFramesEnabled", true);
-user_pref("dom.ipc.tabs.disabled", false);
-user_pref("dom.ipc.browser_frames.oop_by_default", false);
-user_pref("dom.mozBrowserFramesWhitelist","app://test-container.gaiamobile.org,http://mochi.test:8888");
-user_pref("marionette.loadearly", true);
-""")
-        f.close()
+        self.setupCommonOptions(options)
 
         # Copy the profile to the device.
         self._dm._checkCmdAs(['shell', 'rm', '-r', self.remoteProfile])
         try:
             self._dm.pushDir(options.profilePath, self.remoteProfile)
         except devicemanager.DMError:
             print "Automation Error: Unable to copy profile to device."
             raise
@@ -464,74 +494,118 @@ user_pref("marionette.loadearly", true);
             self.copyRemoteFile(self.userJS, '%s.orig' % self.userJS)
         self._dm.pushFile(os.path.join(options.profilePath, "user.js"), self.userJS)
         self.updateProfilesIni(self.remoteProfile)
         options.profilePath = self.remoteProfile
         options.logFile = self.localLog
         return retVal
 
 
-def main():
-    scriptdir = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
-    auto = B2GRemoteAutomation(None, "fennec")
-    parser = B2GOptions(auto, scriptdir)
-    options, args = parser.parse_args()
+class B2GDesktopMochitest(Mochitest, B2GMochitestMixin):
+
+    def __init__(self, automation):
+        #self._automation = automation
+        Mochitest.__init__(self, automation)
+
+    def runMarionetteScript(self, marionette, test_script):
+        assert(marionette.wait_for_port())
+        marionette.start_session()
+        marionette.set_context(marionette.CONTEXT_CHROME)
+        marionette.execute_script(test_script)
+
+    def startTests(self):
+        # This is run in a separate thread because otherwise, the app's
+        # stdout buffer gets filled (which gets drained only after this
+        # function returns, by waitForFinish), which causes the app to hang.
+        thread = threading.Thread(target=self.runMarionetteScript,
+                                  args=(self.automation.marionette,
+                                        self.automation.test_script))
+        thread.start()
+
+    def buildURLOptions(self, options, env):
+        retVal = Mochitest.buildURLOptions(self, options, env)
 
+        self.setupCommonOptions(options, OOP=False)
+
+        # Copy the extensions to the B2G bundles dir.
+        extensionDir = os.path.join(options.profilePath, 'extensions', 'staged')
+        bundlesDir = os.path.join(os.path.dirname(options.app),
+                                  'distribution', 'bundles')
+
+        for filename in os.listdir(extensionDir):
+            shutil.rmtree(os.path.join(bundlesDir, filename), True)
+            shutil.copytree(os.path.join(extensionDir, filename),
+                            os.path.join(bundlesDir, filename))
+
+        return retVal
+
+    def buildProfile(self, options):
+        self.automation.initializeProfile(options.profilePath,
+                                          options.extraPrefs,
+                                          useServerLocations=True,
+                                          initialProfile=options.profile)
+        manifest = self.addChromeToProfile(options)
+        self.copyExtraFilesToProfile(options)
+        self.installExtensionsToProfile(options)
+        return manifest
+
+
+def run_remote_mochitests(automation, parser, options):
     # create our Marionette instance
     kwargs = {}
     if options.emulator:
         kwargs['emulator'] = options.emulator
-        auto.setEmulator(True)
+        automation.setEmulator(True)
         if options.noWindow:
             kwargs['noWindow'] = True
         if options.geckoPath:
             kwargs['gecko_path'] = options.geckoPath
         if options.logcat_dir:
             kwargs['logcat_dir'] = options.logcat_dir
         if options.busybox:
             kwargs['busybox'] = options.busybox
     # needless to say sdcard is only valid if using an emulator
     if options.sdcard:
         kwargs['sdcard'] = options.sdcard
     if options.b2gPath:
         kwargs['homedir'] = options.b2gPath
     if options.marionette:
-        host,port = options.marionette.split(':')
+        host, port = options.marionette.split(':')
         kwargs['host'] = host
         kwargs['port'] = int(port)
 
     marionette = Marionette.getMarionetteOrExit(**kwargs)
 
-    auto.marionette = marionette
+    automation.marionette = marionette
 
     # create the DeviceManager
     kwargs = {'adbPath': options.adbPath,
               'deviceRoot': options.remoteTestRoot}
     if options.deviceIP:
         kwargs.update({'host': options.deviceIP,
                        'port': options.devicePort})
     dm = devicemanagerADB.DeviceManagerADB(**kwargs)
-    auto.setDeviceManager(dm)
-    options = parser.verifyRemoteOptions(options, auto)
+    automation.setDeviceManager(dm)
+    options = parser.verifyRemoteOptions(options, automation)
     if (options == None):
         print "ERROR: Invalid options specified, use --help for a list of valid options"
         sys.exit(1)
 
-    auto.setProduct("b2g")
+    automation.setProduct("b2g")
 
-    mochitest = B2GMochitest(auto, dm, options)
+    mochitest = B2GMochitest(automation, dm, options)
 
     options = parser.verifyOptions(options, mochitest)
     if (options == None):
         sys.exit(1)
 
     logParent = os.path.dirname(options.remoteLogFile)
     dm.mkDir(logParent)
-    auto.setRemoteLog(options.remoteLogFile)
-    auto.setServerInfo(options.webServer, options.httpPort, options.sslPort)
+    automation.setRemoteLog(options.remoteLogFile)
+    automation.setServerInfo(options.webServer, options.httpPort, options.sslPort)
     retVal = 1
     try:
         mochitest.cleanup(None, options)
         retVal = mochitest.runTests(options)
     except:
         print "Automation Error: Exception caught while running tests"
         traceback.print_exc()
         mochitest.stopWebServer(options)
@@ -539,11 +613,55 @@ def main():
         try:
             mochitest.cleanup(None, options)
         except:
             pass
         retVal = 1
 
     sys.exit(retVal)
 
+
+def run_desktop_mochitests(parser, options):
+    automation = B2GDesktopAutomation()
+
+    # create our Marionette instance
+    kwargs = {}
+    if options.marionette:
+        host, port = options.marionette.split(':')
+        kwargs['host'] = host
+        kwargs['port'] = int(port)
+    marionette = Marionette.getMarionetteOrExit(**kwargs)
+    automation.marionette = marionette
+
+    mochitest = B2GDesktopMochitest(automation)
+
+    # b2g desktop builds don't always have a b2g-bin file
+    if options.app[-4:] == '-bin':
+        options.app = options.app[:-4]
+
+    options = MochitestOptions.verifyOptions(parser, options, mochitest)
+    if options == None:
+        sys.exit(1)
+
+    if options.desktop and not options.profile:
+        raise Exception("must specify --profile when specifying --desktop")
+
+    automation.setServerInfo(options.webServer,
+                             options.httpPort,
+                             options.sslPort,
+                             options.webSocketPort)
+    sys.exit(mochitest.runTests(options,
+                                onLaunch=mochitest.startTests))
+
+
+def main():
+    scriptdir = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
+    automation = B2GRemoteAutomation(None, "fennec")
+    parser = B2GOptions(automation, scriptdir)
+    options, args = parser.parse_args()
+
+    if options.desktop:
+        run_desktop_mochitests(parser, options)
+    else:
+        run_remote_mochitests(automation, parser, options)
+
 if __name__ == "__main__":
     main()
-